#### Setup ```ts import { ethers } from "ethers" // ethers@v5 import multisigForwarderABI from "./multisigForwarder.json" import avoMultisigSafeABI from "./avoMultisigSafe.json" const avocadoProvider = new ethers.providers.JsonRpcProvider("https://rpc.avocado.instadapp.io") const polygonProvider = new ethers.providers.JsonRpcProvider("https://polygon-rpc.com") // Should be connected to chainId 634 (https://rpc.avocado.instadapp.io), before doing any transaction const provider = new ethers.providers.Web3Provider(window.ethereum) const signer = provider.getSigner() const forwarder = new ethers.Contract( "0x46978CD477A496028A18c02F07ab7F35EDBa5A54", // available on 10+ networks multisigForwarderABI, polygonProvider ) ``` #### Get safe address by owner and index ```ts const safeAddress = await forwarder.computeAddress( ownerAddress, index, // we treat index 0 as avocado personal on https://avocado.instadapp.io/ ) ``` #### Check is safe is deployed on a specific network ```ts const isDeployed = await polygonProvider.getCode(safeAddress).then((code) => code !== '0x') ``` #### Get safe name and version on a specific network ```ts let name, version if (isDeployed) { const safeContract = new ethers.Contract( safeAddress, avoMultisigSafeABI, polygonProvider ); [name, version] = await Promise.all([ safeContract.DOMAIN_SEPARATOR_NAME(), safeContract.DOMAIN_SEPARATOR_VERSION(), ]) } else { [name, version] = await Promise.all([ forwarder.avocadoVersionName(ownerAddress, index), forwarder.avocadoVersion(ownerAddress, index), ]) } ``` #### Get nonce, signers, requireSigners, owner ```ts const safeContract = new ethers.Contract( safeAddress, avoMultisigSafeABI, polygonProvider ); const nonce = isDeployed ? await contract.avoNonce() : 0 const signers = isDeployed ? await contract.signers() : [ownerAddress] const requiredSigners = isDeployed ? await contract.requiredSigners() : 1 const owner = isDeployed ? await contract.owner() : "probably the signer?" ``` #### Generate a message for signing and executing ```ts // Message types interface IMessageParams { id: number // id for actions, e.g. 0 = CALL, 1 = MIXED (call and delegatecall), 20 = FLASHLOAN_CALL, 21 = FLASHLOAN_MIXED. Default value of 0 will work for all most common use-cases. salt: string // salt to customize non-sequential nonce (if `avoSafeNonce` is set to -1), we recommend at least to send `Date.now()` source: string // source address for referral system actions: IMessageAction[] metadata: string // generic additional metadata avoNonce: string | number // sequential avoNonce as current value on the smart wallet contract or set to `-1`to use a non-sequential nonce } interface IMessageAction { target: string data: string value: string operation: string } interface IMessageForwardParams { gas: string // minimum amount of gas that the relayer (AvoForwarder) is expected to send along for successful execution gasPrice: string // maximum gas price at which the signature is valid and can be executed. Not implemented yet. validAfter: string // time in seconds after which the signature is valid and can be executed validUntil: string // time in seconds until which the signature is valid and can be executed value: string | number } interface IMessage { params: IMessageParams forwardParams: IMessageForwardParams } // example message const message: IMessage = { params: { actions: [{ target: "0x0000000000000000000000000000000000000000", data: "0x", value: "0x", operation: "0", }], id: '0', avoNonce: "0", // const nonce = isDeployed ? await contract.avoNonce() : 0 salt: ethers.utils.defaultAbiCoder.encode(['uint256'], [Date.now()]), source: '0x000000000000000000000000000000000000Cad0', metadata: '0x00', }, forwardParams: { gas: '0', gasPrice: '0', validUntil: '0', validAfter: '0', value: '0', } } ``` #### Estimate usdc gas using avocado rpc ```ts interface IEstimatedFeeData { fee: string; multiplier: string; discount: IEstimatedDiscount; } interface IEstimatedDiscount { amount: number transactionCount: number program: string name: string description: string } const data: IEstimatedFeeData = await avocadoProvider.send("txn_multisigEstimateFeeWithoutSignature", [ message, ]) ``` #### Sign the message ```ts const types = { Cast: [ { name: "params", type: "CastParams" }, { name: "forwardParams", type: "CastForwardParams" }, ], CastParams: [ { name: "actions", type: "Action[]" }, { name: "id", type: "uint256" }, { name: "avoNonce", type: "int256" }, { name: "salt", type: "bytes32" }, { name: "source", type: "address" }, { name: "metadata", type: "bytes" }, ], Action: [ { name: "target", type: "address" }, { name: "data", type: "bytes" }, { name: "value", type: "uint256" }, { name: "operation", type: "uint256" }, ], CastForwardParams: [ { name: "gas", type: "uint256" }, { name: "gasPrice", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validUntil", type: "uint256" }, { name: "value", type: "uint256" }, ], } const domain = { name: "safe name", version, "safe version", chainId: "634, verifyingContract: "safeAddress", salt: ethers.utils.solidityKeccak256(['uint256'], [chainId]), } // make sure you are on chain id 634 const avoSigner = provider.getSigner() const signature = await provider._signTypedData(domain, types, message) ``` #### Execute & Broadcast ##### Personal (index == 0 & requiredSigners == 0) ```ts const txHash = await avocadoProvider.send("txn_broadcast",[ { signatures: [ { signature: "0xsignature", signer: "0xsigner" } ], message: {}, // generated message owner: "safe owner", safe: "safe address", index: "0", targetChainId: "137", executionSignature: undefined, // required when index > 0 || signatures.length > 1 } ]) ``` ##### Multisig (index > 0 || signatures.length > 1) :warning: we might change executionSignature logic later, its not final ```ts const executionTypes = { Cast: [ { name: "params", type: "CastParams" }, { name: "forwardParams", type: "CastForwardParams" }, { name: "signatures", type: "SignatureParams[]" }, ], CastParams: [ { name: "actions", type: "Action[]" }, { name: "id", type: "uint256" }, { name: "avoNonce", type: "int256" }, { name: "salt", type: "bytes32" }, { name: "source", type: "address" }, { name: "metadata", type: "bytes" }, ], Action: [ { name: "target", type: "address" }, { name: "data", type: "bytes" }, { name: "value", type: "uint256" }, { name: "operation", type: "uint256" }, ], CastForwardParams: [ { name: "gas", type: "uint256" }, { name: "gasPrice", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validUntil", type: "uint256" }, { name: "value", type: "uint256" }, ], SignatureParams: [ { name: "signature", type: "bytes" }, { name: "signer", type: "address" }, ] } const domain = { name: "safe name", version, "safe version", chainId: "634, verifyingContract: "safeAddress", salt: ethers.utils.solidityKeccak256(['uint256'], [chainId]), } const executionSignature = await provider._signTypedData(domain, executionTypes, message) const txHash = await avocadoProvider.send("txn_broadcast",[ { signatures: [ { signature: "0xsignature1", signer: "0xsigner1" }, { signature: "0xsignature2", signer: "0xsigner2" } ], message: {}, // generated message owner: "safe owner", safe: "safe address", index: "0", targetChainId: "137", executionSignature, } ]) ``` #### Get all safes for a signer ```ts const safes = await avocadoProvider.send("api_getSafes",[{ address: "0xeoa", multisig: true }]) const safe = await avocadoProvider.send("api_getSafe",["0xsafeAddress"]) ``` #### Get usdc balance by safe address ```ts import BigNumber from "bignumber.js"; const balance = await avocadoProvider.send("api_getSafe",["0xsafeAddress"]) // or ",["0xsafeAddress", "success"]) const balanceWithPendingDeposits = await avocadoProvider.send("api_getSafe",["0xsafeAddress", "pending"]) console.log("Amount: " + new BigNumber(balance).dividedBy(10 ** 18).toFormat()) ```