Deploy a Gnosis Safe
Predict the counterfactual Safe address, guard against re-deployment, then deploy a new multisig via the Safe proxy factory.
Components used:
GET_EVM_SAFE_ADDRESS— predict the CREATE2 address before spending gasIS_EVM_SAFE_DEPLOYED— guard: skip if already deployedBUILD_EVM_GNOSIS_SAFE_DEPLOYMENT— build the unsigned deployment transactionBUILD_EVM_TRANSACTION— wrap the deployment data and estimate gasSIGN_WITH_KEY_SHARE— MPC sign the tx hashSIGN_EVM_TRANSACTION— combine unsigned tx + signatureBROADCAST_EVM_TRANSACTION— submit to the networkWAIT_FOR_EVM_TRANSACTION— wait for on-chain confirmation
Code​
import { WorkspaceClient, ComponentModule } from 'caller-sdk';
const workspace = new WorkspaceClient({ apiKey: process.env.WR_API_KEY! });
const RPC_URL = 'https://rpc.ankr.com/eth';
const KEY_ID = 'your-key-id';
const DERIV = [2147483692, 2147483708, 2147483648, 0, 0]; // m/44'/60'/0'/0/0
const DEPLOYER = '0xDeployerAddress'; // EOA that pays gas
async function deployGnosisSafe(
owners: string[],
threshold: number,
saltNonce: string,
) {
// 1. Predict the Safe address (no gas, deterministic)
const { safeAddress } = await workspace
.call(ComponentModule.GET_EVM_SAFE_ADDRESS, {
jsonRpcUrl: RPC_URL,
owners,
threshold,
saltNonce,
})
.promise();
console.log('Predicted Safe address:', safeAddress);
// 2. Guard: skip deployment if Safe already exists
const { deployed } = await workspace
.call(ComponentModule.IS_EVM_SAFE_DEPLOYED, {
jsonRpcUrl: RPC_URL,
address: safeAddress,
})
.promise();
if (deployed) {
console.log('Safe already deployed at', safeAddress);
return safeAddress;
}
// 3. Build the raw deployment transaction (calldata for SafeProxyFactory)
const { deploymentTransaction } = await workspace
.call(ComponentModule.BUILD_EVM_GNOSIS_SAFE_DEPLOYMENT, {
jsonRpcUrl: RPC_URL,
owners,
threshold,
saltNonce: Number(saltNonce),
})
.promise();
// 4. Wrap in a full EVM transaction (gas estimation + EIP-1559 fields)
const { unsignedTransaction, serializedHash } = await workspace
.call(ComponentModule.BUILD_EVM_TRANSACTION, {
jsonRpcUrl: RPC_URL,
from: DEPLOYER,
to: deploymentTransaction.to,
value: '0',
calldata: deploymentTransaction.data,
})
.promise();
// 5. MPC sign
const { signature } = await workspace
.call(ComponentModule.SIGN_WITH_KEY_SHARE, {
keyId: KEY_ID,
derivationPath: DERIV,
messageHash: serializedHash,
})
.promise();
// 6. Assemble signed transaction
const { signedTransaction } = await workspace
.call(ComponentModule.SIGN_EVM_TRANSACTION, {
unsignedTransaction,
signature,
})
.promise();
// 7. Broadcast
const { transactionHash } = await workspace
.call(ComponentModule.BROADCAST_EVM_TRANSACTION, {
jsonRpcUrl: RPC_URL,
signedTransaction,
})
.promise();
console.log('Deployment tx:', transactionHash);
// 8. Wait for confirmation
await workspace
.call(ComponentModule.WAIT_FOR_EVM_TRANSACTION, {
jsonRpcUrl: RPC_URL,
transactionHash,
})
.promise();
console.log('Safe deployed at:', safeAddress);
return safeAddress;
}
// Example: 2-of-3 Safe
const safeAddress = await deployGnosisSafe(
['0xOwner1', '0xOwner2', '0xOwner3'],
2,
'0',
);
Workflow canvas layout​
GET_EVM_SAFE_ADDRESS
│ safeAddress
â–¼
IS_EVM_SAFE_DEPLOYED ──── (deployed = true) ──▶ [stop / return address]
│ deployed = false
â–¼
BUILD_EVM_GNOSIS_SAFE_DEPLOYMENT
│ deploymentTransaction
â–¼
BUILD_EVM_TRANSACTION
│ serializedHash │ unsignedTransaction
▼ │
SIGN_WITH_KEY_SHARE │
│ signature │
└──────────┬──────────────┘
â–¼
SIGN_EVM_TRANSACTION
│ signedTransaction
â–¼
BROADCAST_EVM_TRANSACTION
│ transactionHash
â–¼
WAIT_FOR_EVM_TRANSACTION
Idempotent deployments
Always call IS_EVM_SAFE_DEPLOYED before deploying. The same owners + threshold + saltNonce always produces the same address — calling the factory twice would revert and waste gas.