Skip to content

Commit b73f805

Browse files
committed
feat: implement wallet_getSubAccounts rpc
1 parent 15b5a8e commit b73f805

File tree

7 files changed

+159
-9
lines changed

7 files changed

+159
-9
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Box, Button } from '@chakra-ui/react';
2+
import { createCoinbaseWalletSDK } from '@coinbase/wallet-sdk';
3+
import { useCallback, useState } from 'react';
4+
5+
type GetSubAccountsProps = {
6+
sdk: ReturnType<typeof createCoinbaseWalletSDK>;
7+
};
8+
9+
export function GetSubAccounts({ sdk }: GetSubAccountsProps) {
10+
const [subAccounts, setSubAccounts] = useState<any>();
11+
const [isLoading, setIsLoading] = useState(false);
12+
13+
const handleGetSubAccounts = useCallback(async () => {
14+
if (!sdk) {
15+
return;
16+
}
17+
18+
setIsLoading(true);
19+
try {
20+
const provider = sdk.getProvider();
21+
const accounts = await provider.request({
22+
method: 'eth_requestAccounts',
23+
}) as string[];
24+
if (accounts.length < 2) {
25+
throw new Error('Create a sub account first by clicking the Add Address button');
26+
}
27+
const response = await provider.request({
28+
method: 'wallet_getSubAccounts',
29+
params: [{
30+
account: accounts[1],
31+
domain: window.location.origin,
32+
}],
33+
});
34+
35+
console.info('getSubAccounts response', response);
36+
setSubAccounts(response);
37+
} catch (error) {
38+
console.error('Error getting sub accounts:', error);
39+
setSubAccounts({ error: error instanceof Error ? error.message : 'Unknown error' });
40+
} finally {
41+
setIsLoading(false);
42+
}
43+
}, [sdk]);
44+
45+
return (
46+
<>
47+
<Button w="full" onClick={handleGetSubAccounts} isLoading={isLoading} loadingText="Getting Sub Accounts...">
48+
Get Sub Accounts
49+
</Button>
50+
{subAccounts && (
51+
<Box
52+
as="pre"
53+
w="full"
54+
p={2}
55+
bg="gray.900"
56+
borderRadius="md"
57+
border="1px solid"
58+
borderColor="gray.700"
59+
overflow="auto"
60+
whiteSpace="pre-wrap"
61+
>
62+
{JSON.stringify(subAccounts, null, 2)}
63+
</Box>
64+
)}
65+
</>
66+
);
67+
}

examples/testapp/src/pages/add-sub-account/index.page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { AddSubAccount } from './components/AddSubAccount';
1919
import { AddSubAccountWithoutKeys } from './components/AddSubAccountWithoutKeys';
2020
import { Connect } from './components/Connect';
2121
import { GenerateNewSigner } from './components/GenerateNewSigner';
22+
import { GetSubAccounts } from './components/GetSubAccounts';
2223
import { GrantSpendPermission } from './components/GrantSpendPermission';
2324
import { PersonalSign } from './components/PersonalSign';
2425
import { SendCalls } from './components/SendCalls';
@@ -81,6 +82,7 @@ export default function SubAccounts() {
8182
onAddSubAccount={setSubAccountAddress}
8283
signerFn={getSubAccountSigner}
8384
/>
85+
<GetSubAccounts sdk={sdk} />
8486
<AddSubAccountWithoutKeys
8587
sdk={sdk}
8688
onAddSubAccount={setSubAccountAddress}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Address, Hex } from 'viem';
2+
3+
export type GetSubAccountRequest = {
4+
account: Address;
5+
domain: string;
6+
};
7+
8+
export type GetSubAccountResponseItem = {
9+
address: Address;
10+
factory: Address;
11+
factoryCalldata: Hex;
12+
};
13+
14+
export type GetSubAccountResponse = {
15+
subAccounts: GetSubAccountResponseItem[];
16+
};
17+
18+
export type GetSubAccountSchema = {
19+
Method: 'wallet_getSubAccount';
20+
Parameters: [GetSubAccountRequest];
21+
ReturnType: GetSubAccountResponse;
22+
};

packages/wallet-sdk/src/sign/scw/SCWSigner.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export class SCWSigner implements Signer {
155155
}
156156
}
157157

