ModuleSDK
Using Modules
Scheduled Transfers

How to use scheduled transfers with the Automations SDK

The Scheduled Transfers Executor allows users to schedule token transfers that are executed at set intervals. Using Rhinestone Automations, these transfers will be executed automatically, securely and without giving up custody of the account or assets. This guide will walk you through installing and using scheduled transfers.

You will need to set up the smart account, install the scheduled transfers module and create an automation.

Install the packages

First, install the required packages. We use the latest version of module sdk and automations sdk, permissionless ^0.2 and viem ^2.21.

npm i viem @rhinestone/module-sdk @rhinestone/automations-sdk permissionless

Import the required functions and constants

import {
  RHINESTONE_ATTESTER_ADDRESS,
  MOCK_ATTESTER_ADDRESS,
  getScheduledTransferData,
  getScheduledTransfersExecutor,
  getExecuteScheduledTransferAction,
  OWNABLE_VALIDATOR_ADDRESS,
  getOwnableValidator,
  encode1271Signature,
  getAccount,
  encode1271Hash,
} from "@rhinestone/module-sdk";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import {
  Address,
  Chain,
  createPublicClient,
  encodeFunctionData,
  http,
  parseAbi,
} from "viem";
import { createSmartAccountClient } from "permissionless";
import { erc7579Actions } from "permissionless/actions/erc7579";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import {
  createPaymasterClient,
  entryPoint07Address,
} from "viem/account-abstraction";
import { toSafeSmartAccount } from "permissionless/accounts";
import { createAutomationClient } from "@rhinestone/automations-sdk";

Create the clients

Create the smart account client, the bundler client and the paymaster client. You will need to add your own urls here.

const publicClient = createPublicClient({
  transport: http(rpcUrl),
  chain: chain,
})
 
const pimlicoClient = createPimlicoClient({
  transport: http(bundlerUrl),
  entryPoint: {
    address: entryPoint07Address,
    version: '0.7',
  },
})
 
const paymasterClient = createPaymasterClient({
  transport: http(paymasterUrl),
})

Create the signer

The Safe account will need to have a signer to sign user operations. In permissionless.js, the default Safe account validates ECDSA signatures.

For example, to create a signer based on a private key:

const owner = privateKeyToAccount(generatePrivateKey())

Create the initial validator

We are also going to add the initial validator. We will use this validator to both install the module and prove ownership of the account as well as giving the automations service the permission to make automations. The first owner address is that of the automations service.

const ownableValidator = getOwnableValidator({
  owners: ["0x2DC2fb2f4F11DeE1d6a2054ffCBf102D09b62bE2", owner.address],
  threshold: 1,
});

Create the Safe account

Create the Safe account object using the signer. Note that you should only use the MockAttester on testnets.

const safeAccount = await toSafeSmartAccount({
  client: publicClient,
  owners: [owner],
  version: '1.4.1',
  entryPoint: {
    address: entryPoint07Address,
    version: '0.7',
  },
  safe4337ModuleAddress: '0x7579EE8307284F293B1927136486880611F20002', // These are not meant to be used in production as of now.
  erc7579LaunchpadAddress: '0x7579011aB74c46090561ea277Ba79D510c6C00ff',
  attesters: [
    RHINESTONE_ATTESTER_ADDRESS, // Rhinestone Attester
    MOCK_ATTESTER_ADDRESS, // Mock Attester - do not use in production
  ],
  attestersThreshold: 1,
  validators: [
    {
      address: ownableValidator.address,
      context: ownableValidator.initData,
    },
  ],
})

Create the smart account client

The smart account client is used to interact with the smart account. You will need to add your own bundler url and the chain that you are using.

const smartAccountClient = createSmartAccountClient({
  account: safeAccount,
  chain: chain,
  bundlerTransport: http(bundlerUrl),
  paymaster: paymasterClient,
  userOperation: {
    estimateFeesPerGas: async () => {
      return (await pimlicoClient.getUserOperationGasPrice()).fast
    },
  },
}).extend(erc7579Actions())

Create the scheduled transfer

First, we will create the scheduled transfer before adding this to both the module and the automations service.

We will create a scheduled transfer to send 1 usdc every 60 seconds for two times and with the defined recipient address.

const executeInterval = 60; // in seconds
const numberOfExecutions = 2;
const startDate = Date.now(); // UNIX timestamp
 
const scheduledTransfer = {
  startDate: startDate,
  repeatEvery: executeInterval,
  numberOfRepeats: numberOfExecutions,
  token: {
    token_address: "0x8034e69FAFEd6588cc36ff3400AFE5c049a3B92E" as Address, // Mock USDC
    decimals: 6,
  },
  amount: 1,
  recipient: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" as Address,
};

Install the scheduled transfer module

Next, we will install the scheduled transfer module. This module will store all our scheduled transfers and make sure that they can only be executed when the scheduled is met.

  const executionData = getScheduledTransferData({
    scheduledTransfer,
  });
 
  const scheduledTransfers = getScheduledTransfersExecutor({
    executeInterval,
    numberOfExecutions,
    startDate,
    executionData,
  });
 
  const opHash = await smartAccountClient.installModule(scheduledTransfers);
 
  await pimlicoClient.waitForUserOperationReceipt({
    hash: opHash,
  });
 

Mint the mock tokens

In this tutorial, we use a Mock USDC token for demonstration purposes. We will now mint some tokens to the created smart account in order to be able to transfer them.

  await smartAccountClient.sendTransaction({
    to: scheduledTransfer.token.token_address,
    data: encodeFunctionData({
      abi: parseAbi(["function mint(address to, uint256 amount) external"]),
      functionName: "mint",
      args: [safeAccount.address, BigInt(10)],
    }),
  });

Get the automation client

Next, we will create the automation client using the Automations SDK. This client will be used to interact with the automations service.

To use this client, we will pass our api key as well as the validator we use, the account address and type and the network, in this case sepolia.

const automationClient = createAutomationClient({
  account: safeAccount.address,
  accountType: "SAFE",
  apiKey: automationsApiKey,
  accountInitCode: "0x",
  network: 11155111,
  validator: OWNABLE_VALIDATOR_ADDRESS,
});

Create the automation

Next, we will create the automation on the automation service. This matches what is stored on the module and will allow the automation service to trigger the automations at the right times and with the required calldata.

const executeScheduledTranferAction = getExecuteScheduledTransferAction({
  jobId: 0, // since this is our first automation on the module
});
 
const actions = [
  {
    type: "static" as const,
    target: executeScheduledTranferAction.target,
    value: Number(executeScheduledTranferAction.value),
    callData: executeScheduledTranferAction.callData,
  },
];
 
const triggerData = {
  cronExpression: "* * * * *",
  startDate: startDate,
};
 
const automation = await automationClient.createAutomation({
  type: "time-based",
  data: {
    trigger: {
      triggerData,
    },
    actions,
    maxNumberOfExecutions: numberOfExecutions,
  },
});

Activate the automation

Our automation has now been created but is not active yet. The reason for this is that anyone could create an automation on behalf of other smart accounts. To prevent this, we require a signature from the smart account to verify that the owner actually wants to create this automation.

const account = getAccount({
  address: safeAccount.address,
  type: "safe",
});
 
const formattedHash = encode1271Hash({
  account,
  validator: OWNABLE_VALIDATOR_ADDRESS,
  chainId: chain.id,
  hash: automation.hash,
});
 
const signature = await owner.signMessage({
  message: { raw: formattedHash },
});
 
const formattedSignature = encode1271Signature({
  account,
  validator: OWNABLE_VALIDATOR_ADDRESS,
  signature,
});
 
await automationClient.signAutomation({
  automationId: automation.id,
  signature: formattedSignature,
});

Wait for the automations

Finally our automation is created and activated. We can now wait for them to happen, such as with a timeout. Then, we can query the logs to see all our executed automations.

await new Promise((resolve) => setTimeout(resolve, 10000));
 
const automationLogs = await automationClient.getAutomationLogs(
  automation.id,
);