Skip to content

Commit dfbebe7

Browse files
committed
refactor: group helpers in one file, split router, clean types
1 parent 5c0e999 commit dfbebe7

7 files changed

Lines changed: 442 additions & 434 deletions

File tree

app/core/WalletConnect/multichain/namespaces.test.ts renamed to app/core/WalletConnect/multichain/helpers.test.ts

Lines changed: 187 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
1+
import { type CaipChainId, KnownCaipNamespace } from '@metamask/utils';
12
import {
23
buildAdapterNamespaces,
34
proposalReferencedAdapterNamespaces,
45
seedAdapterPermissions,
5-
} from './namespaces';
6+
} from './helpers';
67
import type { ChainAdapter, NamespaceConfig } from './types';
8+
import {
9+
getRedirectMethodsForChain,
10+
mapRequestForSnap,
11+
normalizeCaipChainIdInbound,
12+
normalizeCaipChainIdOutbound,
13+
normalizeSnapResponse,
14+
} from './index';
15+
import { getAdapter , getAllAdapters } from './registry';
716

817
jest.mock('./registry', () => ({
9-
getAllAdapters: jest.fn(),
18+
getAdapter: jest.fn(),
19+
getAllAdapters: jest.fn().mockReturnValue([]),
20+
getAllRegisteredNamespaces: jest.fn().mockReturnValue([]),
1021
}));
1122

1223
jest.mock('../../SDKConnect/utils/DevLogger', () => ({
1324
log: jest.fn(),
1425
}));
1526

16-
import { getAllAdapters } from './registry';
27+
const mockedGetAdapter = getAdapter as jest.Mock;
1728

1829
const mockedGetAllAdapters = getAllAdapters as jest.Mock;
1930

