Skip to content

Commit 6ad3206

Browse files
Only subscribe to selected accounts (#543)
Here are some demos - https://www.loom.com/share/334a4f109b44498894be89f86afe5a5a - https://www.loom.com/share/bbafb65d3d624adea0bdf13269d25818?sid=ebf0d5c5-290a-480c-a919-7ecf8b00a0c0 Includes: - Only subscribe to selected accounts - Turn off account full sync on account creation <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Monitors WebSocket subscriptions only for user-selected accounts, adds a SetSelectedAccounts API and test-dapp UI, removes background sync on account creation, and refactors subscription/state handling with dependency updates. > > - **Snap core**: > - **Account monitoring**: Replace per-account monitor with `setMonitoredAccounts` to start/stop subscriptions based on selection; sync only monitored accounts on connection recovery. > - **Selection API**: Add `keyring.setSelectedAccounts` (validates UUIDs) and expose test-dapp RPC `setAccountSelected`; permit via `permissions`. > - **Background events**: Remove `OnSyncAccount` handler and stop scheduling sync on account creation. > - **Keyring**: Stop auto-monitoring on create/delete and remove sync scheduling; emit events unchanged. > - **Accounts service**: Add `getAllSelected` using `getSelectedAccounts` from `@metamask/keyring-snap-sdk`. > - **Subscriptions**: > - Add unsubscription confirmation handling; delete repo entry immediately on unsubscribe. > - Tighten confirmation parsing; maintain re-subscribe and expiry flows. > - **Initialization**: > - Add `MonitoredAccountsInitializer` to monitor selected accounts on `onActive`. > - **State**: > - Change `deleteKey` to use `snap_setState` with `undefined` sentinel; update tests. > - **Test dapp (site)**: > - Add UI to list/select accounts and inform snap via `setAccountSelected`; keep manual sync action. > - **Configs/manifest**: > - Update `snap.manifest.json` shasum. > - Permissions updated for new methods. > - **Dependencies**: > - Bump `@metamask/keyring-api` to `^21.1.0` and `@metamask/keyring-snap-sdk` to `^7.1.0` (root, snap, site). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e37a143. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 329111f commit 6ad3206

File tree

25 files changed

+665
-473
lines changed

25 files changed

+665
-473
lines changed

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@
3535
"yarn lint:fix"
3636
]
3737
},
38-
"resolutions": {
39-
"@metamask/snaps-sdk": "9.3.0"
40-
},
4138
"devDependencies": {
4239
"@commitlint/cli": "^17.7.1",
4340
"@commitlint/config-conventional": "^17.7.0",
@@ -47,6 +44,7 @@
4744
"@metamask/eslint-config-jest": "^12.1.0",
4845
"@metamask/eslint-config-nodejs": "^12.1.0",
4946
"@metamask/eslint-config-typescript": "^12.1.0",
47+
"@metamask/keyring-snap-sdk": "^7.1.0",
5048
"@metamask/utils": "^10.0.0",
5149
"@types/jest": "^27.5.2",
5250
"@types/react": "^18.0.15",

packages/site/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"dependencies": {
3232
"@chakra-ui/react": "^3.0.2",
3333
"@emotion/react": "^11.13.3",
34-
"@metamask/keyring-api": "^18.0.0",
34+
"@metamask/keyring-api": "^21.1.0",
3535
"@metamask/providers": "^18.1.0",
3636
"@solana-program/compute-budget": "^0.7.0",
3737
"@solana-program/system": "^0.7.0",

packages/site/src/components/Handlers/Accounts.tsx

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
11
/* eslint-disable @typescript-eslint/no-unused-vars */
2-
import { Button, Card, Flex } from '@chakra-ui/react';
2+
import { Button, Card, Flex, Heading } from '@chakra-ui/react';
3+
import { KeyringRpcMethod, type KeyringAccount } from '@metamask/keyring-api';
4+
import { useEffect, useState } from 'react';
35

46
import { TestDappRpcRequestMethod } from '../../../../snap/src/core/handlers/onRpcRequest/types';
5-
import { useInvokeSnap } from '../../hooks';
7+
import { useInvokeKeyring, useInvokeSnap } from '../../hooks';
68
import { toaster } from '../Toaster/Toaster';
79

810
export const Accounts = () => {
911
const invokeSnap = useInvokeSnap();
12+
const invokeKeyring = useInvokeKeyring();
13+
const [accounts, setAccounts] = useState<KeyringAccount[]>([]);
14+
const [selectedAccounts, setSelectedAccounts] = useState<KeyringAccount[]>(
15+
[],
16+
);
17+
18+
useEffect(() => {
19+
const fetchAndSetAccounts = async () => {
20+
const accountsToSet = (await invokeKeyring({
21+
method: KeyringRpcMethod.ListAccounts,
22+
})
23+
.then((accountsResponse) => {
24+
return accountsResponse ?? [];
25+
})
26+
.catch((error) => {
27+
console.error('Error fetching accounts', error);
28+
return [];
29+
})) as KeyringAccount[];
30+
setAccounts(accountsToSet);
31+
};
32+
fetchAndSetAccounts();
33+
}, []);
1034

1135
const synchronize = async () => {
1236
const promise = invokeSnap({
@@ -26,17 +50,58 @@ export const Accounts = () => {
2650
});
2751
};
2852

53+
const informSnapAboutSelectedAccounts = async (
54+
selectedAccountsToInform: KeyringAccount[],
55+
) => {
56+
await invokeSnap({
57+
method: TestDappRpcRequestMethod.SetAccountSelected,
58+
params: {
59+
accountIds: selectedAccountsToInform.map((account) => account.id),
60+
},
61+
});
62+
};
63+
2964
return (
3065
<Card.Root>
3166
<Card.Header>
3267
<Card.Title>Accounts</Card.Title>
3368
</Card.Header>
3469
<Card.Body gap="2">
3570
<Flex direction="column" gap="4">
36-
<Flex direction="column" gap="1">
37-
<Button variant="outline" onClick={synchronize}>
38-
Synchronize
39-
</Button>
71+
<Button variant="outline" onClick={synchronize}>
72+
Synchronize
73+
</Button>
74+
<Flex direction="column" gap="2">
75+
<Heading as="h2" size="md">
76+
Selected accounts
77+
</Heading>
78+
{accounts.map((account) => (
79+
<Flex align="center" gap="2" key={account.id}>
80+
<input
81+
type="checkbox"
82+
checked={selectedAccounts.includes(account)}
83+
onChange={(evnt) => {
84+
const isChecked = evnt.target.checked;
85+
let newSelectedAccounts = selectedAccounts;
86+
if (isChecked) {
87+
newSelectedAccounts = [...selectedAccounts, account];
88+
} else {
89+
newSelectedAccounts = selectedAccounts.filter(
90+
(selectedAccount) => selectedAccount !== account,
91+
);
92+
}
93+
setSelectedAccounts(
94+
Array.from(new Set(newSelectedAccounts)),
95+
);
96+
informSnapAboutSelectedAccounts(newSelectedAccounts);
97+
}}
98+
id={`account-checkbox-${account.id}`}
99+
/>
100+
<label htmlFor={`account-checkbox-${account.id}`}>
101+
{account.id}
102+
</label>
103+
</Flex>
104+
))}
40105
</Flex>
41106
</Flex>
42107
</Card.Body>

packages/snap/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@
4141
"@jest/globals": "^29.5.0",
4242
"@metamask/auto-changelog": "4.0.0",
4343
"@metamask/key-tree": "9.1.2",
44-
"@metamask/keyring-api": "^18.0.0",
45-
"@metamask/keyring-snap-sdk": "^4.0.0",
44+
"@metamask/keyring-api": "^21.1.0",
45+
"@metamask/keyring-snap-sdk": "^7.1.0",
4646
"@metamask/snaps-cli": "^8.1.1",
4747
"@metamask/snaps-jest": "9.3.0",
4848
"@metamask/snaps-sdk": "^9.3.0",

packages/snap/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snap-solana-wallet.git"
88
},
99
"source": {
10-
"shasum": "/34yob4VJK1Jr7PMxj4Txlrmv3y2oSJmtx70glfAQ1w=",
10+
"shasum": "Z3x68DtN7fwk0qJJAOHmq2AKuNOgm+IdmFUkF1h0juM=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/snap/src/core/handlers/onCronjob/backgroundEvents/ScheduleBackgroundEventMethod.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ export enum ScheduleBackgroundEventMethod {
55
OnTransactionApproved = 'onTransactionApproved',
66
/** Triggered when a transaction is rejected */
77
OnTransactionRejected = 'onTransactionRejected',
8-
/** Use it to schedule a background event to asynchronously fetch the assets and transactions of an account */
9-
OnSyncAccount = 'onSyncAccount',
108
/** Use it to schedule a background event to refresh the send form */
119
RefreshSend = 'refreshSend',
1210
/** Use it to schedule a background event to refresh the confirmation estimation */

packages/snap/src/core/handlers/onCronjob/backgroundEvents/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { OnCronjobHandler } from '@metamask/snaps-sdk';
22

33
import { closeWebSocketConnections } from './closeWebSocketConnections';
4-
import { onSyncAccount } from './onSyncAccount';
54
import { onTransactionAdded } from './onTransactionAdded';
65
import { onTransactionApproved } from './onTransactionApproved';
76
import { onTransactionRejected } from './onTransactionRejected';
@@ -16,7 +15,6 @@ export const handlers: Record<ScheduleBackgroundEventMethod, OnCronjobHandler> =
1615
onTransactionApproved,
1716
[ScheduleBackgroundEventMethod.OnTransactionRejected]:
1817
onTransactionRejected,
19-
[ScheduleBackgroundEventMethod.OnSyncAccount]: onSyncAccount,
2018
[ScheduleBackgroundEventMethod.RefreshSend]: refreshSend,
2119
[ScheduleBackgroundEventMethod.RefreshConfirmationEstimation]:
2220
refreshConfirmationEstimation,

packages/snap/src/core/handlers/onCronjob/backgroundEvents/onSyncAccount.ts

Lines changed: 0 additions & 43 deletions
This file was deleted.

packages/snap/src/core/handlers/onKeyringRequest/Keyring.test.ts

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
/* eslint-disable jest/prefer-strict-equal */
33
import type { KeyringRequest } from '@metamask/keyring-api';
44
import { SolMethod } from '@metamask/keyring-api';
5-
import type { CaipAssetType, JsonRpcRequest } from '@metamask/snaps-sdk';
5+
import {
6+
InvalidParamsError,
7+
type CaipAssetType,
8+
type JsonRpcRequest,
9+
} from '@metamask/snaps-sdk';
610
import { signature } from '@solana/kit';
711

812
import type { AssetEntity } from '../../../entities';
@@ -44,7 +48,6 @@ import {
4448
import { getBip32EntropyMock } from '../../test/mocks/utils/getBip32Entropy';
4549
import { getBip32Entropy } from '../../utils/getBip32Entropy';
4650
import logger from '../../utils/logger';
47-
import { ScheduleBackgroundEventMethod } from '../onCronjob/backgroundEvents/ScheduleBackgroundEventMethod';
4851
import { SolanaKeyring } from './Keyring';
4952

5053
jest.mock('@metamask/keyring-snap-sdk', () => ({
@@ -121,8 +124,7 @@ describe('SolanaKeyring', () => {
121124
} as unknown as jest.Mocked<TransactionsService>;
122125

123126
mockKeyringAccountMonitor = {
124-
monitorKeyringAccount: jest.fn(),
125-
stopMonitorKeyringAccount: jest.fn(),
127+
setMonitoredAccounts: jest.fn(),
126128
} as unknown as KeyringAccountMonitor;
127129

128130
keyring = new SolanaKeyring({
@@ -531,21 +533,6 @@ describe('SolanaKeyring', () => {
531533
expect(account).toEqual(asStrictKeyringAccount(existingAccount));
532534
expect(stateUpdateSpy).not.toHaveBeenCalled();
533535
});
534-
535-
it('schedules a background event to sync the account', async () => {
536-
await keyring.createAccount();
537-
538-
expect(snap.request).toHaveBeenCalledWith(
539-
expect.objectContaining({
540-
method: 'snap_scheduleBackgroundEvent',
541-
params: expect.objectContaining({
542-
request: expect.objectContaining({
543-
method: ScheduleBackgroundEventMethod.OnSyncAccount,
544-
}),
545-
}),
546-
}),
547-
);
548-
});
549536
});
550537

551538
describe('when an account name suggestion is provided', () => {
@@ -563,18 +550,6 @@ describe('SolanaKeyring', () => {
563550
});
564551
});
565552

566-
it('monitors the account assets', async () => {
567-
await keyring.createAccount();
568-
569-
expect(
570-
mockKeyringAccountMonitor.monitorKeyringAccount,
571-
).toHaveBeenCalledWith(
572-
expect.objectContaining({
573-
id: expect.any(String),
574-
}),
575-
);
576-
});
577-
578553
it('throws when deriving address fails', async () => {
579554
jest.mocked(getBip32Entropy).mockImplementationOnce(async () => {
580555
return Promise.reject(new Error('Error deriving address'));
@@ -611,14 +586,6 @@ describe('SolanaKeyring', () => {
611586
expect(accountAfterDeletion).toBeUndefined();
612587
});
613588

614-
it('stops monitoring the account assets', async () => {
615-
await keyring.deleteAccount(MOCK_SOLANA_KEYRING_ACCOUNT_1.id);
616-
617-
expect(
618-
mockKeyringAccountMonitor.stopMonitorKeyringAccount,
619-
).toHaveBeenCalledWith(MOCK_SOLANA_KEYRING_ACCOUNT_1);
620-
});
621-
622589
it('throws an error if account provided is not a uuid', async () => {
623590
await expect(keyring.deleteAccount('non-existent-id')).rejects.toThrow(
624591
/Expected a string matching/u,
@@ -993,4 +960,26 @@ describe('SolanaKeyring', () => {
993960
).not.toHaveBeenCalled();
994961
});
995962
});
963+
964+
describe('setSelectedAccounts', () => {
965+
it('sets the monitored accounts', async () => {
966+
const accountIds = MOCK_SOLANA_KEYRING_ACCOUNTS.map(
967+
(account) => account.id,
968+
);
969+
await keyring.setSelectedAccounts(accountIds);
970+
971+
expect(
972+
mockKeyringAccountMonitor.setMonitoredAccounts,
973+
).toHaveBeenCalledWith(accountIds);
974+
});
975+
976+
it('rejects if an account id is not valid', async () => {
977+
await expect(
978+
keyring.setSelectedAccounts([
979+
MOCK_SOLANA_KEYRING_ACCOUNT_0.id,
980+
'not-a-uuid',
981+
]),
982+
).rejects.toThrow(InvalidParamsError);
983+
});
984+
});
996985
});

0 commit comments

Comments
 (0)