Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions examples/typescript/evm/smart-accounts/multiOwner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Usage: pnpm tsx evm/smart-accounts/multiOwner.ts

import { CdpClient } from "@coinbase/cdp-sdk";
import "dotenv/config";

const cdp = new CdpClient();

const cdpOwner1 = await cdp.evm.getOrCreateAccount({ name: "Owner-1" });
const cdpOwner2 = await cdp.evm.getOrCreateAccount({ name: "Owner-2" });

const smartAccount = await (
await cdp.evm.getOrCreateSmartAccount({
name: "MultiOwner",
owners: [cdpOwner1, cdpOwner2],
enableSpendPermissions: true,
})
).useNetwork("base-sepolia");

console.log("Smart account address:", smartAccount.address);
console.log("Smart account owners:", smartAccount.owners);

const { userOpHash } = await smartAccount.sendUserOperation({
calls: [
{
to: smartAccount.address,
value: 0n,
},
],
});

console.log("User operation hash with default owner:", userOpHash);

const userOpResult = await smartAccount.waitForUserOperation({
userOpHash,
});

console.log("User operation result:", userOpResult);

const { userOpHash: userOpHash2 } = await smartAccount.sendUserOperation({
calls: [
{
to: smartAccount.address,
value: 0n,
},
],
signer: cdpOwner2,
});

console.log("User operation hash with second owner:", userOpHash2);

const userOpResult2 = await smartAccount.waitForUserOperation({
userOpHash: userOpHash2,
});