2031
const createFakeAdapter = (
2132
overrides: Partial<ChainAdapter> = {},
2233
): ChainAdapter => ({
23-
namespace: overrides.namespace ?? 'fake',
34+
namespace: (overrides.namespace ?? 'fake') as KnownCaipNamespace,
2435
redirectMethods: overrides.redirectMethods ?? [],
2536
proposalReferencesNamespace:
2637
overrides.proposalReferencesNamespace ?? jest.fn().mockReturnValue(false),
@@ -33,6 +44,15 @@ const createFakeAdapter = (
3344
normalizeSnapResponse:
3445
overrides.normalizeSnapResponse ??
3546
jest.fn().mockImplementation(({ result }) => result),
47+
buildScopedPermissionsNamespace:
48+
overrides.buildScopedPermissionsNamespace ??
49+
jest.fn().mockReturnValue(undefined),
50+
normalizeCaipChainIdInbound:
51+
overrides.normalizeCaipChainIdInbound ??
52+
jest.fn().mockImplementation((caipChainId: CaipChainId) => caipChainId),
53+
normalizeCaipChainIdOutbound:
54+
overrides.normalizeCaipChainIdOutbound ??
55+
jest.fn().mockImplementation((caipChainId: CaipChainId) => caipChainId),
3656
});
3757

3858
beforeEach(() => {
@@ -44,8 +64,14 @@ describe('seedAdapterPermissions', () => {
4464
const tronHook = jest.fn();
4565
const solanaHook = jest.fn();
4666
mockedGetAllAdapters.mockReturnValue([
47-
createFakeAdapter({ namespace: 'tron', onBeforeApprove: tronHook }),
48-
createFakeAdapter({ namespace: 'solana', onBeforeApprove: solanaHook }),
67+
createFakeAdapter({
68+
namespace: KnownCaipNamespace.Tron,
69+
onBeforeApprove: tronHook,
70+
}),
71+
createFakeAdapter({
72+
namespace: KnownCaipNamespace.Solana,
73+
onBeforeApprove: solanaHook,
74+
}),
4975
]);
5076

5177
await seedAdapterPermissions({
@@ -66,7 +92,9 @@ describe('seedAdapterPermissions', () => {
6692
});
6793

6894
it('skips adapters that do not declare an onBeforeApprove hook', async () => {
69-
const adapterWithoutHook = createFakeAdapter({ namespace: 'btc' });
95+
const adapterWithoutHook = createFakeAdapter({
96+
namespace: KnownCaipNamespace.Bip122,
97+
});
7098
delete adapterWithoutHook.onBeforeApprove;
7199
mockedGetAllAdapters.mockReturnValue([adapterWithoutHook]);
72100

@@ -79,8 +107,14 @@ describe('seedAdapterPermissions', () => {
79107
const failingHook = jest.fn().mockRejectedValue(new Error('boom'));
80108
const followingHook = jest.fn();
81109
mockedGetAllAdapters.mockReturnValue([
82-
createFakeAdapter({ namespace: 'a', onBeforeApprove: failingHook }),
83-
createFakeAdapter({ namespace: 'b', onBeforeApprove: followingHook }),
110+
createFakeAdapter({
111+
namespace: 'a' as KnownCaipNamespace,
112+
onBeforeApprove: failingHook,
113+
}),
114+
createFakeAdapter({
115+
namespace: 'b' as KnownCaipNamespace,
116+
onBeforeApprove: followingHook,
117+
}),
84118
]);
85119

86120
await seedAdapterPermissions({ proposal: {}, channelId: 'channel-2' });
@@ -117,11 +151,11 @@ describe('buildAdapterNamespaces', () => {
117151
};
118152
mockedGetAllAdapters.mockReturnValue([
119153
createFakeAdapter({
120-
namespace: 'tron',
154+
namespace: KnownCaipNamespace.Tron,
121155
buildNamespace: jest.fn().mockReturnValue(tronSlice),
122156
}),
123157
createFakeAdapter({
124-
namespace: 'solana',
158+
namespace: KnownCaipNamespace.Solana,
125159
buildNamespace: jest.fn().mockReturnValue(solanaSlice),
126160
}),
127161
]);
@@ -140,25 +174,25 @@ describe('buildAdapterNamespaces', () => {
140174
};
141175
mockedGetAllAdapters.mockReturnValue([
142176
createFakeAdapter({
143-
namespace: 'tron',
177+
namespace: KnownCaipNamespace.Tron,
144178
buildNamespace: jest.fn().mockReturnValue(tronSlice),
145179
}),
146180
createFakeAdapter({
147-
namespace: 'solana',
181+
namespace: KnownCaipNamespace.Solana,
148182
buildNamespace: jest.fn().mockReturnValue(undefined),
149183
}),
150184
]);
151185

152186
const result = buildAdapterNamespaces({ proposal: {} });
153187

154188
expect(result).toStrictEqual({ tron: tronSlice });
155-
expect(result).not.toHaveProperty('solana');
189+
expect(result).not.toHaveProperty(KnownCaipNamespace.Solana);
156190
});
157191

158192
it('forwards accounts/methods/events from existingNamespaces to each adapter', () => {
159193
const buildNamespace = jest.fn().mockReturnValue(undefined);
160194
mockedGetAllAdapters.mockReturnValue([
161-
createFakeAdapter({ namespace: 'tron', buildNamespace }),
195+
createFakeAdapter({ namespace: KnownCaipNamespace.Tron, buildNamespace }),
162196
]);
163197

164198
buildAdapterNamespaces({
@@ -183,7 +217,7 @@ describe('buildAdapterNamespaces', () => {
183217
it('passes undefined existing fields when the namespace has no entry in existingNamespaces', () => {
184218
const buildNamespace = jest.fn().mockReturnValue(undefined);
185219
mockedGetAllAdapters.mockReturnValue([
186-
createFakeAdapter({ namespace: 'tron', buildNamespace }),
220+
createFakeAdapter({ namespace: KnownCaipNamespace.Tron, buildNamespace }),
187221
]);
188222

189223
buildAdapterNamespaces({ proposal: {}, existingNamespaces: {} });
@@ -201,28 +235,31 @@ describe('proposalReferencedAdapterNamespaces', () => {
201235
it('returns the namespaces of all adapters that recognize the proposal', () => {
202236
mockedGetAllAdapters.mockReturnValue([
203237
createFakeAdapter({
204-
namespace: 'tron',
238+
namespace: KnownCaipNamespace.Tron,
205239
proposalReferencesNamespace: jest.fn().mockReturnValue(true),
206240
}),
207241
createFakeAdapter({
208-
namespace: 'solana',
242+
namespace: KnownCaipNamespace.Solana,
209243
proposalReferencesNamespace: jest.fn().mockReturnValue(false),
210244
}),
211245
createFakeAdapter({
212-
namespace: 'bitcoin',
246+
namespace: KnownCaipNamespace.Bip122,
213247
proposalReferencesNamespace: jest.fn().mockReturnValue(true),
214248
}),
215249
]);
216250

217251
const result = proposalReferencedAdapterNamespaces({});
218252

219-
expect(result).toStrictEqual(['tron', 'bitcoin']);
253+
expect(result).toStrictEqual([
254+
KnownCaipNamespace.Tron,
255+
KnownCaipNamespace.Bip122,
256+
]);
220257
});
221258

222259
it('returns an empty array when no adapter recognizes the proposal', () => {
223260
mockedGetAllAdapters.mockReturnValue([
224261
createFakeAdapter({
225-
namespace: 'tron',
262+
namespace: KnownCaipNamespace.Tron,
226263
proposalReferencesNamespace: jest.fn().mockReturnValue(false),
227264
}),
228265
]);
@@ -238,3 +275,132 @@ describe('proposalReferencedAdapterNamespaces', () => {
238275
expect(proposalReferencedAdapterNamespaces({})).toStrictEqual([]);
239276
});
240277
});
278+
279+
describe('mapRequestForSnap', () => {
280+
it('extracts the CAIP-2 namespace from the scope and looks up the adapter once', () => {
281+
mockedGetAdapter.mockReturnValue(undefined);
282+
283+
mapRequestForSnap({
284+
scope: 'tron:728126428',
285+
method: 'tron_signTransaction',
286+
params: [],
287+
});
288+
289+
expect(mockedGetAdapter).toHaveBeenCalledTimes(1);
290+
expect(mockedGetAdapter).toHaveBeenCalledWith('tron');
291+
});
292+
293+
it('delegates to the matched adapter and returns the mapped request', () => {
294+
const adapterMapped = { method: 'signTransaction', params: { foo: 1 } };
295+
const fakeAdapter = createFakeAdapter({
296+
namespace: KnownCaipNamespace.Tron,
297+
mapRequestForSnap: jest.fn().mockReturnValue(adapterMapped),
298+
});
299+
mockedGetAdapter.mockReturnValue(fakeAdapter);
300+
301+
const result = mapRequestForSnap({
302+
scope: 'tron:728126428',
303+
method: 'tron_signTransaction',
304+
params: [{ raw_data_hex: '0xabc' }],
305+
});
306+
307+
expect(result).toBe(adapterMapped);
308+
expect(fakeAdapter.mapRequestForSnap).toHaveBeenCalledWith({
309+
method: 'tron_signTransaction',
310+
params: [{ raw_data_hex: '0xabc' }],
311+
});
312+
});
313+
314+
it('returns the original method/params when no adapter is registered for the scope', () => {
315+
mockedGetAdapter.mockReturnValue(undefined);
316+
317+
const result = mapRequestForSnap({
318+
scope: 'eip155:1',
319+
method: 'eth_sign',
320+
params: ['0x1', '0x2'],
321+
});
322+
323+
expect(result).toStrictEqual({
324+
method: 'eth_sign',
325+
params: ['0x1', '0x2'],
326+
});
327+
});
328+
});
329+
330+
describe('normalizeSnapResponse', () => {
331+
it('delegates to the matched adapter and returns its normalized result', () => {
332+
const adapterResult = { txID: 'tx-1', signature: ['0xsig'] };
333+
const fakeAdapter = createFakeAdapter({
334+
namespace: KnownCaipNamespace.Tron,
335+
normalizeSnapResponse: jest.fn().mockReturnValue(adapterResult),
336+
});
337+
mockedGetAdapter.mockReturnValue(fakeAdapter);
338+
339+
const result = normalizeSnapResponse({
340+
scope: 'tron:728126428',
341+
method: 'tron_signTransaction',
342+
params: [],
343+
result: { signature: '0xsig' },
344+
});
345+
346+
expect(result).toBe(adapterResult);
347+
expect(fakeAdapter.normalizeSnapResponse).toHaveBeenCalledWith({
348+
method: 'tron_signTransaction',
349+
params: [],
350+
result: { signature: '0xsig' },
351+
});
352+
});
353+
354+
it('returns the raw snap result when no adapter is registered for the scope', () => {
355+
mockedGetAdapter.mockReturnValue(undefined);
356+
const snapResult = { hello: 'world' };
357+
358+
const result = normalizeSnapResponse({
359+
scope: 'eip155:1',
360+
method: 'eth_sign',
361+
params: [],
362+
result: snapResult,
363+
});
364+
365+
expect(result).toBe(snapResult);
366+
});
367+
});
368+
369+
describe('getRedirectMethodsForChain', () => {
370+
it('returns the redirectMethods of the adapter for the scope namespace', () => {
371+
mockedGetAdapter.mockReturnValue(
372+
createFakeAdapter({
373+
namespace: KnownCaipNamespace.Tron,
374+
redirectMethods: ['tron_signTransaction', 'tron_signMessage'],
375+
}),
376+
);
377+
378+
const result = getRedirectMethodsForChain('tron:728126428');
379+
380+
expect(result).toStrictEqual(['tron_signTransaction', 'tron_signMessage']);
381+
});
382+
383+
it('returns an empty array when no adapter matches the scope', () => {
384+
mockedGetAdapter.mockReturnValue(undefined);
385+
386+
expect(getRedirectMethodsForChain('eip155:1')).toStrictEqual([]);
387+
});
388+
});
389+
390+
describe('CAIP chain id normalization helpers', () => {
391+
it('normalizes tron hex chain ids inbound to decimal', () => {
392+
expect(normalizeCaipChainIdInbound('tron:0x2b6653dc')).toBe(
393+
'tron:728126428',
394+
);
395+
});
396+
397+
it('normalizes tron decimal chain ids outbound to hex', () => {
398+
expect(normalizeCaipChainIdOutbound('tron:728126428')).toBe(
399+
'tron:0x2b6653dc',
400+
);
401+
});
402+
403+
it('keeps non-numeric tron chain references unchanged outbound', () => {
404+
expect(normalizeCaipChainIdOutbound('tron:mainnet')).toBe('tron:mainnet');
405+
});
406+
});

0 commit comments

Comments
 (0)