Create programmable transfer
Steps to create Recurring Transfers
Create wagmi config
In the first step, create a Wagmi configuration file.
import { http, createConfig } from "wagmi";
import { polygon } from "wagmi/chains";
export const config = createConfig({
chains: [polygon],
transports: {
[polygon.id]: http(),
},
});
Create smart account instance
Use the useActaAccount()
hook to create smart account instance by passing the required parameters. Mock validators can be used for development and testing, but for production, please contact us to obtain custom validators.
The useSalt()
hook requests a signature to generate a unique salt, which is then used to precompute the smart wallet address.
import {useAccount} from "wagmi";
import { useActaAccount, useFees, useMerkleSignUserOps, useSalt } from "@actalink/react-hooks";
import { config } from "../wagmi";
export default function Home() {
const { address: eoaAddress, status: eoaStatus, chainId } = useAccount();
const { salt } = useSalt({ eoaAddress, eoaStatus, config }); // this hook generates a unique salt using wallet signature
const validators = ["0x", "0x"]; // use your own validators
const { address: swAddress, status: swStatus, actaAccount } = useActaAccount({ eoaAddress, eoaStatus, chainId, config, validators });
return ()
}
Insert required parameters
Pass the required parameters, such as Paymaster URL, Paymaster Address, Token Address, etc. Additionally, useFees() is required for fee calculation, useMerkleSignUserOps() is needed for the Merkle operations signing process and useNonceKeys() is need to get unused validators.
import {useAccount} from "wagmi";
import { useActaAccount, useFees, useMerkleSignUserOps, useSalt } from "@actalink/react-hooks";
import { config } from "../wagmi";
import { PAYMASTER_URL, PAYMASTER_ADDRESS, generateExecutionTimes, getDefaultUserOp } from "../utils";
export default function Home() {
const { address: eoaAddress, status: eoaStatus, chainId } = useAccount();
const TOKEN_ADDRESS = "<erc20 token address
const { salt } = useSalt({ eoaAddress, eoaStatus, config }); // this hook generates a unique salt using wallet signature
const validators = ["0x", "0x"]; // use your own validators
const { address: swAddress, status: swStatus, actaAccount } = useActaAccount({ eoaAddress, eoaStatus, chainId, config, validators });
console.log(swAddress);
const { calculateActaFees, getActaFeesRecipients, getPaymasterfees } = useFees({ config });
const { createERC20Transfer } = useMerkleSignUserOps({ eoaAddress: eoaAddress, config });
return ()
}
Create Recurring transfers
Create a recurring transfer of an ERC20 token to the specified receiver address. The specified amount will be deducted at the selected frequency (e.g., monthly, weekly, yearly) until the defined transfer volume
is reached (e.g., 3 times, 6 times, 12 times, etc.).
import {useAccount} from "wagmi";
import { Address, encodeFunctionData, encodePacked, erc20Abi, getAddress, parseUnits, zeroAddress } from "viem";
import { UserOperation } from "viem/account-abstraction";
import { createTransferCallData } from "@actalink/modules";
import { toSignedPaymasterData } from "@actalink/sdk";
import { useActaAccount, useFees, useMerkleSignUserOps, useSalt } from "@actalink/react-hooks";
import { config } from "../wagmi";
import { v4 as uuidv4 } from "uuid";
import { PAYMASTER_URL, PAYMASTER_ADDRESS, generateExecutionTimes, getDefaultUserOp } from "../utils";
export default function Home() {
...
const { createERC20Transfer } = useMerkleSignUserOps({ eoaAddress: eoaAddress, config });
const createERC20RecurringPayment = async (
recipientAddr: Address,
executionTimes: Array<number>,
amount: bigint,
times: number
) => {
try {
if (actaAccount === undefined) return;
// Fetch the list of unused validators. A higher number of validators allows for more recurring transfers.
const unusedValidators = await getPendingNonceKeys(PAYMASTER_URL, validators, salt as Hex);
if (unusedValidators === undefined || unusedValidators?.length === 0) {
throw new Error("Transfers schedule limit exceed.");
}
// Read value of Fees charged by Actalink
const actaFees = await calculateActaFees(amount, unusedValidators[0]);
// Read value of Fees charged by Paymaster
const paymasterFees = await getPaymasterfees(unusedValidators[0]);
// Read the addresses of Actalink Fee receiver and paymaster Fee receiver
const { actaFeesRecipient, paymasterFeesRecipient } = await getActaFeesRecipients(unusedValidators[0]);
const userOps: Array<UserOperation<"0.7">> = [];
const { factory, factoryData } = await actaAccount.getFactoryArgs();
// Read latest nonce value of smart account for unusedValidators[0]
const nonce = await actaAccount.getValidatorNonce(unusedValidators[0]);
if (swAddress && actaFees !== undefined && nonce) {
// create recurring transfer calldata
const transferData = await createTransferCallData(
eoaAddress, // EOA address of owner or spender
recipientAddr, // Receiver address
TOKEN_ADDRESS, // ERC20 token address
amount, // ERC20 token transfer amount. Amount should be parsed in token decimal units
actaFees, // value of Actalink fees
paymasterFees, // value of Paymaster fees
actaFeesRecipient, // Address of Actalink fee receiver
paymasterFeesRecipient // Address of Paymaster fee receiver
);
for (let i = 0; i < times; i++) {
const preOp: UserOperation<"0.7"> = {
...getDefaultUserOp(),
sender: swAddress,
nonce: nonce + BigInt(i),
callData: transferData,
paymaster: PAYMASTER_ADDRESS,
paymasterData: encodePacked(
["address", "uint128", "uint128"],
[PAYMASTER_ADDRESS, 100000n, 500000n]
),
};
// Request Paymaster to sponsor the transfer
const sponsoredUserOp = await toSignedPaymasterData(
`${PAYMASTER_URL}/api/sign/v2`,
preOp
);
const userOp: UserOperation<"0.7"> = {
...sponsoredUserOp,
paymaster: PAYMASTER_ADDRESS,
};
userOps.push(userOp);
}
}
// Function used to sign the operations
await createERC20Transfer({
userOps: userOps,
executionTimes: executionTimes,
paymasterUrl: PAYMASTER_URL,
paymentType: "transfers",
});
} catch (error) {
console.error("Error in createERC20RecurringPayment: ", error);
}
};
const createTransaction = async () => {
const frequency = "month";
const volume = 2;
const receiver = "0x48F15519186A11567bBB3D4cDd1bC3f6778765A8" // replace with yours receiver address
const amount = "1"
const tokenDecimals = 6;
const execTimes = generateExecutionTimes( Date.now() + 3 * 60 * 1000, frequency, volume ); // for logic to calculate execution time please visit here: link
if (amount === "0" || receiver === zeroAddress || volume === 0) {
console.log("please fill all fields");
return;
}
const usdcAmount = parseUnits(amount, tokenDecimals);
await createERC20RecurringPayment(receiver, execTimes, usdcAmount, volume);
};
return (
<button className="w-full mt-2 py-2 bg-black text-white font-bold rounded-lg" onClick={(e) => { createTransaction() }}>
Submit
</button>
)
}
NOTE: Ensure the smart wallet has sufficient allowance before creating a programmable transfer.
The required allowance is calculated as:
totalAllowanceRequired = (transferAmount + actaFees + paymasterFees) * volume
Refer to this code example to calculate the required allowance.
👏 Congratulations! Your have successfully scheduled a recurring transfer.
Full Code
import {useAccount} from "wagmi";
import { Address, encodeFunctionData, encodePacked, erc20Abi, getAddress, parseUnits, zeroAddress } from "viem";
import { UserOperation } from "viem/account-abstraction";
import { createTransferCallData } from "@actalink/modules";
import { toSignedPaymasterData } from "@actalink/sdk";
import { useActaAccount, useFees, useMerkleSignUserOps, useSalt } from "@actalink/react-hooks";
import { config } from "../wagmi";
import { v4 as uuidv4 } from "uuid";
import { PAYMASTER_URL, PAYMASTER_ADDRESS, generateExecutionTimes, getDefaultUserOp } from "../utils";
export default function Home() {
const { address: eoaAddress, status: eoaStatus, chainId } = useAccount();
const TOKEN_ADDRESS = "<erc20 token address
const { salt } = useSalt({ eoaAddress, eoaStatus, config }); // this hook generates a unique salt using wallet signature
const validators = ["0x", "0x"]; // use your own validators
const { address: swAddress, status: swStatus, actaAccount } = useActaAccount({ eoaAddress, eoaStatus, chainId, config, validators });
console.log(swAddress);
const { calculateActaFees, getActaFeesRecipients, getPaymasterfees } = useFees({ config });
const { createERC20Transfer } = useMerkleSignUserOps({ eoaAddress: eoaAddress, config });
const createERC20RecurringPayment = async (
recipientAddr: Address,
executionTimes: Array<number>,
amount: bigint,
times: number
) => {
try {
if (actaAccount === undefined) return;
// Fetch the list of unused validators. A higher number of validators allows for more recurring transfers.
const unusedValidators = await getPendingNonceKeys(PAYMASTER_URL, validators, salt as Hex);
if (unusedValidators === undefined || unusedValidators?.length === 0) {
throw new Error("Transfers schedule limit exceed.");
}
// Read value of Fees charged by Actalink
const actaFees = await calculateActaFees(amount, unusedValidators[0]);
// Read value of Fees charged by Paymaster
const paymasterFees = await getPaymasterfees(unusedValidators[0]);
// Read the addresses of Actalink Fee receiver and paymaster Fee receiver
const { actaFeesRecipient, paymasterFeesRecipient } = await getActaFeesRecipients(unusedValidators[0]);
const userOps: Array<UserOperation<"0.7">> = [];
const { factory, factoryData } = await actaAccount.getFactoryArgs();
// Read latest nonce value of smart account for unusedValidators[0]
const nonce = await actaAccount.getValidatorNonce(unusedValidators[0]);
if (swAddress && actaFees !== undefined && nonce) {
// create recurring transfer calldata
const transferData = await createTransferCallData(
eoaAddress, // EOA address of owner or spender
recipientAddr, // Receiver address
TOKEN_ADDRESS, // ERC20 token address
amount, // ERC20 token transfer amount. Amount should be parsed in token decimal units
actaFees, // value of Actalink fees
paymasterFees, // value of Paymaster fees
actaFeesRecipient, // Address of Actalink fee receiver
paymasterFeesRecipient // Address of Paymaster fee receiver
);
for (let i = 0; i < times; i++) {
const preOp: UserOperation<"0.7"> = {
...getDefaultUserOp(),
sender: swAddress,
nonce: nonce + BigInt(i),
callData: transferData,
paymaster: PAYMASTER_ADDRESS,
paymasterData: encodePacked(
["address", "uint128", "uint128"],
[PAYMASTER_ADDRESS, 100000n, 500000n]
),
};
// Request Paymaster to sponsor the transfer
const sponsoredUserOp = await toSignedPaymasterData(
`${PAYMASTER_URL}/api/sign/v2`,
preOp
);
const userOp: UserOperation<"0.7"> = {
...sponsoredUserOp,
paymaster: PAYMASTER_ADDRESS,
};
userOps.push(userOp);
}
}
// Function used to sign the operations
await createERC20Transfer({
userOps: userOps,
executionTimes: executionTimes,
paymasterUrl: PAYMASTER_URL,
paymentType: "transfers",
});
} catch (error) {
console.error("Error in createERC20RecurringPayment: ", error);
}
};
const createTransaction = async () => {
const frequency = "month";
const volume = 2;
const receiver = "0x48F15519186A11567bBB3D4cDd1bC3f6778765A8" // replace with yours receiver address
const amount = "1"
const tokenDecimals = 6;
const execTimes = generateExecutionTimes( Date.now() + 3 _ 60 _ 1000, frequency, volume ); // for logic to calculate execution time please visit here: link
if (amount === "0" || receiver === zeroAddress || volume === 0) {
console.log("please fill all fields");
return;
}
const usdcAmount = parseUnits(amount, tokenDecimals);
await createERC20RecurringPayment(receiver, execTimes, usdcAmount, volume);
};
return (
<button className="w-full mt-2 py-2 bg-black text-white font-bold rounded-lg" onClick={(e) => { createTransaction() }}>
Submit
</button>
)
}