Actalink Docs
Usage/Acta payments

Create programmable transfer

Steps to create Recurring Transfers

Create wagmi config

In the first step, create a Wagmi configuration file.

src/wagmi.ts
 
import { http, createConfig } from "wagmi";
import { polygon } from "wagmi/chains";
 
export const config = createConfig({
chains: [polygon],
transports: {
[polygon.id]: http(),
},
});
Projects built with Rainbowkit can utilize Rainbowkit's default Wagmi configuration.

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.

src/pages/index.tsx
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>
)
}

On this page