console.log("User operation result with second owner:", userOpResult2);
27 changes: 19 additions & 8 deletions typescript/src/accounts/evm/toEvmSmartAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
SignTypedDataOptions,
UserOperation,
} from "../../client/evm/evm.types.js";
import { UserInputValidationError } from "../../errors.js";
import {
type CdpOpenApiClientType,
type EvmSmartAccount as EvmSmartAccountModel,
Expand All @@ -58,34 +59,44 @@ import type {
import type { Address, Hex } from "../../types/misc.js";

/**
* Options for converting a pre-existing EvmSmartAccount and owner to a EvmSmartAccount
* Options for converting a pre-existing EvmSmartAccount and owner(s) to a EvmSmartAccount
*/
export type ToEvmSmartAccountOptions = {
/** The pre-existing EvmSmartAccount. */
smartAccount: EvmSmartAccountModel;
/** The owner of the smart account. */
owner: EvmAccount;
/** The owner of the smart account (for backwards compatibility). */
owner?: EvmAccount;
/** The owners of the smart account. If provided, takes precedence over `owner`. */
owners?: EvmAccount[];
};

/**
* Creates a EvmSmartAccount instance from an existing EvmSmartAccount and owner.
* Creates a EvmSmartAccount instance from an existing EvmSmartAccount and owner(s).
* Use this to interact with previously deployed EvmSmartAccounts, rather than creating new ones.
*
* The owner must be the original owner of the evm smart account.
* The owner(s) must be among the original owners of the evm smart account.
*
* @param {CdpOpenApiClientType} apiClient - The API client.
* @param {ToEvmSmartAccountOptions} options - Configuration options.
* @param {EvmSmartAccount} options.smartAccount - The deployed evm smart account.
* @param {EvmAccount} options.owner - The owner which signs for the smart account.
* @param {EvmAccount} [options.owner] - The owner which signs for the smart account (for backwards compatibility).
* @param {EvmAccount[]} [options.owners] - The owners which can sign for the smart account. Takes precedence over `owner`.
* @returns {EvmSmartAccount} A configured EvmSmartAccount instance ready for user operation submission.
*/
export function toEvmSmartAccount(
apiClient: CdpOpenApiClientType,
options: ToEvmSmartAccountOptions,
): EvmSmartAccount {
// Handle backwards compatibility: if owners is provided, use it; otherwise fall back to owner
const accountOwners = options.owners || (options.owner ? [options.owner] : []);

if (accountOwners.length === 0) {
throw new UserInputValidationError("At least one owner must be provided");
}

const account: EvmSmartAccount = {
address: options.smartAccount.address as Address,
owners: [options.owner],
owners: accountOwners,
policies: options.smartAccount.policies,
async transfer(transferArgs): Promise<SendUserOperationReturnType> {
Analytics.trackAction({
Expand Down Expand Up @@ -287,7 +298,7 @@ export function toEvmSmartAccount(

return toNetworkScopedEvmSmartAccount(apiClient, {
smartAccount: account,
owner: options.owner,
owners: accountOwners,
network,
});
},
Expand Down
27 changes: 18 additions & 9 deletions typescript/src/accounts/evm/toNetworkScopedEvmSmartAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,34 +44,43 @@ import type {
} from "../../openapi-client/index.js";

/**
* Options for converting a pre-existing EvmSmartAccount and owner to a NetworkScopedEvmSmartAccount
* Options for converting a pre-existing EvmSmartAccount and owner(s) to a NetworkScopedEvmSmartAccount
*/
export type ToNetworkScopedEvmSmartAccountOptions = {
/** The pre-existing EvmSmartAccount. */
smartAccount: EvmSmartAccount;
/** The network to scope the smart account object to. */
network: KnownEvmNetworks;
/** The owner of the smart account. */
owner: EvmAccount;
/** The owner of the smart account (for backwards compatibility). */
owner?: EvmAccount;
/** The owners of the smart account. If provided, takes precedence over `owner`. */
owners?: EvmAccount[];
};

/**
* Creates a NetworkScopedEvmSmartAccount instance from an existing EvmSmartAccount and owner.
* Creates a NetworkScopedEvmSmartAccount instance from an existing EvmSmartAccount and owner(s).
* Use this to interact with previously deployed EvmSmartAccounts, rather than creating new ones.
*
* The owner must be the original owner of the evm smart account.
* The owner(s) must be among the original owners of the evm smart account.
*
* @param {CdpOpenApiClientType} apiClient - The API client.
* @param {ToNetworkScopedEvmSmartAccountOptions} options - Configuration options.
* @param {EvmSmartAccount} options.smartAccount - The deployed evm smart account.
* @param {EvmAccount} options.owner - The owner which signs for the smart account.
* @param {EvmAccount} [options.owner] - The owner which signs for the smart account (for backwards compatibility).
* @param {EvmAccount[]} [options.owners] - The owners which can sign for the smart account. Takes precedence over `owner`.
* @param {KnownEvmNetworks} options.network - The network to scope the smart account to.
* @returns {NetworkScopedEvmSmartAccount} A configured NetworkScopedEvmSmartAccount instance ready for user operation submission.
*/
export async function toNetworkScopedEvmSmartAccount<Network extends KnownEvmNetworks>(
apiClient: CdpOpenApiClientType,
options: ToNetworkScopedEvmSmartAccountOptions & { network: Network },
): Promise<NetworkScopedEvmSmartAccount<Network>> {
// Handle backwards compatibility: if owners is provided, use it; otherwise fall back to owner
const accountOwners = options.owners || (options.owner ? [options.owner] : []);

if (accountOwners.length === 0) {
throw new Error("At least one owner must be provided");
}
const paymasterUrl = await (async () => {
if (options.network === "base") {
return getBaseNodeRpcUrl(options.network);
Expand All @@ -82,7 +91,7 @@ export async function toNetworkScopedEvmSmartAccount<Network extends KnownEvmNet
const account = {
address: options.smartAccount.address,
network: options.network,
owners: [options.owner],
owners: accountOwners,
name: options.smartAccount.name,
type: "evm-smart",
sendUserOperation: async (
Expand Down Expand Up @@ -266,7 +275,7 @@ export async function toNetworkScopedEvmSmartAccount<Network extends KnownEvmNet
return createSwapQuote(apiClient, {
...quoteSwapOptions,
taker: options.smartAccount.address,
signerAddress: options.owner.address,
signerAddress: accountOwners[0].address,
smartAccount: options.smartAccount,
network: options.network as SmartAccountSwapNetwork,
});
Expand Down Expand Up @@ -299,7 +308,7 @@ export async function toNetworkScopedEvmSmartAccount<Network extends KnownEvmNet
...swapOptionsWithNetwork,
smartAccount: options.smartAccount,
taker: options.smartAccount.address,
signerAddress: options.owner.address,
signerAddress: accountOwners[0].address,
paymasterUrl: swapOptions.paymasterUrl ?? paymasterUrl,
});
},
Expand Down
6 changes: 4 additions & 2 deletions typescript/src/actions/evm/sendUserOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export type SendUserOperationOptions<T extends readonly unknown[]> = {
paymasterUrl?: string;
/** The idempotency key. */
idempotencyKey?: string;
/** Optional signer to use. If not provided, defaults to the first owner of the smart account. */
signer?: EvmSmartAccount["owners"][0];
};

/**
Expand Down Expand Up @@ -148,9 +150,9 @@ export async function sendUserOperation<T extends readonly unknown[]>(
paymasterUrl,
});

const owner = options.smartAccount.owners[0];
const signer = options.signer || options.smartAccount.owners[0];

const signature = await owner.sign({
const signature = await signer.sign({
hash: createOpResponse.userOpHash as Hex,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function useSpendPermission(
account: EvmSmartAccount,
options: UseSpendPermissionOptions,
): Promise<SendUserOperationReturnType> {
const { spendPermission, value, network } = options;
const { spendPermission, value, network, signer } = options;

const data = encodeFunctionData({
abi: SPEND_PERMISSION_MANAGER_ABI,
Expand All @@ -38,6 +38,7 @@ export function useSpendPermission(
return sendUserOperation(apiClient, {
smartAccount: account,
network: network as EvmUserOperationNetwork,
signer,
calls: [
{
to: SPEND_PERMISSION_MANAGER_ADDRESS,
Expand Down
3 changes: 3 additions & 0 deletions typescript/src/actions/evm/spend-permissions/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EvmSmartAccount } from "../../../accounts/evm/types.js";
import type { SpendPermissionNetwork } from "../../../openapi-client/index.js";
import type { SpendPermission } from "../../../spend-permissions/types.js";

Expand All @@ -11,4 +12,6 @@ export type UseSpendPermissionOptions = {
value: bigint;
/** The network to execute the transaction on */
network: SpendPermissionNetwork;
/** The owner to use for signing the transaction */
signer?: EvmSmartAccount["owners"][0];
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import type { EvmSmartAccount } from "../../../accounts/evm/types.js";
import type { EvmUserOperationNetwork } from "../../../openapi-client/index.js";

export const smartAccountTransferStrategy: TransferExecutionStrategy<EvmSmartAccount> = {
executeTransfer: async ({ apiClient, from, to, value, token, network, paymasterUrl }) => {
executeTransfer: async ({ apiClient, from, to, value, token, network, paymasterUrl, signer }) => {
const smartAccountNetwork = network as EvmUserOperationNetwork;

if (token === "eth") {
const result = await sendUserOperation(apiClient, {
smartAccount: from,
paymasterUrl,
signer,
network: smartAccountNetwork,
calls: [
{
Expand All @@ -31,6 +32,7 @@ export const smartAccountTransferStrategy: TransferExecutionStrategy<EvmSmartAcc
const result = await sendUserOperation(apiClient, {
smartAccount: from,
paymasterUrl,
signer,
network: smartAccountNetwork,
calls: [
{
Expand Down
4 changes: 3 additions & 1 deletion typescript/src/actions/evm/transfer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export interface TransferExecutionStrategy<T extends EvmAccount | EvmSmartAccoun
value: bigint;
token: TransferOptions["token"];
network: TransferOptions["network"];
} & (T extends EvmSmartAccount ? { paymasterUrl?: string } : object),
} & (T extends EvmSmartAccount
? { paymasterUrl?: string; signer?: EvmSmartAccount["owners"][0] }
: object),
): Promise<T extends EvmSmartAccount ? SendUserOperationReturnType : TransactionResult>;
}

Expand Down
Loading
Loading