Skip to content

Commit

Permalink
Add safe multisig docs
Browse files Browse the repository at this point in the history
  • Loading branch information
plusminushalf committed Dec 27, 2024
1 parent ae40ed0 commit 6e9e2c9
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 9 deletions.
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).

::::

128 changes: 128 additions & 0 deletions docs/snippets/accounts/safe-multi-sig.ts
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]
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"caniuse-lite": "^1.0.30001655",
"lucide-react": "^0.309.0",
"magic-sdk": "^22.1.1",
"permissionless": "0.2.14",
"permissionless": "0.2.24",
"react": "latest",
"react-dom": "latest",
"viem": "^2.21.34",
Expand Down
15 changes: 9 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions vocs.config.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { defineConfig } from "vocs"
import viteConfig from "./utils"
import { link } from "fs"
import { text } from "stream/consumers"

export const platformSidebar = [
{
Expand Down Expand Up @@ -486,6 +484,10 @@ export const permissionlessSidebar = [
text: "How to use a Safe account",
link: "/permissionless/how-to/accounts/use-safe-account",
},
{
text: "How to use a Safe account with multiple signers",
link: "/permissionless/how-to/accounts/use-safe-account-with-multiple-signers",
},
{
text: "How to use a Kernel account",
link: "/permissionless/how-to/accounts/use-kernel-account",
Expand Down

0 comments on commit 6e9e2c9

Please sign in to comment.