Full example USDC (ERC20) transfer
The below example is the full code for a USDC transfer on Polygon, from an Avocado personal wallet (index = 0). It uses a sequential nonce.
import { ethers } from "ethers"; // ethers@v5
import avocadoV1ABI from "./avocado-v1-abi.json";
import avoForwarderV1ABI from "./avo-forwarder-v1-abi.json";
// -------------------------------- Types etc. -----------------------------------
interface ITransactionParams {
id: string; // 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 `avoNonce` is set to -1), we recommend at least to send `Date.now()`
source: string; // source address for referral system
actions: ITransactionAction[]; // actions to execute
metadata: string; // generic additional metadata
avoNonce: string; // sequential avoNonce as current value on the smart wallet contract or set to `-1`to use a non-sequential nonce
}
interface ITransactionAction {
target: string; // the target address to execute the action on
data: string; // the calldata to be passed to the call for each target
value: string; // the msg.value to be passed to the call for each target. set to 0 if none
operation: string; // type of operation to execute: 0 -> .call; 1 -> .delegateCall, 2 -> flashloan (via .call)
}
interface ITransactionForwardParams {
gas: string; // minimum amount of gas that the relayer (AvoForwarder) is expected to send along for successful execution
gasPrice: string; // UNUSED: 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; // UNUSED: msg.value that broadcaster should send along. Not implemented yet.
}
interface ITransactionPayload {
params: ITransactionParams;
forwardParams: ITransactionForwardParams;
}
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" },
],
};
// -------------------------------- Setup -----------------------------------
const avocadoRPCChainId = "634";
const avocadoProvider = new ethers.providers.JsonRpcProvider("https://rpc.avocado.instadapp.io");
// can use any other RPC on the network you want to interact with:
const polygonProvider = new ethers.providers.JsonRpcProvider("https://polygon-rpc.com");
const chainId = (await polygonProvider.getNetwork()).chainId; // e.g. when executing later on Polygon
// Should be connected to chainId 634 (https://rpc.avocado.instadapp.io), before doing any transaction
const provider = new ethers.providers.Web3Provider(window.ethereum);
// request connection
await window.ethereum.request({ method: "eth_requestAccounts" }).catch((err: any) => {
if (err.code === 4001) {
console.log("Please connect to the Web3 wallet.");
} else {
console.error(err);
}
});
const avoForwarderAddress = "0x46978CD477A496028A18c02F07ab7F35EDBa5A54"; // available on 10+ networks
// set up AvoForwarder contract (main interaction point) on e.g. Polygon
const forwarder = new ethers.Contract(avoForwarderAddress, avoForwarderV1ABI, polygonProvider);
const ownerAddress = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; // Vitalik as owner EOA example
const index = "0";
const avocadoAddress = await forwarder.computeAvocado(ownerAddress, index);
// set up Avocado
const avocado = new ethers.Contract(avocadoAddress, avocadoV1ABI, polygonProvider);
const isDeployed = (await polygonProvider.getCode(avocadoAddress)) !== "0x";
// -------------------------------- Read values -----------------------------------
let domainName, domainVersion; // domain separator name & version required for building signatures
if (isDeployed) {
// if avocado is deployed, can read values directly from there
[domainName, domainVersion] = await Promise.all([avocado.DOMAIN_SEPARATOR_NAME(), avocado.DOMAIN_SEPARATOR_VERSION()]);
} else {
// if avocado is not deployed yet, AvoForwarder will resolve to the default values set when deploying the Avocado
[domainName, domainVersion] = await Promise.all([forwarder.avocadoVersionName(ownerAddress, index), forwarder.avocadoVersion(ownerAddress, index)]);
}
const nonce = isDeployed ? await avocado.avoNonce() : "0";
const requiredSigners = isDeployed ? await avocado.requiredSigners() : 1;
if (requiredSigners > 1) {
throw new Error("Example is for Avocado personal with only owner as signer");
}
// -------------------------------- Build transaction payload -----------------------------------
// USDC address on Polygon (different on other networks)
const usdcAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
// Sending "10" USDC (USDC has 6 decimals!)
const usdcAmount = ethers.utils.parseUnits("10", 6);
const reciver = ownerAddress; // sending to owner EOA address
const usdcInterface = new ethers.utils.Interface(["function transfer(address to, uint amount) returns (bool)"]);
const calldata = usdcInterface.encodeFunctionData("transfer", [reciver, usdcAmount]); // create calldata from interface
const action: ITransactionAction = {
target: usdcAddress,
data: calldata,
value: "0",
operation: "0",
};
// transaction with action to transfer USDC
const txPayload: ITransactionPayload = {
params: {
actions: [action],
id: "0",
avoNonce: nonce.toString(), // setting nonce to previously obtained value for sequential avoNonce
salt: ethers.utils.defaultAbiCoder.encode(["uint256"], [Date.now()]),
source: "0x000000000000000000000000000000000000Cad0", // could set source here for referral system
metadata: "0x",
},
forwardParams: {
gas: "0",
gasPrice: "0",
validAfter: "0",
validUntil: "0",
value: "0",
},
};
// -------------------------------- Estimate fee -----------------------------------
const estimate = await avocadoProvider.send("txn_multisigEstimateFeeWithoutSignature", [
{
message: txPayload, // transaction payload as built in previous step
owner: ownerAddress, // avocado owner EOA address
safe: avocadoAddress, // avocado address
index: index,
targetChainId: chainId,
},
]);
// convert fee from hex and 1e18, is in USDC:
console.log("estimate", Number(estimate.fee) / 1e18);
// -------------------------------- Sign -----------------------------------
const domain = {
name: domainName, // see previous steps
version: domainVersion, // see previous steps
chainId: avocadoRPCChainId,
verifyingContract: avocadoAddress, // see previous steps
salt: ethers.utils.solidityKeccak256(["uint256"], [chainId]), // salt is set to actual chain id where execution happens
};
// make sure you are on chain id 634 (to interact with Avocado RPC) with expected owner
const avoSigner = provider.getSigner();
if ((await provider.getNetwork()).chainId !== 634) {
throw new Error("Not connected to Avocado network");
}
if ((await avoSigner.getAddress()) !== ownerAddress) {
throw new Error("Not connected with expected owner address");
}
// transaction payload as built in previous step
const signature = await avoSigner._signTypedData(domain, types, txPayload);
// -------------------------------- Execute -----------------------------------
const txHash = await avocadoProvider.send("txn_broadcast", [
{
signatures: [
{
signature, // signature as built in previous step
signer: ownerAddress, // signer address that signed the signature
},
],
message: txPayload, // transaction payload as built in previous step
owner: ownerAddress, // avocado owner EOA address
safe: avocadoAddress, // avocado address
index,
targetChainId: chainId,
executionSignature: undefined, // not required for Avocado personal
},
]);
// -------------------------------- Check status -----------------------------------
const txDetails = await avocadoProvider.send("api_getTransactionByHash", [txHash]);
// txDetails.status is of type 'pending' | 'success' | 'failed' | 'confirming'
// in case of 'failed', use the error message: txDetails.revertReason
if (txDetails.status === "failed") {
// handle errors
console.log(txDetails.revertReason);
} else {
// status might still be pending or confirming
console.log("tx executed! hash:", txHash, ", Avoscan: https://avoscan.co/tx/" + txHash);
}