Skip to content

Commit 811c879

Browse files
authored
feat: implement wallet_getSubAccounts rpc (#1676)
* feat: implement wallet_getSubAccounts * types * pr feedback * add eof * eof
1 parent 4539500 commit 811c879

File tree

5 files changed

+204
-1
lines changed

5 files changed

+204
-1
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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<{
11+
subAccounts: {
12+
address: string;
13+
factory: string;
14+
factoryCalldata: string;
15+
}[];
16+
}>();
17+
const [error, setError] = useState<string>();
18+
const [isLoading, setIsLoading] = useState(false);
19+
20+
const handleGetSubAccounts = useCallback(async () => {
21+
if (!sdk) {
22+
return;
23+
}
24+
25+
setIsLoading(true);
26+
try {
27+
const provider = sdk.getProvider();
28+
const accounts = await provider.request({
29+
method: 'eth_requestAccounts',
30+
}) as string[];
31+
if (accounts.length < 2) {
32+
throw new Error('Create a sub account first by clicking the Add Address button');
33+
}
34+
const response = await provider.request({
35+
method: 'wallet_getSubAccounts',
36+
params: [{
37+
account: accounts[1],
38+
domain: window.location.origin,
39+
}],
40+
});
41+
42+
console.info('getSubAccounts response', response);
43+
setSubAccounts(response as { subAccounts: { address: string; factory: string; factoryCalldata: string; }[] });
44+
} catch (error) {
45+
console.error('Error getting sub accounts:', error);
46+
setError(error instanceof Error ? error.message : 'Unknown error');
47+
} finally {
48+
setIsLoading(false);
49+
}
50+
}, [sdk]);
51+
52+
return (
53+
<>
54+
<Button w="full" onClick={handleGetSubAccounts} isLoading={isLoading} loadingText="Getting Sub Accounts...">
55+
Get Sub Accounts
56+
</Button>
57+
{subAccounts && (
58+
<Box
59+
as="pre"
60+
w="full"
61+
p={2}
62+
bg="gray.900"
63+
borderRadius="md"
64+
border="1px solid"
65+
borderColor="gray.700"
66+
overflow="auto"
67+
whiteSpace="pre-wrap"
68+
>
69+
{JSON.stringify(subAccounts, null, 2)}
70+
</Box>
71+
)}
72+
{error && (
73+
<Box
74+
as="pre"
75+
w="full"
76+
p={2}
77+
bg="red.900"
78+
borderRadius="md"
79+
border="1px solid"
80+
borderColor="red.700"
81+
overflow="auto"
82+
whiteSpace="pre-wrap"
83+
>
84+
{error}
85+
</Box>
86+
)}
87+
</>
88+
);
89+
}

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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import {
4+
GetSubAccountsRequest,
5+
GetSubAccountsResponse,
6+
GetSubAccountsResponseItem,
7+
} from './wallet_getSubAccount.js';
8+
9+
describe('wallet_getSubAccounts schema', () => {
10+
it('should have correct request type structure', () => {
11+
const request: GetSubAccountsRequest = {
12+
account: '0x742C4d6c2B4ABE65a3A3B0A4B2Ed8B6a67c2E3f1',
13+
domain: 'example.com',
14+
};
15+
16+
expect(request.account).toBeDefined();
17+
expect(request.domain).toBeDefined();
18+
expect(typeof request.account).toBe('string');
19+
expect(typeof request.domain).toBe('string');
20+
});
21+
22+
it('should have correct response item type structure', () => {
23+
const responseItem: GetSubAccountsResponseItem = {
24+
address: '0x742C4d6c2B4ABE65a3A3B0A4B2Ed8B6a67c2E3f1',
25+
factory: '0x4e59b44847b379578588920cA78FbF26c0B4956C',
26+
factoryData: '0x1234567890abcdef',
27+
};
28+
29+
expect(responseItem.address).toBeDefined();
30+
expect(responseItem.factory).toBeDefined();
31+
expect(responseItem.factoryData).toBeDefined();
32+
expect(typeof responseItem.address).toBe('string');
33+
expect(typeof responseItem.factory).toBe('string');
34+
expect(typeof responseItem.factoryData).toBe('string');
35+
});
36+
37+
it('should have correct response type structure', () => {
38+
const response: GetSubAccountsResponse = {
39+
subAccounts: [
40+
{
41+
address: '0x742C4d6c2B4ABE65a3A3B0A4B2Ed8B6a67c2E3f1',
42+
factory: '0x4e59b44847b379578588920cA78FbF26c0B4956C',
43+
factoryData: '0x1234567890abcdef',
44+
},
45+
{
46+
address: '0x8ba1f109551bD432803012645Hac136c5e84F31D',
47+
factory: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
48+
factoryData: '0xabcdef1234567890',
49+
},
50+
],
51+
};
52+
53+
expect(response.subAccounts).toBeDefined();
54+
expect(Array.isArray(response.subAccounts)).toBe(true);
55+
expect(response.subAccounts.length).toBe(2);
56+
expect(response.subAccounts[0]).toHaveProperty('address');
57+
expect(response.subAccounts[0]).toHaveProperty('factory');
58+
expect(response.subAccounts[0]).toHaveProperty('factoryData');
59+
});
60+
61+
it('should allow empty subAccounts array', () => {
62+
const response: GetSubAccountsResponse = {
63+
subAccounts: [],
64+
};
65+
66+
expect(response.subAccounts).toBeDefined();
67+
expect(Array.isArray(response.subAccounts)).toBe(true);
68+
expect(response.subAccounts.length).toBe(0);
69+
});
70+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Address, Hex } from 'viem';
2+
3+
export type GetSubAccountsRequest = {
4+
account: Address;
5+
domain: string;
6+
};
7+
8+
export type GetSubAccountsResponseItem = {
9+
address: Address;
10+
factory: Address;
11+
factoryData: Hex;
12+
};
13+
14+
export type GetSubAccountsResponse = {
15+
subAccounts: GetSubAccountsResponseItem[];
16+
};

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { RPCResponse } from ':core/message/RPCResponse.js';
88
import { AppMetadata, ProviderEventCallback, RequestArguments } from ':core/provider/interface.js';
99
import { FetchPermissionsResponse } from ':core/rpc/coinbase_fetchSpendPermissions.js';
1010
import { WalletConnectRequest, WalletConnectResponse } from ':core/rpc/wallet_connect.js';
11+
import { GetSubAccountsResponse } from ':core/rpc/wallet_getSubAccount.js';
1112
import {
1213
logHandshakeCompleted,
1314
logHandshakeError,
@@ -177,7 +178,7 @@ export class SCWSigner implements Signer {
177178
this.callback?.('connect', { chainId: numberToHex(this.chain.id) });
178179
return this.accounts;
179180
}
180-
case 'wallet_switchEthereumChain': {
181+
case 'wallet_switchEthereumChain': {
181182
assertParamsChainId(request.params);
182183
this.chain.id = Number(request.params[0].chainId);
183184
return;
@@ -278,6 +279,31 @@ export class SCWSigner implements Signer {
278279
return this.sendRequestToPopup(modifiedRequest);
279280
}
280281
// Sub Account Support
282+
case 'wallet_getSubAccounts': {
283+
const subAccount = store.subAccounts.get();
284+
if (subAccount?.address) {
285+
return {
286+
subAccounts: [subAccount],
287+
};
288+
}
289+
290+
if (!this.chain.rpcUrl) {
291+
throw standardErrors.rpc.internal('No RPC URL set for chain');
292+
}
293+
const response = await fetchRPCRequest(request, this.chain.rpcUrl) as GetSubAccountsResponse;
294+
assertArrayPresence(response.subAccounts, 'subAccounts');
295+
if (response.subAccounts.length > 0) {
296+
// cache the sub account
297+
assertSubAccount(response.subAccounts[0]);
298+
const subAccount = response.subAccounts[0];
299+
store.subAccounts.set({
300+
address: subAccount.address,
301+
factory: subAccount.factory,
302+
factoryData: subAccount.factoryData,
303+
});
304+
}
305+
return response;
306+
}
281307
case 'wallet_addSubAccount':
282308
return this.addSubAccount(request);
283309
case 'coinbase_fetchPermissions': {

0 commit comments

Comments
 (0)