Skip to content

Commit cf3a860

Browse files
Merge pull request #66 from MetaMask/refactor/account-resolver
refactor: add account resolver
2 parents 96e9a36 + a97ee7b commit cf3a860

18 files changed

Lines changed: 659 additions & 345 deletions

packages/snap/src/context.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { assert, object } from '@metamask/superstruct';
22

33
import { AppConfig } from './config';
44
import { KeyringHandler, CronjobHandler, UserInputHandler } from './handlers';
5+
import { AccountResolver } from './handlers/accountResolver';
56
import { AssetsHandler } from './handlers/asset/assets';
67
import type { IClientRequestHandler } from './handlers/clientRequest';
78
import {
@@ -109,27 +110,31 @@ const confirmationUIController = new ConfirmationUXController({
109110
logger,
110111
});
111112

113+
/** ------------------------------ Account Resolver ------------------------------ */
114+
const accountResolver = new AccountResolver({
115+
accountService,
116+
onChainAccountService,
117+
walletService,
118+
});
119+
112120
/** ------------------------------ Keyring Handler ------------------------------ */
113121
const signTransactionHandler = new SignTransactionHandler({
114122
logger,
115-
accountService,
116-
walletService,
123+
accountResolver,
117124
transactionBuilder,
118125
transactionService,
119126
confirmationUIController,
120127
});
121128

122129
const signMessageHandler = new SignMessageHandler({
123130
logger,
124-
accountService,
125-
walletService,
131+
accountResolver,
126132
confirmationUIController,
127133
});
128134

129135
const signAuthEntryHandler = new SignAuthEntryHandler({
130136
logger,
131-
accountService,
132-
walletService,
137+
accountResolver,
133138
confirmationUIController,
134139
});
135140

@@ -200,27 +205,21 @@ const assetsHandler = new AssetsHandler({
200205
/** ------------------------------ Client Request Handlers ------------------------------ */
201206
const changeTrustOptHandler = new ChangeTrustOptHandler({
202207
logger,
203-
accountService,
208+
accountResolver,
204209
assetMetadataService,
205-
onChainAccountService,
206-
walletService,
207210
transactionService,
208211
confirmationUIController,
209212
});
210213

211214
const signAndSendTransactionHandler = new SignAndSendTransactionHandler({
212215
logger,
213-
accountService,
214-
onChainAccountService,
215-
walletService,
216+
accountResolver,
216217
transactionService,
217218
});
218219

219220
const computeFeeHandler = new ComputeFeeHandler({
220221
logger,
221-
accountService,
222-
onChainAccountService,
223-
walletService,
222+
accountResolver,
224223
transactionService,
225224
});
226225

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import { KnownCaip2ChainId } from '../api';
2+
import {
3+
AccountResolver,
4+
DEFAULT_RESOLVE_ACCOUNT_OPTIONS,
5+
RESOLVE_ACCOUNT_FULL_FROM_KEYRING_STATE,
6+
RESOLVE_ACCOUNT_KEYRING_AND_WALLET,
7+
ResolveAccountSource,
8+
} from './accountResolver';
9+
import {
10+
AccountService,
11+
DerivedAccountAddressMismatchException,
12+
} from '../services/account';
13+
import { generateStellarKeyringAccount } from '../services/account/__mocks__/account.fixtures';
14+
import { AccountNotActivatedException } from '../services/network';
15+
import {
16+
OnChainAccount,
17+
OnChainAccountService,
18+
} from '../services/on-chain-account';
19+
import {
20+
createMockAccountWithBalances,
21+
DEFAULT_MOCK_ACCOUNT_WITH_BALANCES,
22+
horizonSource,
23+
mockOnChainAccountService,
24+
} from '../services/on-chain-account/__mocks__/onChainAccount.fixtures';
25+
import { WalletService } from '../services/wallet';
26+
import {
27+
generateStellarAddress,
28+
getTestWallet,
29+
} from '../services/wallet/__mocks__/wallet.fixtures';
30+
31+
jest.mock('../utils/logger');
32+
33+
describe('AccountResolver', () => {
34+
const keyringAccountId = '22222222-2222-4222-8222-222222222222';
35+
const scope = KnownCaip2ChainId.Mainnet;
36+
37+
afterEach(() => {
38+
jest.restoreAllMocks();
39+
});
40+
41+
function setup() {
42+
const { accountService, onChainAccountService, walletService } =
43+
mockOnChainAccountService();
44+
const wallet = getTestWallet();
45+
const account = generateStellarKeyringAccount(
46+
keyringAccountId,
47+
wallet.address,
48+
'entropy-source-1',
49+
0,
50+
);
51+
const mockRawAccount = createMockAccountWithBalances(
52+
wallet.address,
53+
'1',
54+
DEFAULT_MOCK_ACCOUNT_WITH_BALANCES,
55+
);
56+
const onChainAccount = new OnChainAccount(
57+
mockRawAccount,
58+
scope,
59+
horizonSource(mockRawAccount, scope),
60+
);
61+
62+
const resolveAccountSpy = jest
63+
.spyOn(AccountService.prototype, 'resolveAccount')
64+
.mockResolvedValue({ account });
65+
const resolveOnChainAccountSpy = jest
66+
.spyOn(OnChainAccountService.prototype, 'resolveOnChainAccount')
67+
.mockResolvedValue(onChainAccount);
68+
const resolveOnChainAccountByKeyringAccountIdSpy = jest.spyOn(
69+
OnChainAccountService.prototype,
70+
'resolveOnChainAccountByKeyringAccountId',
71+
);
72+
const resolveWalletSpy = jest
73+
.spyOn(WalletService.prototype, 'resolveWallet')
74+
.mockResolvedValue(wallet);
75+
76+
const accountResolver = new AccountResolver({
77+
accountService,
78+
onChainAccountService,
79+
walletService,
80+
});
81+
82+
return {
83+
accountResolver,
84+
account,
85+
wallet,
86+
onChainAccount,
87+
resolveAccountSpy,
88+
resolveOnChainAccountSpy,
89+
resolveOnChainAccountByKeyringAccountIdSpy,
90+
resolveWalletSpy,
91+
};
92+
}
93+
94+
it('resolves account, on-chain data from the network, and wallet with default options', async () => {
95+
const {
96+
accountResolver,
97+
account,
98+
wallet,
99+
onChainAccount,
100+
resolveAccountSpy,
101+
resolveOnChainAccountSpy,
102+
resolveWalletSpy,
103+
} = setup();
104+
105+
const result = await accountResolver.resolveAccount({
106+
accountId: keyringAccountId,
107+
scope,
108+
options: DEFAULT_RESOLVE_ACCOUNT_OPTIONS,
109+
});
110+
111+
expect(result).toStrictEqual({
112+
account,
113+
onChainAccount,
114+
wallet,
115+
});
116+
expect(resolveAccountSpy).toHaveBeenCalledWith({
117+
accountId: keyringAccountId,
118+
});
119+
expect(resolveOnChainAccountSpy).toHaveBeenCalledWith(
120+
account.address,
121+
scope,
122+
);
123+
expect(resolveWalletSpy).toHaveBeenCalledWith(account);
124+
});
125+
126+
it('loads only keyring account and wallet when on-chain load is disabled', async () => {
127+
const {
128+
accountResolver,
129+
account,
130+
wallet,
131+
resolveAccountSpy,
132+
resolveOnChainAccountSpy,
133+
resolveWalletSpy,
134+
} = setup();
135+
136+
const result = await accountResolver.resolveAccount({
137+
accountId: keyringAccountId,
138+
scope,
139+
options: RESOLVE_ACCOUNT_KEYRING_AND_WALLET,
140+
});
141+
142+
expect(result).toStrictEqual({
143+
account,
144+
wallet,
145+
});
146+
expect(resolveAccountSpy).toHaveBeenCalledWith({
147+
accountId: keyringAccountId,
148+
});
149+
expect(resolveOnChainAccountSpy).not.toHaveBeenCalled();
150+
expect(resolveWalletSpy).toHaveBeenCalledWith(account);
151+
});
152+
153+
it('loads on-chain snapshot from state when source is State', async () => {
154+
const {
155+
accountResolver,
156+
account,
157+
wallet,
158+
onChainAccount,
159+
resolveOnChainAccountByKeyringAccountIdSpy,
160+
resolveOnChainAccountSpy,
161+
} = setup();
162+
resolveOnChainAccountByKeyringAccountIdSpy.mockResolvedValue(
163+
onChainAccount,
164+
);
165+
166+
const result = await accountResolver.resolveAccount({
167+
accountId: keyringAccountId,
168+
scope,
169+
options: RESOLVE_ACCOUNT_FULL_FROM_KEYRING_STATE,
170+
});
171+
172+
expect(result).toStrictEqual({
173+
account,
174+
onChainAccount,
175+
wallet,
176+
});
177+
expect(resolveOnChainAccountByKeyringAccountIdSpy).toHaveBeenCalledWith(
178+
account.id,
179+
scope,
180+
);
181+
expect(resolveOnChainAccountSpy).not.toHaveBeenCalled();
182+
});
183+
184+
it('throws AccountNotActivatedException when state has no on-chain snapshot', async () => {
185+
const { accountResolver, resolveOnChainAccountByKeyringAccountIdSpy } =
186+
setup();
187+
resolveOnChainAccountByKeyringAccountIdSpy.mockResolvedValue(null);
188+
189+
await expect(
190+
accountResolver.resolveAccount({
191+
accountId: keyringAccountId,
192+
scope,
193+
options: RESOLVE_ACCOUNT_FULL_FROM_KEYRING_STATE,
194+
}),
195+
).rejects.toThrow(AccountNotActivatedException);
196+
});
197+
198+
it('throws DerivedAccountAddressMismatchException when state snapshot address differs from keyring', async () => {
199+
const { accountResolver, resolveOnChainAccountByKeyringAccountIdSpy } =
200+
setup();
201+
202+
const otherAddress = generateStellarAddress();
203+
const mockRawAccount = createMockAccountWithBalances(
204+
otherAddress,
205+
'1',
206+
DEFAULT_MOCK_ACCOUNT_WITH_BALANCES,
207+
);
208+
const mismatchedOnChainAccount = new OnChainAccount(
209+
mockRawAccount,
210+
scope,
211+
horizonSource(mockRawAccount, scope),
212+
);
213+
resolveOnChainAccountByKeyringAccountIdSpy.mockResolvedValue(
214+
mismatchedOnChainAccount,
215+
);
216+
217+
await expect(
218+
accountResolver.resolveAccount({
219+
accountId: keyringAccountId,
220+
scope,
221+
options: RESOLVE_ACCOUNT_FULL_FROM_KEYRING_STATE,
222+
}),
223+
).rejects.toThrow(DerivedAccountAddressMismatchException);
224+
});
225+
226+
it('omits wallet when wallet load is disabled', async () => {
227+
const { accountResolver, account, onChainAccount, resolveWalletSpy } =
228+
setup();
229+
230+
const result = await accountResolver.resolveAccount({
231+
accountId: keyringAccountId,
232+
scope,
233+
options: {
234+
onChainAccount: {
235+
load: true,
236+
source: ResolveAccountSource.OnChain,
237+
},
238+
wallet: false,
239+
},
240+
});
241+
242+
expect(result).toStrictEqual({
243+
account,
244+
onChainAccount,
245+
});
246+
expect(resolveWalletSpy).not.toHaveBeenCalled();
247+
});
248+
});

0 commit comments

Comments
 (0)