158-
async _request(request: RequestArguments) {
158+
async _request(request: RequestArguments) {
159159
if (this.accounts.length === 0) {
160160
switch (request.method) {
161161
case 'eth_requestAccounts': {
@@ -179,7 +179,7 @@ export class SCWSigner implements Signer {
179179
this.callback?.('connect', { chainId: numberToHex(this.chain.id) });
180180
return this.accounts;
181181
}
182-
case 'wallet_switchEthereumChain': {
182+
case 'wallet_switchEthereumChain': {
183183
assertParamsChainId(request.params);
184184
this.chain.id = Number(request.params[0].chainId);
185185
return;
@@ -280,6 +280,17 @@ export class SCWSigner implements Signer {
280280
return this.sendRequestToPopup(modifiedRequest);
281281
}
282282
// Sub Account Support
283+
case 'wallet_getSubAccounts':
284+
const client = getClient(this.chain.id);
285+
assertPresence(
286+
client,
287+
standardErrors.rpc.internal(
288+
`client not found for chainId ${this.chain.id} when fetching sub accounts`
289+
)
290+
);
291+
const subAccounts = await client.request(request);
292+
return subAccounts;
293+
283294
case 'wallet_addSubAccount':
284295
return this.addSubAccount(request);
285296
case 'coinbase_fetchPermissions': {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { PublicClient } from 'viem';
2+
3+
import { GetSubAccountSchema } from ':core/rpc/wallet_getSubAccount.js';
4+
5+
export type ExtendedRpcMethods = {
6+
getSubAccount: (args: GetSubAccountSchema['Parameters'][0]) => Promise<GetSubAccountSchema['ReturnType']>;
7+
// Add more non-standardmethods here as needed
8+
};
9+
10+
// Configuration for RPC method extensions
11+
export type RpcMethodConfig<T extends keyof ExtendedRpcMethods> = {
12+
methodName: T;
13+
rpcMethod: string;
14+
handler: (client: PublicClient, args: Parameters<ExtendedRpcMethods[T]>[0]) => ReturnType<ExtendedRpcMethods[T]>;
15+
};
16+
17+
// Define all extended RPC methods here
18+
// To add a new method, add a new configuration object to this array
19+
export const extendedRpcMethods: RpcMethodConfig<keyof ExtendedRpcMethods>[] = [
20+
{
21+
methodName: 'getSubAccount',
22+
rpcMethod: 'wallet_getSubAccount',
23+
handler: (client, args) => client.request<GetSubAccountSchema>({
24+
method: 'wallet_getSubAccount',
25+
params: [args],
26+
}),
27+
},
28+
];
29+
30+
// Generic function to create extended client
31+
export function createExtendedClient(baseClient: PublicClient): PublicClient & ExtendedRpcMethods {
32+
return baseClient.extend((client) => {
33+
const extensions = {} as ExtendedRpcMethods;
34+
35+
extendedRpcMethods.forEach((config) => {
36+
extensions[config.methodName] = (args: any) => config.handler(client, args);
37+
});
38+
39+
return extensions;
40+
});
41+
}

packages/wallet-sdk/src/store/chain-clients/store.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { PublicClient } from 'viem';
22
import { BundlerClient } from 'viem/account-abstraction';
33
import { createStore } from 'zustand/vanilla';
44

5+
import { ExtendedRpcMethods } from './extensions.js';
6+
57
export type ChainClientState = {
68
[key: number]: {
7-
client: PublicClient;
9+
client: PublicClient & ExtendedRpcMethods;
810
bundlerClient: BundlerClient;
911
};
1012
};

packages/wallet-sdk/src/store/chain-clients/utils.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { createPublicClient, defineChain, http, PublicClient } from 'viem';
1+
import { createPublicClient, defineChain, http } from 'viem';
22
import { BundlerClient, createBundlerClient } from 'viem/account-abstraction';
33

4-
import { ChainClients } from './store.js';
54
import { RPCResponseNativeCurrency } from ':core/message/RPCResponse.js';
5+
import { createExtendedClient } from './extensions.js';
6+
import { ChainClients } from './store.js';
67

78
export type SDKChain = {
89
id: number;
@@ -30,12 +31,16 @@ export function createClients(chains: SDKChain[]) {
3031
},
3132
});
3233

33-
const client = createPublicClient({
34+
const baseClient = createPublicClient({
3435
chain: viemchain,
3536
transport: http(c.rpcUrl),
3637
});
38+
39+
// Create extended client with all custom RPC methods
40+
const client = createExtendedClient(baseClient);
41+
3742
const bundlerClient = createBundlerClient({
38-
client,
43+
client: baseClient,
3944
transport: http(c.rpcUrl),
4045
});
4146

@@ -48,10 +53,10 @@ export function createClients(chains: SDKChain[]) {
4853
});
4954
}
5055

51-
export function getClient(chainId: number): PublicClient | undefined {
56+
export function getClient(chainId: number) {
5257
return ChainClients.getState()[chainId]?.client;
5358
}
5459

5560
export function getBundlerClient(chainId: number): BundlerClient | undefined {
5661
return ChainClients.getState()[chainId]?.bundlerClient;
57-
}
62+
}

0 commit comments

Comments
 (0)