0xtrails
Version:
SDK for Trails
530 lines (446 loc) • 14.9 kB
text/typescript
import type { Account, Chain, PublicClient, WalletClient } from "viem"
import {
createPublicClient,
encodeFunctionData,
encodePacked,
http,
pad,
parseAbi,
parseEther,
parseGwei,
toHex,
zeroAddress,
} from "viem"
import type { UserOperation } from "viem/account-abstraction"
import {
createBundlerClient,
createPaymasterClient,
prepareUserOperation,
} from "viem/account-abstraction"
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"
import {
getPermitCalldata,
getPermitSignature,
getTransferCalldata,
getTransferFromCalldata,
} from "./gasless.js"
import { sendUserOperationDirectly } from "./sendUserOp.js"
import type { ToSimpleSmartAccountReturnType } from "./toSimpleSmartAccount.js"
import { toSimpleSmartAccount } from "./toSimpleSmartAccount.js"
import { logger } from "./logger.js"
// --- Interfaces ---
interface PackedCall {
to: `0x${string}`
value: bigint
gasLimit: bigint
behaviorOnError: number
data: `0x${string}`
}
interface Payload {
kind: number
noChainId: boolean
space: number
nonce: number
parentWallets: string[]
calls: PackedCall[]
}
type UserOpWithSignature = UserOperation & { signature: `0x${string}` }
// --- ABIs ---
export const ENTRYPOINT_ABI = [
{
name: "handleOps",
type: "function",
stateMutability: "nonpayable",
inputs: [
{
name: "ops",
type: "tuple[]",
components: [
{ name: "sender", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "initCode", type: "bytes" },
{ name: "callData", type: "bytes" },
{ name: "callGasLimit", type: "uint256" },
{ name: "verificationGasLimit", type: "uint256" },
{ name: "preVerificationGas", type: "uint256" },
{ name: "maxFeePerGas", type: "uint256" },
{ name: "maxPriorityFeePerGas", type: "uint256" },
{ name: "paymasterAndData", type: "bytes" },
{ name: "signature", type: "bytes" },
],
},
{
name: "beneficiary",
type: "address",
},
],
outputs: [],
},
{
name: "getUserOpHash",
type: "function",
stateMutability: "view",
inputs: [
{
name: "userOp",
type: "tuple",
components: [
{ name: "sender", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "initCode", type: "bytes" },
{ name: "callData", type: "bytes" },
{ name: "accountGasLimits", type: "bytes32" },
{ name: "preVerificationGas", type: "uint256" },
{ name: "gasFees", type: "bytes32" },
{ name: "paymasterAndData", type: "bytes" },
{ name: "signature", type: "bytes" },
],
},
],
outputs: [
{
name: "",
type: "bytes32",
},
],
},
]
const ENTRYPOINT_ADDRESS = "0x0000000071727de22e5e9d8baf0edac6f37da032"
// --- Payload packer ---
export function packPayload(payload: Payload): `0x${string}` {
const globalFlag = 0x00
const numCalls = payload.calls.length
const encodedCalls = payload.calls.map((call) => {
const callFlags = 0x4a // has value + gas limit + revert on error
return encodePacked(
["uint8", "address", "uint256", "uint256", "bytes"],
[callFlags, call.to, call.value, call.gasLimit, call.data],
)
})
return encodePacked(
["uint8", "bytes20", "uint8", ...Array(numCalls).fill("bytes")],
[
globalFlag,
pad(toHex(payload.space), { size: 20 }),
numCalls,
...encodedCalls,
],
)
}
// --- Main logic ---
export async function getDelegatorSmartAccount({
publicClient,
}: {
publicClient: PublicClient
}): Promise<ToSimpleSmartAccountReturnType> {
// Create delegator account from private key
const delegatorPrivateKey = generatePrivateKey()
const delegatorAccount = privateKeyToAccount(delegatorPrivateKey)
logger.console.log(
"[trails-sdk] Delegator account:",
delegatorAccount.address,
)
// Initialize delegator smart account
logger.console.log("Creating smart account...")
const delegatorSmartAccount = await toSimpleSmartAccount({
client: publicClient,
entryPoint: {
address: ENTRYPOINT_ADDRESS,
version: "0.7",
},
owner: privateKeyToAccount(delegatorPrivateKey),
})
logger.console.log(
"[trails-sdk] Smart account address:",
delegatorSmartAccount.address,
)
logger.console.log(
"[trails-sdk] delegatorSmartAccount.address",
delegatorSmartAccount.address,
)
return delegatorSmartAccount
}
export async function getPaymasterGaslessTransaction({
walletClient,
chain,
tokenAddress,
amount,
recipient,
delegatorSmartAccount,
}: {
walletClient: WalletClient
chain: Chain
tokenAddress: `0x${string}`
amount: bigint
recipient: `0x${string}`
delegatorSmartAccount: ToSimpleSmartAccountReturnType
}): Promise<{ to: string; data: string; value: string }[]> {
// Initialize clients
const publicClient = createPublicClient({
chain,
transport: http(),
})
if (!walletClient.account) {
throw new Error("No account found")
}
const connectedAccount = walletClient.account.address as `0x${string}`
logger.console.log("[trails-sdk] Transfer amount:", amount.toString())
const { signature, deadline } = await getPermitSignature({
publicClient,
walletClient,
signer: connectedAccount,
spender: delegatorSmartAccount.address,
tokenAddress,
amount,
chain,
})
logger.console.log("[trails-sdk] Received signature:", signature)
// Encode permit call
const permitCalldata = getPermitCalldata({
signer: connectedAccount,
spender: delegatorSmartAccount.address,
amount,
deadline,
signature,
})
// Encode transferFrom call
const transferFromCalldata = getTransferFromCalldata({
signer: connectedAccount,
spender: delegatorSmartAccount.address,
amount,
})
// Encode transfer call to recipient
const transferCalldata = getTransferCalldata({ recipient, amount })
const calls = [
{ to: zeroAddress, data: "0x", value: "0x" },
{ to: tokenAddress, data: permitCalldata, value: "0x" },
{ to: tokenAddress, data: transferFromCalldata, value: "0x" },
{ to: tokenAddress, data: transferCalldata, value: "0x" },
]
return calls
}
export async function sendPaymasterGaslessTransaction({
walletClient,
publicClient,
chain,
paymasterUrl,
delegatorSmartAccount,
calls,
}: {
walletClient: WalletClient
publicClient: PublicClient
chain: Chain
paymasterUrl: string
delegatorSmartAccount: ToSimpleSmartAccountReturnType
calls: { to: string; data: string; value: string }[]
}): Promise<string> {
try {
if (!walletClient.account) {
throw new Error("No account found")
}
// Create relayer wallet
// const relayerAccount = privateKeyToAccount(
// RELAYER_PRIVATE_KEY as `0x${string}`,
// )
// const relayerClient = createWalletClient({
// account: relayerAccount,
// chain,
// transport: http(),
// })
// logger.console.log("[trails-sdk] Relayer address:", relayerAccount.address)
// logger.console.log('[trails-sdk] Staking on entry point...')
// await stakeOnEntryPoint({ relayerClient, relayerAccount, delegatorAddress: delegatorAccount.address, chain })
// const prefundTx = await relayerClient.sendTransaction({
// to: delegatorSmartAccount.address,
// value: parseEther('0.0005'),
// chain: chain
// });
// logger.console.log('[trails-sdk] Prefund tx:', prefundTx);
// const prefundReceipt = await publicClient.waitForTransactionReceipt({ hash: prefundTx });
// logger.console.log('[trails-sdk] Prefund receipt:', prefundReceipt);
logger.console.log("[trails-sdk] Estimating gas fees...")
const fees = await publicClient.estimateFeesPerGas()
const maxPriorityFeePerGas = parseGwei("1") // adjustable
const maxFeePerGas = fees.maxFeePerGas! + maxPriorityFeePerGas
if (paymasterUrl) {
// alchemy bundler doesn't support paymaster client
if (paymasterUrl.includes("alchemy")) {
const bundlerClient = createBundlerClient({
chain: chain,
transport: http(paymasterUrl),
})
logger.console.log("[trails-sdk] preparing user op")
const userOp = await prepareUserOperation(publicClient, {
account: delegatorSmartAccount,
calls,
maxFeePerGas,
maxPriorityFeePerGas,
callGasLimit: 500_000n,
verificationGasLimit: 500_000n,
preVerificationGas: 500_000n,
})
const signedUserOp =
await delegatorSmartAccount.signUserOperation(userOp)
;(userOp as UserOpWithSignature).signature = signedUserOp
logger.console.log("[trails-sdk] Signed user operation:", signedUserOp)
logger.console.log("[trails-sdk] Sending user operation...")
const hash = await bundlerClient.sendUserOperation(userOp)
logger.console.log("[trails-sdk] User operation sent! Hash:", hash)
const receipt = await bundlerClient.waitForUserOperationReceipt({
hash: hash as `0x${string}`,
})
logger.console.log("[trails-sdk] User operation receipt:", receipt)
const txHash = receipt.receipt.transactionHash
if (!txHash) {
throw new Error(
"No transaction hash returned from bundlerClient.sendUserOperation",
)
}
return txHash
} else {
// other bundlers: thirdweb, pimlico, zerodev, etc
const paymasterClient = createPaymasterClient({
transport: http(paymasterUrl),
})
const bundlerClient = createBundlerClient({
chain: chain,
paymaster: paymasterClient,
transport: http(paymasterUrl),
})
logger.console.log("[trails-sdk] preparing user op")
let userOp = await prepareUserOperation(publicClient, {
account: delegatorSmartAccount,
calls,
maxFeePerGas,
maxPriorityFeePerGas,
callGasLimit: 500_000n,
verificationGasLimit: 500_000n,
preVerificationGas: 500_000n,
})
// Add paymaster data
userOp = await bundlerClient.prepareUserOperation({
...userOp,
})
logger.console.log("[trails-sdk] preparedUserOp", userOp)
const signedUserOp =
await delegatorSmartAccount.signUserOperation(userOp)
;(userOp as UserOpWithSignature).signature = signedUserOp
logger.console.log("[trails-sdk] Signed user operation:", signedUserOp)
logger.console.log("[trails-sdk] Sending user operation...")
const hash = await bundlerClient.sendUserOperation(userOp)
logger.console.log("[trails-sdk] User operation sent! Hash:", hash)
const receipt = await bundlerClient.waitForUserOperationReceipt({
hash: hash as `0x${string}`,
})
logger.console.log("[trails-sdk] User operation receipt:", receipt)
const txHash = receipt.receipt.transactionHash
if (!txHash) {
throw new Error(
"No transaction hash returned from bundlerClient.sendUserOperation",
)
}
return txHash
}
} else {
// --- Send user operation directly ---
const RELAYER_PRIVATE_KEY = "" // This is for testing only
logger.console.log("[trails-sdk] preparing user op")
const userOp1 = await prepareUserOperation(publicClient, {
account: delegatorSmartAccount,
calls,
maxFeePerGas,
maxPriorityFeePerGas,
callGasLimit: 500_000n,
verificationGasLimit: 500_000n,
preVerificationGas: 500_000n,
paymasterAndData: "0x",
})
logger.console.log("[trails-sdk] preparedUserOp", userOp1)
const requiredPrefund = getRequiredPrefund(userOp1)
logger.console.log("[trails-sdk] Required prefund:", requiredPrefund)
const balance = await publicClient.getBalance({
address: delegatorSmartAccount.address,
})
logger.console.log("[trails-sdk] Balance:", balance)
const signedUserOp1 =
await delegatorSmartAccount.signUserOperation(userOp1)
;(userOp1 as UserOpWithSignature).signature = signedUserOp1
logger.console.log("[trails-sdk] Signed user operation:", signedUserOp1)
const txHash = await sendUserOperationDirectly({
userOp: userOp1,
relayerPrivateKey: RELAYER_PRIVATE_KEY as `0x${string}`,
chain: chain,
})
if (!txHash) {
throw new Error(
"No transaction hash returned from sendUserOperationDirectly",
)
}
logger.console.log("[trails-sdk] User operation submitted! Hash:", txHash)
return txHash
}
} catch (error) {
logger.console.error("[trails-sdk] Gasless flow error:", error)
throw error
}
}
export type stakeOnEntryPointParams = {
relayerClient: WalletClient
relayerAccount: Account
delegatorAddress: `0x${string}`
chain: Chain
}
export async function stakeOnEntryPoint({
relayerClient,
relayerAccount,
delegatorAddress,
chain,
}: stakeOnEntryPointParams) {
const STAKE_AMOUNT = parseEther("0.01")
const addStakeAbi = parseAbi([
"function addStake(uint32 unstakeDelaySec) public payable",
"function depositTo(address to) public payable",
])
const shouldStake = false
if (shouldStake) {
const addStakeData = encodeFunctionData({
abi: addStakeAbi,
functionName: "addStake",
args: [95000], // unstakeDelaySec needs to be 1 day minimum
})
const txHash = await relayerClient.sendTransaction({
account: relayerAccount,
to: ENTRYPOINT_ADDRESS,
data: addStakeData,
value: STAKE_AMOUNT,
chain: chain,
})
logger.console.log(`[trails-sdk] [Stake] Transaction sent: ${txHash}`)
// const receipt = await relayerClient.waitForTransactionReceipt({ hash: txHash })
// logger.console.log(`[trails-sdk] [Stake] Transaction receipt: ${receipt}`)
}
const depositData = encodeFunctionData({
abi: addStakeAbi,
functionName: "depositTo",
args: [delegatorAddress],
})
const txHash2 = await relayerClient.sendTransaction({
account: relayerAccount,
to: ENTRYPOINT_ADDRESS,
value: STAKE_AMOUNT,
data: depositData,
chain: chain,
})
logger.console.log(`[trails-sdk] [Stake] Transaction sent: ${txHash2}`)
return txHash2
}
export const getRequiredPrefund = (userOperation: UserOperation) => {
const op = userOperation
const requiredGas =
op.verificationGasLimit +
op.callGasLimit +
(op.paymasterVerificationGasLimit || 0n) +
(op.paymasterPostOpGasLimit || 0n) +
op.preVerificationGas
return requiredGas * op.maxFeePerGas
}