Skip to content

Commit 50ca422

Browse files
committed
feat: support multiple smart account owners
1 parent e98a665 commit 50ca422

File tree

10 files changed

+277
-53
lines changed

10 files changed

+277
-53
lines changed

typescript/src/accounts/evm/toEvmSmartAccount.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
SignTypedDataOptions,
4444
UserOperation,
4545
} from "../../client/evm/evm.types.js";
46+
import { UserInputValidationError } from "../../errors.js";
4647
import {
4748
type CdpOpenApiClientType,
4849
type EvmSmartAccount as EvmSmartAccountModel,
@@ -58,34 +59,44 @@ import type {
5859
import type { Address, Hex } from "../../types/misc.js";
5960

6061
/**
61-
* Options for converting a pre-existing EvmSmartAccount and owner to a EvmSmartAccount
62+
* Options for converting a pre-existing EvmSmartAccount and owner(s) to a EvmSmartAccount
6263
*/
6364
export type ToEvmSmartAccountOptions = {
6465
/** The pre-existing EvmSmartAccount. */
6566
smartAccount: EvmSmartAccountModel;
66-
/** The owner of the smart account. */
67-
owner: EvmAccount;
67+
/** The owner of the smart account (for backwards compatibility). */
68+
owner?: EvmAccount;
69+
/** The owners of the smart account. If provided, takes precedence over `owner`. */
70+
owners?: EvmAccount[];
6871
};
6972

7073
/**
71-
* Creates a EvmSmartAccount instance from an existing EvmSmartAccount and owner.
74+
* Creates a EvmSmartAccount instance from an existing EvmSmartAccount and owner(s).
7275
* Use this to interact with previously deployed EvmSmartAccounts, rather than creating new ones.
7376
*
74-
* The owner must be the original owner of the evm smart account.
77+
* The owner(s) must be among the original owners of the evm smart account.
7578
*
7679
* @param {CdpOpenApiClientType} apiClient - The API client.
7780
* @param {ToEvmSmartAccountOptions} options - Configuration options.
7881
* @param {EvmSmartAccount} options.smartAccount - The deployed evm smart account.
79-
* @param {EvmAccount} options.owner - The owner which signs for the smart account.
82+
* @param {EvmAccount} [options.owner] - The owner which signs for the smart account (for backwards compatibility).
83+
* @param {EvmAccount[]} [options.owners] - The owners which can sign for the smart account. Takes precedence over `owner`.
8084
* @returns {EvmSmartAccount} A configured EvmSmartAccount instance ready for user operation submission.
8185
*/
8286
export function toEvmSmartAccount(
8387
apiClient: CdpOpenApiClientType,
8488
options: ToEvmSmartAccountOptions,
8589
): EvmSmartAccount {
90+
// Handle backwards compatibility: if owners is provided, use it; otherwise fall back to owner
91+
const accountOwners = options.owners || (options.owner ? [options.owner] : []);
92+
93+
if (accountOwners.length === 0) {
94+
throw new UserInputValidationError("At least one owner must be provided");
95+
}
96+
8697
const account: EvmSmartAccount = {
8798
address: options.smartAccount.address as Address,
88-
owners: [options.owner],
99+
owners: accountOwners,
89100
policies: options.smartAccount.policies,
90101
async transfer(transferArgs): Promise<SendUserOperationReturnType> {
91102
Analytics.trackAction({
@@ -287,7 +298,7 @@ export function toEvmSmartAccount(
287298

288299
return toNetworkScopedEvmSmartAccount(apiClient, {
289300
smartAccount: account,
290-
owner: options.owner,
301+
owners: accountOwners,
291302
network,
292303
});
293304
},

typescript/src/accounts/evm/toNetworkScopedEvmSmartAccount.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,34 +44,43 @@ import type {
4444
} from "../../openapi-client/index.js";
4545

4646
/**
47-
* Options for converting a pre-existing EvmSmartAccount and owner to a NetworkScopedEvmSmartAccount
47+
* Options for converting a pre-existing EvmSmartAccount and owner(s) to a NetworkScopedEvmSmartAccount
4848
*/
4949
export type ToNetworkScopedEvmSmartAccountOptions = {
5050
/** The pre-existing EvmSmartAccount. */
5151
smartAccount: EvmSmartAccount;
5252
/** The network to scope the smart account object to. */
5353
network: KnownEvmNetworks;
54-
/** The owner of the smart account. */
55-
owner: EvmAccount;
54+
/** The owner of the smart account (for backwards compatibility). */
55+
owner?: EvmAccount;
56+
/** The owners of the smart account. If provided, takes precedence over `owner`. */
57+
owners?: EvmAccount[];
5658
};
5759

5860
/**
59-
* Creates a NetworkScopedEvmSmartAccount instance from an existing EvmSmartAccount and owner.
61+
* Creates a NetworkScopedEvmSmartAccount instance from an existing EvmSmartAccount and owner(s).
6062
* Use this to interact with previously deployed EvmSmartAccounts, rather than creating new ones.
6163
*
62-
* The owner must be the original owner of the evm smart account.
64+
* The owner(s) must be among the original owners of the evm smart account.
6365
*
6466
* @param {CdpOpenApiClientType} apiClient - The API client.
6567
* @param {ToNetworkScopedEvmSmartAccountOptions} options - Configuration options.
6668
* @param {EvmSmartAccount} options.smartAccount - The deployed evm smart account.
67-
* @param {EvmAccount} options.owner - The owner which signs for the smart account.
69+
* @param {EvmAccount} [options.owner] - The owner which signs for the smart account (for backwards compatibility).
70+
* @param {EvmAccount[]} [options.owners] - The owners which can sign for the smart account. Takes precedence over `owner`.
6871
* @param {KnownEvmNetworks} options.network - The network to scope the smart account to.
6972
* @returns {NetworkScopedEvmSmartAccount} A configured NetworkScopedEvmSmartAccount instance ready for user operation submission.
7073
*/
7174
export async function toNetworkScopedEvmSmartAccount<Network extends KnownEvmNetworks>(
7275
apiClient: CdpOpenApiClientType,
7376
options: ToNetworkScopedEvmSmartAccountOptions & { network: Network },
7477
): Promise<NetworkScopedEvmSmartAccount<Network>> {
78+
// Handle backwards compatibility: if owners is provided, use it; otherwise fall back to owner
79+
const accountOwners = options.owners || (options.owner ? [options.owner] : []);
80+
81+
if (accountOwners.length === 0) {
82+
throw new Error("At least one owner must be provided");
83+
}
7584
const paymasterUrl = await (async () => {
7685
if (options.network === "base") {
7786
return getBaseNodeRpcUrl(options.network);
@@ -82,7 +91,7 @@ export async function toNetworkScopedEvmSmartAccount<Network extends KnownEvmNet
8291
const account = {
8392
address: options.smartAccount.address,
8493
network: options.network,
85-
owners: [options.owner],
94+
owners: accountOwners,
8695
name: options.smartAccount.name,
8796
type: "evm-smart",
8897
sendUserOperation: async (
@@ -266,7 +275,7 @@ export async function toNetworkScopedEvmSmartAccount<Network extends KnownEvmNet
266275
return createSwapQuote(apiClient, {
267276
...quoteSwapOptions,
268277
taker: options.smartAccount.address,
269-
signerAddress: options.owner.address,
278+
signerAddress: accountOwners[0].address,
270279
smartAccount: options.smartAccount,
271280
network: options.network as SmartAccountSwapNetwork,
272281
});
@@ -299,7 +308,7 @@ export async function toNetworkScopedEvmSmartAccount<Network extends KnownEvmNet
299308
...swapOptionsWithNetwork,
300309
smartAccount: options.smartAccount,
301310
taker: options.smartAccount.address,
302-
signerAddress: options.owner.address,
311+
signerAddress: accountOwners[0].address,
303312
paymasterUrl: swapOptions.paymasterUrl ?? paymasterUrl,
304313
});
305314
},

typescript/src/actions/evm/sendUserOperation.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export type SendUserOperationOptions<T extends readonly unknown[]> = {
4949
paymasterUrl?: string;
5050
/** The idempotency key. */
5151
idempotencyKey?: string;
52+
/** Optional signer to use. If not provided, defaults to the first owner of the smart account. */
53+
signer?: EvmSmartAccount["owners"][0];
5254
};
5355

5456
/**
@@ -148,9 +150,9 @@ export async function sendUserOperation<T extends readonly unknown[]>(
148150
paymasterUrl,
149151
});
150152

151-
const owner = options.smartAccount.owners[0];
153+
const signer = options.signer || options.smartAccount.owners[0];
152154

153-
const signature = await owner.sign({
155+
const signature = await signer.sign({
154156
hash: createOpResponse.userOpHash as Hex,
155157
});
156158

typescript/src/actions/evm/spend-permissions/smartAccount.use.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function useSpendPermission(
2727
account: EvmSmartAccount,
2828
options: UseSpendPermissionOptions,
2929
): Promise<SendUserOperationReturnType> {
30-
const { spendPermission, value, network } = options;
30+
const { spendPermission, value, network, signer } = options;
3131

3232
const data = encodeFunctionData({
3333
abi: SPEND_PERMISSION_MANAGER_ABI,
@@ -38,6 +38,7 @@ export function useSpendPermission(
3838
return sendUserOperation(apiClient, {
3939
smartAccount: account,
4040
network: network as EvmUserOperationNetwork,
41+
signer,
4142
calls: [
4243
{
4344
to: SPEND_PERMISSION_MANAGER_ADDRESS,

typescript/src/actions/evm/spend-permissions/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { EvmSmartAccount } from "../../../accounts/evm/types.js";
12
import type { SpendPermissionNetwork } from "../../../openapi-client/index.js";
23
import type { SpendPermission } from "../../../spend-permissions/types.js";
34

@@ -11,4 +12,6 @@ export type UseSpendPermissionOptions = {
1112
value: bigint;
1213
/** The network to execute the transaction on */
1314
network: SpendPermissionNetwork;
15+
/** The owner to use for signing the transaction */
16+
signer?: EvmSmartAccount["owners"][0];
1417
};

typescript/src/actions/evm/transfer/smartAccountTransferStrategy.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import type { EvmSmartAccount } from "../../../accounts/evm/types.js";
88
import type { EvmUserOperationNetwork } from "../../../openapi-client/index.js";
99

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

1414
if (token === "eth") {
1515
const result = await sendUserOperation(apiClient, {
1616
smartAccount: from,
1717
paymasterUrl,
18+
signer,
1819
network: smartAccountNetwork,
1920
calls: [
2021
{
@@ -31,6 +32,7 @@ export const smartAccountTransferStrategy: TransferExecutionStrategy<EvmSmartAcc
3132
const result = await sendUserOperation(apiClient, {
3233
smartAccount: from,
3334
paymasterUrl,
35+
signer,
3436
network: smartAccountNetwork,
3537
calls: [
3638
{

typescript/src/actions/evm/transfer/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ export interface TransferExecutionStrategy<T extends EvmAccount | EvmSmartAccoun
6363
value: bigint;
6464
token: TransferOptions["token"];
6565
network: TransferOptions["network"];
66-
} & (T extends EvmSmartAccount ? { paymasterUrl?: string } : object),
66+
} & (T extends EvmSmartAccount
67+
? { paymasterUrl?: string; signer?: EvmSmartAccount["owners"][0] }
68+
: object),
6769
): Promise<T extends EvmSmartAccount ? SendUserOperationReturnType : TransactionResult>;
6870
}
6971

0 commit comments

Comments
 (0)