-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ae40ed0
commit 6e9e2c9
Showing
5 changed files
with
232 additions
and
9 deletions.
There are no files selected for viewing
90 changes: 90 additions & 0 deletions
90
...pages/permissionless/how-to/accounts/use-safe-account-with-multiple-signers.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import VersionWarning from "../../VersionWarning" | ||
|
||
<VersionWarning version="0.2" /> | ||
|
||
# How to create and use a Safe account with multiple signers | ||
|
||
[Safe](https://safe.global) is the most battle-tested Ethereum smart account provider. With their recent release of their ERC-4337 module, it is now possible to plug in Safe accounts to ERC-4337 bundlers and paymasters. This guide will walk you through how to create and use a Safe account with permissionless.js. | ||
|
||
## Steps | ||
|
||
::::steps | ||
|
||
### Import the required packages | ||
|
||
```ts | ||
// [!include ~/snippets/accounts/safe-multi-sig.ts:imports] | ||
``` | ||
|
||
### Create the clients | ||
|
||
First we must create the public, (optionally) pimlico paymaster clients that will be used to interact with the Safe account. | ||
|
||
```ts | ||
// [!include ~/snippets/accounts/safe-multi-sig.ts:clients] | ||
``` | ||
|
||
### Get the owner addresses | ||
|
||
The Safe account will need to have a signer to sign user operations. In permissionless.js, the default Safe account validates ECDSA signatures. [Any permissionless.js-compatible signer](/permissionless/how-to/signers) can be used for the Safe account. | ||
|
||
For example, to create a signer based on a private key: | ||
|
||
```ts | ||
// [!include ~/snippets/accounts/safe-multi-sig.ts:signer] | ||
``` | ||
|
||
### Create the Safe account | ||
|
||
:::info | ||
For a full list of options for creating a Safe account, take a look at the reference documentation page for [`toSafeSmartAccount`](/permissionless/reference/accounts/toSafeSmartAccount). | ||
::: | ||
|
||
With a signer, you can create a Safe account as such: | ||
|
||
```ts | ||
// [!include ~/snippets/accounts/safe-multi-sig.ts:smartAccount] | ||
``` | ||
|
||
:::info | ||
You can also create a Safe account with 7579 module, read more about it [here](/permissionless/how-to/accounts/use-erc7579-account). | ||
::: | ||
|
||
### Create the smart account client | ||
|
||
The smart account client is a permissionless.js client that is meant to serve as an almost drop-in replacement for viem's [walletClient](https://viem.sh/docs/clients/wallet.html). | ||
|
||
```ts | ||
// [!include ~/snippets/accounts/safe-multi-sig.ts:smartAccountClient] | ||
``` | ||
|
||
### Prepare a user operation | ||
|
||
Since we may not have access to all the signers at once, we should prepare a user operation and then submit it later after all the signers have signed. | ||
|
||
```ts | ||
// [!include ~/snippets/accounts/safe-multi-sig.ts:prepare] | ||
``` | ||
|
||
### Collect signatures | ||
|
||
You can use the `SafeSmartAccount.signUserOperation` method to collect signatures from the signers. | ||
|
||
```ts | ||
// [!include ~/snippets/accounts/safe-multi-sig.ts:sign] | ||
``` | ||
|
||
### Submit the user operation | ||
|
||
Once you have the final signature, you can submit the user operation. | ||
|
||
```ts | ||
// [!include ~/snippets/accounts/safe-multi-sig.ts:submit] | ||
``` | ||
|
||
### Understanding the errors | ||
|
||
If you're getting an error that starts with `GS`, it probably means that something went off with the Safe account. Checkout the Safe error codes [here](https://github.com/safe-global/safe-smart-account/blob/main/docs/error_codes.md). | ||
|
||
:::: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// [!region imports] | ||
import { createSmartAccountClient } from "permissionless" | ||
import { createPublicClient, getContract, http, parseEther } from "viem" | ||
import { sepolia } from "viem/chains" | ||
// [!endregion imports] | ||
|
||
// [!region clients] | ||
export const publicClient = createPublicClient({ | ||
chain: sepolia, | ||
transport: http("https://rpc.ankr.com/eth_sepolia"), | ||
}) | ||
|
||
export const paymasterClient = createPimlicoClient({ | ||
transport: http("https://api.pimlico.io/v2/sepolia/rpc?apikey=API_KEY"), | ||
entryPoint: { | ||
address: entryPoint07Address, | ||
version: "0.7", | ||
}, | ||
}) | ||
// [!endregion clients] | ||
|
||
// [!region signer] | ||
import { privateKeyToAccount, toAccount } from "viem/accounts" | ||
import { createPimlicoClient } from "permissionless/clients/pimlico" | ||
import { entryPoint07Address } from "viem/account-abstraction" | ||
import { toSafeSmartAccount } from "permissionless/accounts" | ||
|
||
const ownerOne = "0xPUBLIC-ADDRESS-ONE" | ||
const ownerTwo = "0xPUBLIC-ADDRESS-TWO" | ||
const ownerThree = "0xPUBLIC-ADDRESS-THREE" | ||
// [!endregion signer] | ||
|
||
// [!region smartAccount] | ||
|
||
const owners = [toAccount(ownerOne), toAccount(ownerTwo), toAccount(ownerThree)] | ||
|
||
const safeAccount = await toSafeSmartAccount({ | ||
client: publicClient, | ||
entryPoint: { | ||
address: entryPoint07Address, | ||
version: "0.7", | ||
}, | ||
owners, | ||
saltNonce: 0n, // optional | ||
version: "1.4.1", | ||
}) | ||
// [!endregion smartAccount] | ||
|
||
// [!region smartAccountClient] | ||
const smartAccountClient = createSmartAccountClient({ | ||
account: safeAccount, | ||
chain: sepolia, | ||
paymaster: paymasterClient, | ||
bundlerTransport: http("https://api.pimlico.io/v2/sepolia/rpc?apikey=API_KEY"), | ||
userOperation: { | ||
estimateFeesPerGas: async () => (await paymasterClient.getUserOperationGasPrice()).fast, | ||
}, | ||
}) | ||
// [!endregion smartAccountClient] | ||
|
||
// [!region prepare] | ||
const unSignedUserOperation = await smartAccountClient.prepareUserOperation({ | ||
calls: [ | ||
{ | ||
to: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", | ||
value: parseEther("0.1"), | ||
}, | ||
], | ||
}) | ||
// [!endregion prepare] | ||
|
||
// [!region sign] | ||
import { SafeSmartAccount } from "permissionless/accounts/safe" | ||
|
||
const ownerOneAccount = privateKeyToAccount("0xPRIVATE-KEY-ONE") // this can any LocalAccount | EIP1193Provider | WalletClient | ||
|
||
let partialSignatures = await SafeSmartAccount.signUserOperation({ | ||
version: "1.4.1", | ||
entryPoint: { | ||
address: entryPoint07Address, | ||
version: "0.7", | ||
}, | ||
chainId: sepolia.id, | ||
owners: owners.map((owner) => toAccount(owner.address)), | ||
account: ownerOneAccount, // the owner that will sign the user operation | ||
...unSignedUserOperation, | ||
}) | ||
|
||
const ownerTwoAccount = privateKeyToAccount("0xPRIVATE-KEY-TWO") // this can any LocalAccount | EIP1193Provider | WalletClient | ||
partialSignatures = await SafeSmartAccount.signUserOperation({ | ||
version: "1.4.1", | ||
entryPoint: { | ||
address: entryPoint07Address, | ||
version: "0.7", | ||
}, | ||
chainId: sepolia.id, | ||
owners: owners.map((owner) => toAccount(owner.address)), | ||
account: ownerTwoAccount, // the owner that will sign the user operation | ||
signatures: partialSignatures, | ||
...unSignedUserOperation, | ||
}) | ||
|
||
const ownerThreeAccount = privateKeyToAccount("0xPRIVATE-KEY-THREE") // this can any LocalAccount | EIP1193Provider | WalletClient | ||
const finalSignature = await SafeSmartAccount.signUserOperation({ | ||
version: "1.4.1", | ||
entryPoint: { | ||
address: entryPoint07Address, | ||
version: "0.7", | ||
}, | ||
chainId: sepolia.id, | ||
owners: owners.map((owner) => toAccount(owner.address)), | ||
account: ownerThreeAccount, // the owner that will sign the user operation | ||
signatures: partialSignatures, | ||
...unSignedUserOperation, | ||
}) | ||
|
||
// [!endregion sign] | ||
|
||
// [!region submit] | ||
const userOpHash = await smartAccountClient.sendUserOperation({ | ||
...unSignedUserOperation, | ||
signature: finalSignature, | ||
}) | ||
|
||
const receipt = await smartAccountClient.waitForUserOperationReceipt({ | ||
hash: userOpHash, | ||
}) | ||
// [!endregion submit] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters