RathRath Finance
Use Cases

Sending AA Transaction with SDK

Prepare, sign, and relay EIP-7702 ERC-4337 UserOperations using the Tachyon SDK.

The Tachyon SDK wraps the EIP-7702 and ERC-4337 work required to execute a UserOperation through a delegated EOA.

The SDK provides two signing flows:

FlowMethodsUse when
Automatic signingsignAndRelayUserOpA viem wallet client is available
External signinggetUserOpAndHash and relayUserOpSigning happens outside the SDK

Prerequisites

  • Node.js 18+
  • A Tachyon API key
  • A funded EOA wallet
  • An EIP-7702-compatible network
  • An ERC-4337-compatible delegate implementation

Automatic Signing

Use signAndRelayUserOp when the SDK can access a viem wallet client. The SDK checks delegation, creates the authorization when required, prepares and signs the UserOperation, encodes handleOps, and relays the transaction.

Install Dependencies

npm install viem @rathfi/tachyon

Configure the Clients

import {
  createPublicClient,
  createWalletClient,
  encodeFunctionData,
  http,
  parseAbi,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import { Tachyon } from "@rathfi/tachyon";

const account = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);

const publicClient = createPublicClient({
  chain: base,
  transport: http(),
});

const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(),
});

const tachyon = new Tachyon({
  apiKey: process.env.TACHYON_API_KEY!,
});

Encode the Target Call

const delegateContract =
  "0xd6CEDDe84be40893d153Be9d467CD6aD37875b28";
const beneficiary =
  "0x4C16955d8A0DcB2e7826d50f4114990c787b21E7";
const target =
  "0xA7A833e6641D7901F30EaD6f27d4Ee2C9bb670a7";

const callData = encodeFunctionData({
  abi: parseAbi(["function sayHello(string message)"]),
  functionName: "sayHello",
  args: ["Hello from Tachyon!"],
});

Pass only the target contract calldata. The SDK encodes the delegated account's execute call internally.

Sign and Relay

const taskId = await tachyon.signAndRelayUserOp({
  publicClient,
  walletClient,
  delegateContract,
  beneficiary,
  target,
  callData,
  value: BigInt(0),
  delegation: "auto",
  label: "EIP-7702 UserOperation",
});

With delegation: "auto", the SDK creates an EIP-7702 authorization only when the EOA is not already delegated to delegateContract.

Wait for Execution

console.log("Task ID:", taskId);

const transaction = await tachyon.waitForExecutionHash(taskId, 30_000);
console.log("Transaction executed:", transaction);

Complete Automatic Example

import {
  createPublicClient,
  createWalletClient,
  encodeFunctionData,
  http,
  parseAbi,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import { Tachyon } from "@rathfi/tachyon";

const account = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);

const publicClient = createPublicClient({
  chain: base,
  transport: http(),
});

const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(),
});

const tachyon = new Tachyon({
  apiKey: process.env.TACHYON_API_KEY!,
});

const delegateContract =
  "0xd6CEDDe84be40893d153Be9d467CD6aD37875b28";
const beneficiary =
  "0x4C16955d8A0DcB2e7826d50f4114990c787b21E7";
const target =
  "0xA7A833e6641D7901F30EaD6f27d4Ee2C9bb670a7";

const callData = encodeFunctionData({
  abi: parseAbi(["function sayHello(string message)"]),
  functionName: "sayHello",
  args: ["Hello from Tachyon!"],
});

async function main() {
  const taskId = await tachyon.signAndRelayUserOp({
    publicClient,
    walletClient,
    delegateContract,
    beneficiary,
    target,
    callData,
    value: BigInt(0),
    delegation: "auto",
    // nonce: BigInt(42), // Optional
    label: "EIP-7702 UserOperation",
  });

  console.log("Task ID:", taskId);

  const transaction = await tachyon.waitForExecutionHash(taskId, 30_000);
  console.log("Transaction executed:", transaction);
}

main().catch(console.error);

External Signing

Use this alternative when another wallet library, signing service, or hardware device must sign the UserOperation hash.

Skip this section when using signAndRelayUserOp.

Create the Authorization When Required

getUserOpAndHash does not receive a wallet client, so the caller must provide an authorization list when the EOA is not already delegated.

let authorizationList;

const delegatedCode = await publicClient.getCode({
  address: account.address,
});
const expectedDelegation =
  `0xef0100${delegateContract.slice(2).toLowerCase()}`;

if (!delegatedCode?.toLowerCase().startsWith(expectedDelegation)) {
  const authorization = await walletClient.signAuthorization({
    contractAddress: delegateContract,
  });

  const authorizationNonce =
    typeof authorization.nonce === "bigint"
      ? authorization.nonce
      : BigInt(authorization.nonce);

  if (authorizationNonce > BigInt(Number.MAX_SAFE_INTEGER)) {
    throw new Error("Authorization nonce exceeds MAX_SAFE_INTEGER");
  }

  authorizationList = [
    {
      chainId: authorization.chainId,
      address: authorization.address,
      nonce: Number(authorizationNonce),
      r: authorization.r,
      s: authorization.s,
      v: Number(authorization.v),
      yParity: Number(authorization.yParity) as 0 | 1,
    },
  ];
}

Prepare the UserOperation

const prepared = await tachyon.getUserOpAndHash({
  publicClient,
  account: account.address,
  delegateContract,
  beneficiary,
  target,
  callData,
  value: BigInt(0),
  delegation: "auto",
  authorizationList,
});

The result contains the prepared UserOperation and userOperationHash.

Sign the Hash

const signature = await walletClient.signMessage({
  message: { raw: prepared.userOperationHash },
});

Replace this call with your external signer when not using viem.

Relay the Signed UserOperation

const taskId = await tachyon.relayUserOp({
  ...prepared,
  signature,
  label: "EIP-7702 UserOperation external signing",
});

Complete External-Signing Example

import {
  createPublicClient,
  createWalletClient,
  encodeFunctionData,
  http,
  parseAbi,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import { Tachyon } from "@rathfi/tachyon";

const account = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);

const publicClient = createPublicClient({
  chain: base,
  transport: http(),
});

const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(),
});

const tachyon = new Tachyon({
  apiKey: process.env.TACHYON_API_KEY!,
});

const delegateContract =
  "0xd6CEDDe84be40893d153Be9d467CD6aD37875b28";
const beneficiary =
  "0x4C16955d8A0DcB2e7826d50f4114990c787b21E7";
const target =
  "0xA7A833e6641D7901F30EaD6f27d4Ee2C9bb670a7";

const callData = encodeFunctionData({
  abi: parseAbi(["function sayHello(string message)"]),
  functionName: "sayHello",
  args: ["Hello from manual UserOperation signing!"],
});

async function main() {
  let authorizationList;

  const delegatedCode = await publicClient.getCode({
    address: account.address,
  });
  const expectedDelegation =
    `0xef0100${delegateContract.slice(2).toLowerCase()}`;

  if (!delegatedCode?.toLowerCase().startsWith(expectedDelegation)) {
    const authorization = await walletClient.signAuthorization({
      contractAddress: delegateContract,
    });

    const authorizationNonce =
      typeof authorization.nonce === "bigint"
        ? authorization.nonce
        : BigInt(authorization.nonce);

    if (authorizationNonce > BigInt(Number.MAX_SAFE_INTEGER)) {
      throw new Error("Authorization nonce exceeds MAX_SAFE_INTEGER");
    }

    authorizationList = [
      {
        chainId: authorization.chainId,
        address: authorization.address,
        nonce: Number(authorizationNonce),
        r: authorization.r,
        s: authorization.s,
        v: Number(authorization.v),
        yParity: Number(authorization.yParity) as 0 | 1,
      },
    ];
  }

  const prepared = await tachyon.getUserOpAndHash({
    publicClient,
    account: account.address,
    delegateContract,
    beneficiary,
    target,
    callData,
    value: BigInt(0),
    delegation: "auto",
    authorizationList,
    // nonce: BigInt(42), // Optional
  });

  const signature = await walletClient.signMessage({
    message: { raw: prepared.userOperationHash },
  });

  const taskId = await tachyon.relayUserOp({
    ...prepared,
    signature,
    label: "EIP-7702 UserOperation manual signing",
  });

  console.log("Task ID:", taskId);

  const transaction = await tachyon.waitForExecutionHash(taskId, 30_000);
  console.log("Transaction executed:", transaction);
}

main().catch(console.error);

Optional UserOperation Nonce

Both signAndRelayUserOp and getUserOpAndHash accept nonce?: bigint.

When omitted, the SDK fetches the pending nonce from EntryPoint v0.7 by calling getNonce(account, 0). When provided, the SDK uses it directly and skips that RPC call:

const prepared = await tachyon.getUserOpAndHash({
  publicClient,
  account: account.address,
  delegateContract,
  beneficiary,
  target,
  callData,
  authorizationList,
  nonce: BigInt(42),
});

An incorrect, stale, or already-used nonce causes UserOperation validation to fail. The UserOperation nonce is separate from the EOA transaction nonce used by the EIP-7702 authorization.

Key Considerations

  • After the first successful authorization, the EOA remains delegated until a later authorization changes or clears the delegation.
  • delegation: "auto" is recommended for normal use.
  • getUserOpAndHash requires an authorization list when delegation is missing.
  • Override the SDK gas defaults when the delegate implementation or target call requires different limits.
  • Verify that the delegate implementation is trusted and compatible with the SDK's execute(bytes32,bytes) encoding.

On this page