@coinbase/cdp-sdk
Version:
SDK for interacting with the Coinbase Developer Platform Wallet API
232 lines (210 loc) • 6.85 kB
text/typescript
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;
}