UNPKG

@coinbase/cdp-sdk

Version:

SDK for interacting with the Coinbase Developer Platform Wallet API

232 lines (210 loc) 6.85 kB
import { encodeFunctionData, type Abi } from "viem"; import { getBaseNodeRpcUrl } from "../../accounts/evm/getBaseNodeRpcUrl.js"; import { EvmUserOperationNetwork, EvmUserOperationStatus, CdpOpenApiClientType, } from "../../openapi-client/index.js"; import type { EvmSmartAccount } from "../../accounts/evm/types.js"; import type { PaymasterContext } from "../../openapi-client/index.js"; import type { Calls } from "../../types/calls.js"; import type { Address, Hex } from "../../types/misc.js"; // Base interface that all call types implement interface BaseCall { to: Address; value?: bigint; overrideGasLimit?: bigint; } // Direct call interface interface DirectCall extends BaseCall { data?: Hex; } // Contract call interface interface ContractCall extends BaseCall { abi: readonly unknown[] | Abi; functionName: string; args?: readonly unknown[]; } // Type guards /** * Type guard to check if a BaseCall is a ContractCall. * * @param call - The BaseCall to check * @returns True if the call is a ContractCall */ function isContractCall(call: BaseCall): call is ContractCall { return "abi" in call && "functionName" in call; } /** * Options for sending a user operation. * * @template T - Array type for the calls parameter. */ export type SendUserOperationOptions<T extends readonly unknown[]> = { /** The smart account. */ smartAccount: EvmSmartAccount; /** * Array of contract calls to execute in the user operation. * Each call can either be: * - A direct call with `to`, `value`, and `data`. * - A contract call with `to`, `abi`, `functionName`, and `args`. * * @example * ```ts * const calls = [ * { * to: "0x1234567890123456789012345678901234567890", * value: parseEther("0.0000005"), * data: "0x", * }, * { * to: "0x1234567890123456789012345678901234567890", * abi: erc20Abi, * functionName: "transfer", * args: [to, amount], * }, * ] * ``` */ calls: Calls<T>; /** Chain ID of the network to execute on. */ network: EvmUserOperationNetwork; /** Optional URL of the paymaster service to use for gas sponsorship. Must be ERC-7677 compliant. */ paymasterUrl?: string; /** Optional paymaster context (ERC-7677) to pass to the paymaster service. */ paymasterContext?: PaymasterContext; /** The idempotency key. */ idempotencyKey?: string; /** Optional data suffix (EIP-8021) to enable transaction attribution. */ dataSuffix?: string; }; /** * Return type for the sendUserOperation function. */ export type SendUserOperationReturnType = { /** The address of the smart wallet. */ smartAccountAddress: Address; /** The status of the user operation. */ status: typeof EvmUserOperationStatus.broadcast; /** The hash of the user operation. This is not the transaction hash which is only available after the operation is completed.*/ userOpHash: Hex; /** The timestamp at which the prepared user operation expires, as an ISO 8601 string. */ expiresAt?: string; }; /** * Return type for the prepareAndSendUserOperation function. */ export type PrepareAndSendUserOperationReturnType = { /** The address of the smart wallet. */ smartAccountAddress: Address; /** The status of the user operation. */ status: typeof EvmUserOperationStatus.broadcast; /** The hash of the user operation. This is not the transaction hash which is only available after the operation is completed.*/ userOpHash: Hex; /** The timestamp at which the prepared user operation expires, as an ISO 8601 string. */ expiresAt?: string; }; /** * Sends a user operation to the network. * * @example * ```ts * import { sendUserOperation } from "@coinbase/cdp-sdk"; * import { parseEther } from "viem"; * import { CdpClient } from "@coinbase/cdp-sdk"; * * const client = new CdpClient({ * apiKeyId: "your-api-key-id", * apiKeySecret: "your-api-key-secret", * walletSecret: "your-wallet-secret", * }); * * const ethAccount = await client.createEvmServerAccount() * const smartAccount = await client.createEvmSmartAccount({ owner: ethAccount }) * * const result = await sendUserOperation(client, smartAccount, { * calls: [ * { * abi: erc20Abi, * functionName: "transfer", * args: [to, amount], * }, * { * to: "0x1234567890123456789012345678901234567890", * data: "0x", * value: parseEther("0.0000005"), * }, * ], * network: "base-sepolia", * paymasterUrl: "https://api.developer.coinbase.com/rpc/v1/base/someapikey", * }); * ``` * * @param {CdpOpenApiClientType} client - The client to use to send the user operation. * @param {SendUserOperationOptions<T>} options - The options for the user operation. * @returns {Promise<SendUserOperationReturnType>} The result of the user operation. */ export async function sendUserOperation<T extends readonly unknown[]>( client: CdpOpenApiClientType, options: SendUserOperationOptions<T>, ): Promise<SendUserOperationReturnType> { const { calls, network, paymasterUrl: _paymasterUrl } = options; const paymasterUrl = await (async () => { if (!_paymasterUrl && network === "base") { return getBaseNodeRpcUrl("base"); } return _paymasterUrl; })(); if (calls.length === 0) { throw new Error("Calls array is empty"); } const encodedCalls = calls.map(call => { const baseCall = call as BaseCall; const value = (baseCall.value ?? BigInt(0)).toString(); const overrideGasLimit = baseCall.overrideGasLimit?.toString(); if (isContractCall(baseCall)) { return { to: baseCall.to, data: encodeFunctionData({ abi: baseCall.abi, functionName: baseCall.functionName, args: baseCall.args, }), value, overrideGasLimit, }; } const directCall = baseCall as DirectCall; return { to: directCall.to, data: directCall.data ?? "0x", value, overrideGasLimit, }; }); const createOpResponse = await client.prepareUserOperation(options.smartAccount.address, { network, calls: encodedCalls, paymasterUrl, paymasterContext: options.paymasterContext, dataSuffix: options.dataSuffix, }); const owner = options.smartAccount.owners[0]; const signature = await owner.sign({ hash: createOpResponse.userOpHash as Hex, }); const broadcastResponse = await client.sendUserOperation( options.smartAccount.address, createOpResponse.userOpHash as Hex, { signature, }, options.idempotencyKey, ); return { smartAccountAddress: options.smartAccount.address, status: broadcastResponse.status, userOpHash: createOpResponse.userOpHash, expiresAt: broadcastResponse.expiresAt, } as SendUserOperationReturnType; }