Safe Unified Account
SafeMultiChainSigAccountV1 enables chain abstraction for Safe Accounts. It extends the standard Safe Account with multichain signature support via Merkle trees, allowing you to sign UserOperations for multiple chains with a single signature.
Uses EntryPoint v0.9 and EIP-712 typed data with Merkle proofs.
Audits
To learn more about the contracts, visit the Safe 4337 Multi-Chain Signature Module repository.
Import
import { SafeMultiChainSigAccountV1 as SafeAccount } from "abstractionkit";
How to Use
const ownerPublicAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31";
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);
console.log("Account address:", smartAccount.accountAddress);
This class inherits all methods from Safe Account. The methods below are specific to multichain operations or override the base class with multichain defaults.
Methods
initializeNewAccount
Initializes a new SafeMultiChainSigAccountV1 from a list of owners. Only needed on the first transaction when the account has not been deployed yet.
- example.ts
- Param Types
- Return Type
import { SafeMultiChainSigAccountV1 as SafeAccount } from "abstractionkit";
const ownerPublicAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31";
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);
console.log("Account address (sender): " + smartAccount.accountAddress);
| key | type | description | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
owners[] | | Pass the owner(s) address(es) of the account. It can be a single owner account, a multi-sig, or a WebAuthn | ||||||||||||||||||||||||||||||||||||||||||
initCodeOverrides? | | Override values to change the initialization default values |
| key | type | description |
|---|---|---|
ECDSASignature | string | ECDSA signature represented as a string |
WebauthnPublicKey
| key | type | description |
|---|---|---|
authenticatorData | ArrayBuffer | Binary data returned by the authenticator during the Webauthn process |
clientDataFields | string | Fields associated with the client's Webauthn request data |
rs | [bigint, bigint] | Array of two bigints representing the 'r' and 's' values of the signature |
| key | type | description |
|---|---|---|
SafeAccount class | SafeAccountV0_3_0 | An instance of the Safe V3 Account and the initialization parameters |
Source code
createUserOperation
Creates a UserOperation for multichain signing. This overrides the base SafeAccount method to always set isMultiChainSignature: true and use EntryPoint v0.9.
Determines the nonce, fetches gas prices, estimates gas limits, and returns a UserOperation to be signed. You can override any of these values using the overrides parameter.
- example.ts
- Param Types
import {
SafeMultiChainSigAccountV1 as SafeAccount,
MetaTransaction,
} from "abstractionkit";
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);
const transaction: MetaTransaction = {
to: "0xTargetAddress",
value: 0n,
data: "0x",
};
const userOperation = await smartAccount.createUserOperation(
[transaction],
"https://rpc-node-url",
"https://bundler-url",
);
| key | type | description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
MetaTransaction | | MetaTransaction is the type of transaction to construct a Safe operation. |
Source code
signUserOperations
Signs multiple UserOperations across different chains with a single signing session. Builds a Merkle tree from the UserOperation hashes, signs the Merkle root, and returns an array of signatures (one per UserOperation) each containing the Merkle proof for its chain.
If only one UserOperation is provided, falls back to single-chain signing.
- example.ts
- Param Types
- Return Type
import { SafeMultiChainSigAccountV1 as SafeAccount } from "abstractionkit";
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);
// Create UserOperations for each chain
const userOp1 = await smartAccount.createUserOperation([tx], nodeUrl1, bundlerUrl1);
const userOp2 = await smartAccount.createUserOperation([tx], nodeUrl2, bundlerUrl2);
// Sign all UserOperations with one signing session
const signatures = smartAccount.signUserOperations(
[
{ userOperation: userOp1, chainId: 11155111n },
{ userOperation: userOp2, chainId: 11155420n },
],
[ownerPrivateKey],
);
// Attach signatures
userOp1.signature = signatures[0];
userOp2.signature = signatures[1];
| key | type | description |
|---|---|---|
userOperationsToSign | UserOperationToSign[] | Array of UserOperations with their target chain IDs and optional validity windows |
privateKeys | string[] | Private keys of the Safe owners to sign with |
UserOperationToSign
| key | type | description |
|---|---|---|
chainId | bigint | Target chain ID for this UserOperation |
userOperation | UserOperationV9 | The UserOperation to sign |
validAfter? | bigint | Timestamp the signature will be valid after |
validUntil? | bigint | Timestamp the signature will be valid until |
| key | type | description |
|---|---|---|
signatures | string[] | Array of signatures, one per UserOperation. Each contains the Merkle proof for its respective chain. If only one UserOperation is provided, falls back to single-chain signing. |
Source code
getMultiChainSingleSignatureUserOperationsEip712Hash
Static method. Computes the EIP-712 hash of the Merkle tree root for a set of UserOperations. This is the hash that wallet signers (browser extensions, hardware wallets) sign to approve multiple cross-chain operations at once.
- example.ts
- Param Types
- Return Type
import { SafeMultiChainSigAccountV1 as SafeAccount } from "abstractionkit";
const hash = SafeAccount.getMultiChainSingleSignatureUserOperationsEip712Hash([
{ userOperation: userOp1, chainId: 11155111n },
{ userOperation: userOp2, chainId: 11155420n },
]);
| key | type | description |
|---|---|---|
userOperationsToSign | UserOperationToSign[] | Array of UserOperations with their target chain IDs |
overrides? | object | Optional overrides for the Safe 4337 module address |
UserOperationToSign
| key | type | description |
|---|---|---|
chainId | bigint | Target chain ID for this UserOperation |
userOperation | UserOperationV9 | The UserOperation to sign |
validAfter? | bigint | Timestamp the signature will be valid after |
validUntil? | bigint | Timestamp the signature will be valid until |
| key | type | description |
|---|---|---|
hash | string | The EIP-712 hash of the Merkle tree root, ready for signing by wallets |
Source code
getMultiChainSingleSignatureUserOperationsEip712Hash
getMultiChainSingleSignatureUserOperationsEip712Data
Static method. Returns the EIP-712 typed data components (domain, types, message value) for a multi-chain Merkle tree root. Use this for wallet-compatible signing via eth_signTypedData_v4.
- example.ts
- Param Types
- Return Type
import { SafeMultiChainSigAccountV1 as SafeAccount } from "abstractionkit";
const { domain, types, messageValue } =
SafeAccount.getMultiChainSingleSignatureUserOperationsEip712Data([
{ userOperation: userOp1, chainId: 11155111n },
{ userOperation: userOp2, chainId: 11155420n },
]);
// Use with a wallet provider
const signature = await wallet.signTypedData(domain, types, messageValue);
| key | type | description |
|---|---|---|
userOperationsToSign | UserOperationToSign[] | Array of UserOperations with their target chain IDs |
overrides? | object | Optional overrides for module and entrypoint addresses |
UserOperationToSign
| key | type | description |
|---|---|---|
chainId | bigint | Target chain ID for this UserOperation |
userOperation | UserOperationV9 | The UserOperation to sign |
validAfter? | bigint | Timestamp the signature will be valid after |
validUntil? | bigint | Timestamp the signature will be valid until |
| key | type | description |
|---|---|---|
domain | { verifyingContract: string } | The EIP-712 domain with the Safe 4337 module as verifying contract |
types | Record<string, { name: string; type: string }[]> | The EIP-712 type definitions for multi-chain operations |
messageValue | { merkleTreeRoot: string } | The message containing the Merkle tree root of all UserOperation hashes |
Source code
getMultiChainSingleSignatureUserOperationsEip712Data
formatSignaturesToUseroperationsSignatures
Static method. Formats EIP-712 signatures (from wallet signing) into UserOperation signatures with Merkle proofs. Use this after signing with getMultiChainSingleSignatureUserOperationsEip712Hash or getMultiChainSingleSignatureUserOperationsEip712Data.
- example.ts
- Param Types
- Return Type
import { SafeMultiChainSigAccountV1 as SafeAccount } from "abstractionkit";
const userOpsToSign = [
{ userOperation: userOp1, chainId: 11155111n },
{ userOperation: userOp2, chainId: 11155420n },
];
const signerSignaturePairs = [
{ signer: ownerAddress, signature: eip712Signature },
];
const signatures = SafeAccount.formatSignaturesToUseroperationsSignatures(
userOpsToSign,
signerSignaturePairs,
);
userOp1.signature = signatures[0];
userOp2.signature = signatures[1];
| key | type | description |
|---|---|---|
userOperationsToSign | UserOperationToSign[] | Array of UserOperations with their target chain IDs |
signerSignaturePairs | SignerSignaturePair[] | Array of signer address and signature pairs from EIP-712 signing |
overrides? | WebAuthnSignatureOverrides | Optional overrides for WebAuthn verifier addresses |
UserOperationToSign
| key | type | description |
|---|---|---|
chainId | bigint | Target chain ID for this UserOperation |
userOperation | UserOperationV9 | The UserOperation to sign |
validAfter? | bigint | Timestamp the signature will be valid after |
validUntil? | bigint | Timestamp the signature will be valid until |
| key | type | description |
|---|---|---|
signatures | string[] | Array of formatted signatures with Merkle proofs, one per UserOperation |
Source code
formatSignaturesToUseroperationsSignatures
sendUserOperation
Inherited from Safe Account. Sends a signed UserOperation to the bundler.
const account = new SafeAccount(userOperation.sender);
const response = await account.sendUserOperation(userOperation, bundlerUrl);
const receipt = await response.included();
Gas Sponsorship
Use CandidePaymaster for gas sponsorship with multichain operations. The paymaster uses a two-phase flow: commit (before signing) and finalize (after signing).
import { CandidePaymaster } from "abstractionkit";
// One paymaster instance per chain
const paymaster1 = new CandidePaymaster("https://api.candide.dev/public/v3/sepolia");
const paymaster2 = new CandidePaymaster("https://api.candide.dev/public/v3/optimism-sepolia");
// Phase 1: Commit (before signing)
const commitOverrides = {
preVerificationGasPercentageMultiplier: 20,
context: { signingPhase: "commit" as const },
};
const [[commitOp1], [commitOp2]] = await Promise.all([
paymaster1.createSponsorPaymasterUserOperation(
smartAccount, userOperation1, bundlerUrl1, undefined, commitOverrides
),
paymaster2.createSponsorPaymasterUserOperation(
smartAccount, userOperation2, bundlerUrl2, undefined, commitOverrides
),
]);
userOperation1 = commitOp1;
userOperation2 = commitOp2;
// Sign here with signUserOperations...
// Phase 2: Finalize (after signing)
const finalizeOverrides = { context: { signingPhase: "finalize" as const } };
const [[finalOp1], [finalOp2]] = await Promise.all([
paymaster1.createSponsorPaymasterUserOperation(
smartAccount, userOperation1, bundlerUrl1, undefined, finalizeOverrides
),
paymaster2.createSponsorPaymasterUserOperation(
smartAccount, userOperation2, bundlerUrl2, undefined, finalizeOverrides
),
]);
userOperation1 = finalOp1;
userOperation2 = finalOp2;