@canton-network/wallet-gateway-remote 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/auth/jwt-auth-service.d.ts.map +1 -1
- package/dist/auth/jwt-auth-service.js +6 -5
- package/dist/auth/jwt-auth-service.test.d.ts +2 -0
- package/dist/auth/jwt-auth-service.test.d.ts.map +1 -0
- package/dist/auth/jwt-auth-service.test.js +259 -0
- package/dist/auth/jwt-unsafe-auth-service.test.d.ts +2 -0
- package/dist/auth/jwt-unsafe-auth-service.test.d.ts.map +1 -0
- package/dist/auth/jwt-unsafe-auth-service.test.js +120 -0
- package/dist/config/Config.d.ts +64 -8
- package/dist/config/Config.d.ts.map +1 -1
- package/dist/config/Config.js +1 -0
- package/dist/config/ConfigUtils.d.ts.map +1 -1
- package/dist/config/ConfigUtils.js +3 -0
- package/dist/dapp-api/controller.d.ts +1 -0
- package/dist/dapp-api/controller.d.ts.map +1 -1
- package/dist/dapp-api/controller.js +87 -26
- package/dist/dapp-api/controller.test.d.ts +2 -0
- package/dist/dapp-api/controller.test.d.ts.map +1 -0
- package/dist/dapp-api/controller.test.js +516 -0
- package/dist/dapp-api/rpc-gen/index.d.ts +3 -0
- package/dist/dapp-api/rpc-gen/index.d.ts.map +1 -1
- package/dist/dapp-api/rpc-gen/index.js +1 -0
- package/dist/dapp-api/rpc-gen/typings.d.ts +99 -14
- package/dist/dapp-api/rpc-gen/typings.d.ts.map +1 -1
- package/dist/dapp-api/server.d.ts.map +1 -1
- package/dist/dapp-api/server.js +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +4 -6
- package/dist/ledger/transaction-service.d.ts +14 -11
- package/dist/ledger/transaction-service.d.ts.map +1 -1
- package/dist/ledger/transaction-service.js +95 -8
- package/dist/ledger/transaction-service.test.d.ts +2 -0
- package/dist/ledger/transaction-service.test.d.ts.map +1 -0
- package/dist/ledger/transaction-service.test.js +356 -0
- package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.d.ts +5 -2
- package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.d.ts.map +1 -1
- package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.js +7 -6
- package/dist/ledger/wallet-allocation/wallet-allocation-service.d.ts +11 -5
- package/dist/ledger/wallet-allocation/wallet-allocation-service.d.ts.map +1 -1
- package/dist/ledger/wallet-allocation/wallet-allocation-service.js +28 -14
- package/dist/ledger/wallet-allocation/wallet-allocation-service.test.js +550 -19
- package/dist/ledger/wallet-sync-service.js +3 -3
- package/dist/middleware/apiKeyAuth.d.ts +6 -0
- package/dist/middleware/apiKeyAuth.d.ts.map +1 -0
- package/dist/middleware/apiKeyAuth.js +88 -0
- package/dist/middleware/apiKeyAuth.test.d.ts +2 -0
- package/dist/middleware/apiKeyAuth.test.d.ts.map +1 -0
- package/dist/middleware/apiKeyAuth.test.js +126 -0
- package/dist/middleware/jsonRpcHandler.d.ts +11 -1
- package/dist/middleware/jsonRpcHandler.d.ts.map +1 -1
- package/dist/middleware/jsonRpcHandler.js +14 -15
- package/dist/middleware/jsonRpcHandler.test.d.ts +2 -0
- package/dist/middleware/jsonRpcHandler.test.d.ts.map +1 -0
- package/dist/middleware/jsonRpcHandler.test.js +254 -0
- package/dist/middleware/jwtAuth.d.ts.map +1 -1
- package/dist/middleware/jwtAuth.js +4 -0
- package/dist/middleware/jwtAuth.test.d.ts +2 -0
- package/dist/middleware/jwtAuth.test.d.ts.map +1 -0
- package/dist/middleware/jwtAuth.test.js +93 -0
- package/dist/middleware/rateLimit.js +2 -2
- package/dist/middleware/rateLimit.test.js +26 -0
- package/dist/middleware/sessionHandler.js +1 -1
- package/dist/middleware/sessionHandler.test.d.ts +2 -0
- package/dist/middleware/sessionHandler.test.d.ts.map +1 -0
- package/dist/middleware/sessionHandler.test.js +121 -0
- package/dist/notification/NotificationService.test.d.ts +2 -0
- package/dist/notification/NotificationService.test.d.ts.map +1 -0
- package/dist/notification/NotificationService.test.js +110 -0
- package/dist/signing/signing-drivers.d.ts +7 -0
- package/dist/signing/signing-drivers.d.ts.map +1 -0
- package/dist/signing/signing-drivers.js +3 -0
- package/dist/user-api/controller.d.ts +12 -4
- package/dist/user-api/controller.d.ts.map +1 -1
- package/dist/user-api/controller.js +330 -95
- package/dist/user-api/controller.test.d.ts +2 -0
- package/dist/user-api/controller.test.d.ts.map +1 -0
- package/dist/user-api/controller.test.js +862 -0
- package/dist/user-api/rpc-gen/index.d.ts +30 -0
- package/dist/user-api/rpc-gen/index.d.ts.map +1 -1
- package/dist/user-api/rpc-gen/index.js +10 -0
- package/dist/user-api/rpc-gen/typings.d.ts +170 -25
- package/dist/user-api/rpc-gen/typings.d.ts.map +1 -1
- package/dist/user-api/server.test.js +53 -0
- package/dist/utils.d.ts +19 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +53 -2
- package/dist/utils.test.d.ts +2 -0
- package/dist/utils.test.d.ts.map +1 -0
- package/dist/utils.test.js +24 -0
- package/dist/web/frontend/404/index.html +2 -2
- package/dist/web/frontend/activities/index.html +3 -3
- package/dist/web/frontend/approve/index.html +4 -5
- package/dist/web/frontend/assets/404-MVLWUPwN.js +5 -0
- package/dist/web/frontend/assets/activities-DaWkmGqA.js +61 -0
- package/dist/web/frontend/assets/addIdentityProvider--rBb0ihb.js +28 -0
- package/dist/web/frontend/assets/addNetwork-C6UDsRkU.js +38 -0
- package/dist/web/frontend/assets/addParty-B3fFD1eM.js +45 -0
- package/dist/web/frontend/assets/approve-WbGL4E82.js +21 -0
- package/dist/web/frontend/assets/callback--tihEryB.js +1 -0
- package/dist/web/frontend/assets/identityProviders-Dra4qG6t.js +71 -0
- package/dist/web/frontend/assets/index-Bm76pXA_.js +91 -0
- package/dist/web/frontend/assets/index-D1dC4Cl9.js +4477 -0
- package/dist/web/frontend/assets/login-BC8a-kD_.js +10 -0
- package/dist/web/frontend/assets/networks-B3ztsogL.js +73 -0
- package/dist/web/frontend/assets/reviewIdentityProvider-Co420Dbk.js +43 -0
- package/dist/web/frontend/assets/reviewNetwork-B-Sh8pCH.js +43 -0
- package/dist/web/frontend/assets/{settings-B1ga2TK0.js → settings-DvyUjqLl.js} +1 -1
- package/dist/web/frontend/assets/state-Bq65GwdF.js +1 -0
- package/dist/web/frontend/assets/{utils-CT9Hzi7v.js → utils-B2iziW8V.js} +1 -1
- package/dist/web/frontend/callback/index.html +2 -2
- package/dist/web/frontend/identity-providers/add/index.html +3 -3
- package/dist/web/frontend/identity-providers/index.html +3 -3
- package/dist/web/frontend/identity-providers/review/index.html +3 -3
- package/dist/web/frontend/index.html +1 -1
- package/dist/web/frontend/login/index.html +3 -4
- package/dist/web/frontend/networks/add/index.html +3 -3
- package/dist/web/frontend/networks/index.html +3 -3
- package/dist/web/frontend/networks/review/index.html +3 -3
- package/dist/web/frontend/parties/add/index.html +5 -5
- package/dist/web/frontend/parties/index.html +4 -4
- package/dist/web/frontend/settings/index.html +3 -3
- package/package.json +40 -33
- package/dist/web/frontend/assets/404-B-o9ppJB.js +0 -5
- package/dist/web/frontend/assets/activities-CGjCIjUH.js +0 -61
- package/dist/web/frontend/assets/addIdentityProvider-CR4Wm9Tl.js +0 -28
- package/dist/web/frontend/assets/addNetwork-Dx0-SN4j.js +0 -38
- package/dist/web/frontend/assets/addParty-COhk_rFn.js +0 -41
- package/dist/web/frontend/assets/approve-B2w66l0J.js +0 -21
- package/dist/web/frontend/assets/callback-BTVon_yQ.js +0 -1
- package/dist/web/frontend/assets/identityProviders-CK8zSrd3.js +0 -71
- package/dist/web/frontend/assets/index-CF4BKzgl.js +0 -91
- package/dist/web/frontend/assets/index-DWz_3f3y.js +0 -1
- package/dist/web/frontend/assets/index-_pMHlJoE.js +0 -4042
- package/dist/web/frontend/assets/login-CVoPNVDw.js +0 -10
- package/dist/web/frontend/assets/networks-D1nPvUzM.js +0 -73
- package/dist/web/frontend/assets/reviewIdentityProvider-CNSf2qQv.js +0 -43
- package/dist/web/frontend/assets/reviewNetwork-DGLK-Ume.js +0 -43
- package/dist/web/frontend/assets/state-B2k3ak7d.js +0 -1
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { pino } from 'pino';
|
|
5
|
+
import { sink } from 'pino-test';
|
|
6
|
+
import { PartyLevelRight, } from '@canton-network/core-wallet-store';
|
|
7
|
+
import { StoreInternal } from '@canton-network/core-wallet-store-inmemory';
|
|
8
|
+
import { SigningProvider } from '@canton-network/core-signing-lib';
|
|
9
|
+
import { NotificationService } from '../notification/NotificationService.js';
|
|
10
|
+
import { userController } from './controller.js';
|
|
11
|
+
const ledgerMocks = vi.hoisted(() => ({
|
|
12
|
+
getWithRetry: vi.fn(),
|
|
13
|
+
postWithRetry: vi.fn(),
|
|
14
|
+
getSynchronizerId: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
const mockNetworkStatus = vi.hoisted(() => vi.fn().mockResolvedValue({
|
|
17
|
+
isConnected: true,
|
|
18
|
+
reason: undefined,
|
|
19
|
+
cantonVersion: '3.4',
|
|
20
|
+
}));
|
|
21
|
+
const walletAllocationMocks = vi.hoisted(() => ({
|
|
22
|
+
createWallet: vi.fn(),
|
|
23
|
+
allocateParty: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
const walletSyncMocks = vi.hoisted(() => ({
|
|
26
|
+
syncWallets: vi.fn().mockResolvedValue({
|
|
27
|
+
added: [],
|
|
28
|
+
updated: [],
|
|
29
|
+
disabled: [],
|
|
30
|
+
}),
|
|
31
|
+
isWalletSyncNeeded: vi.fn().mockResolvedValue(false),
|
|
32
|
+
}));
|
|
33
|
+
const transactionServiceMocks = vi.hoisted(() => ({
|
|
34
|
+
sign: vi.fn(),
|
|
35
|
+
execute: vi.fn(),
|
|
36
|
+
}));
|
|
37
|
+
vi.mock('@canton-network/core-ledger-client', async (importOriginal) => {
|
|
38
|
+
const actual = await importOriginal();
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
LedgerClient: vi.fn(function LedgerClientMock() {
|
|
42
|
+
return {
|
|
43
|
+
getWithRetry: ledgerMocks.getWithRetry,
|
|
44
|
+
postWithRetry: ledgerMocks.postWithRetry,
|
|
45
|
+
getSynchronizerId: ledgerMocks.getSynchronizerId,
|
|
46
|
+
};
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
vi.mock('../utils.js', async (importOriginal) => {
|
|
51
|
+
const actual = await importOriginal();
|
|
52
|
+
return {
|
|
53
|
+
...actual,
|
|
54
|
+
networkStatus: mockNetworkStatus,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
vi.mock('../ledger/wallet-allocation/wallet-allocation-service.js', () => ({
|
|
58
|
+
WalletAllocationService: vi.fn(function WalletAllocationServiceMock() {
|
|
59
|
+
return walletAllocationMocks;
|
|
60
|
+
}),
|
|
61
|
+
}));
|
|
62
|
+
vi.mock('../ledger/wallet-sync-service.js', () => ({
|
|
63
|
+
WalletSyncService: vi.fn(function WalletSyncServiceMock() {
|
|
64
|
+
return walletSyncMocks;
|
|
65
|
+
}),
|
|
66
|
+
}));
|
|
67
|
+
vi.mock('../ledger/party-allocation-service.js', () => ({
|
|
68
|
+
PartyAllocationService: vi.fn(),
|
|
69
|
+
}));
|
|
70
|
+
vi.mock('../ledger/transaction-service.js', () => ({
|
|
71
|
+
TransactionService: vi.fn(function TransactionServiceMock() {
|
|
72
|
+
return transactionServiceMocks;
|
|
73
|
+
}),
|
|
74
|
+
}));
|
|
75
|
+
const kernelInfo = {
|
|
76
|
+
id: 'kernel-test',
|
|
77
|
+
clientType: 'browser',
|
|
78
|
+
};
|
|
79
|
+
const userUrl = 'https://user.example';
|
|
80
|
+
const regularUserId = 'user-1';
|
|
81
|
+
const adminUserId = 'admin-user';
|
|
82
|
+
const idp = {
|
|
83
|
+
id: 'idp1',
|
|
84
|
+
type: 'oauth',
|
|
85
|
+
issuer: 'http://auth',
|
|
86
|
+
configUrl: 'http://auth/.well-known/openid-configuration',
|
|
87
|
+
};
|
|
88
|
+
const storeNetwork = {
|
|
89
|
+
id: 'network1',
|
|
90
|
+
name: 'testnet',
|
|
91
|
+
synchronizerId: 'sync1::fingerprint',
|
|
92
|
+
description: 'Test',
|
|
93
|
+
identityProviderId: 'idp1',
|
|
94
|
+
ledgerApi: { baseUrl: 'http://ledger.test' },
|
|
95
|
+
auth: {
|
|
96
|
+
method: 'authorization_code',
|
|
97
|
+
clientId: 'cid',
|
|
98
|
+
scope: 'scope',
|
|
99
|
+
audience: 'aud',
|
|
100
|
+
},
|
|
101
|
+
adminAuth: {
|
|
102
|
+
method: 'client_credentials',
|
|
103
|
+
clientId: 'admin-cid',
|
|
104
|
+
clientSecret: 'admin-secret',
|
|
105
|
+
audience: 'admin-aud',
|
|
106
|
+
scope: 'admin-scope',
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const auth = {
|
|
110
|
+
userId: regularUserId,
|
|
111
|
+
accessToken: 'access-token-1',
|
|
112
|
+
};
|
|
113
|
+
const adminAuth = {
|
|
114
|
+
userId: adminUserId,
|
|
115
|
+
accessToken: 'admin-access-token',
|
|
116
|
+
};
|
|
117
|
+
const session = {
|
|
118
|
+
id: 'session-1',
|
|
119
|
+
network: 'network1',
|
|
120
|
+
accessToken: 'session-token',
|
|
121
|
+
};
|
|
122
|
+
const pendingTransaction = {
|
|
123
|
+
id: 'tx-1',
|
|
124
|
+
commandId: 'cmd-1',
|
|
125
|
+
status: 'pending',
|
|
126
|
+
preparedTransaction: 'blob',
|
|
127
|
+
preparedTransactionHash: 'hash',
|
|
128
|
+
origin: null,
|
|
129
|
+
createdAt: new Date('2026-01-01T00:00:00.000Z'),
|
|
130
|
+
};
|
|
131
|
+
const primaryWallet = {
|
|
132
|
+
primary: true,
|
|
133
|
+
partyId: 'party::namespace',
|
|
134
|
+
status: 'allocated',
|
|
135
|
+
hint: 'party',
|
|
136
|
+
signingProviderId: SigningProvider.WALLET_KERNEL,
|
|
137
|
+
publicKey: 'wallet-public-key',
|
|
138
|
+
namespace: 'namespace',
|
|
139
|
+
networkId: 'network1',
|
|
140
|
+
rights: [PartyLevelRight.CanActAs],
|
|
141
|
+
};
|
|
142
|
+
const participantWallet = {
|
|
143
|
+
...primaryWallet,
|
|
144
|
+
partyId: 'party::participant',
|
|
145
|
+
signingProviderId: SigningProvider.PARTICIPANT,
|
|
146
|
+
};
|
|
147
|
+
async function createStore(logger, context, options = {}) {
|
|
148
|
+
const { withSession = true, withWallet = true } = options;
|
|
149
|
+
const store = new StoreInternal({ idps: [idp], networks: [storeNetwork] }, logger, context);
|
|
150
|
+
if (context && withSession) {
|
|
151
|
+
await store.setSession(session);
|
|
152
|
+
}
|
|
153
|
+
if (context && withWallet) {
|
|
154
|
+
await store.addWallet(primaryWallet);
|
|
155
|
+
}
|
|
156
|
+
return store;
|
|
157
|
+
}
|
|
158
|
+
function createController(store, notificationService, logger, context, drivers = {}, adminId) {
|
|
159
|
+
return userController(kernelInfo, userUrl, store, notificationService, context, drivers, logger, adminId);
|
|
160
|
+
}
|
|
161
|
+
describe('userController', () => {
|
|
162
|
+
let logger;
|
|
163
|
+
let notificationService;
|
|
164
|
+
beforeEach(() => {
|
|
165
|
+
logger = pino({ level: 'silent' }, sink());
|
|
166
|
+
notificationService = new NotificationService(logger);
|
|
167
|
+
ledgerMocks.getWithRetry.mockReset();
|
|
168
|
+
ledgerMocks.getWithRetry.mockResolvedValue({ rights: [] });
|
|
169
|
+
ledgerMocks.postWithRetry.mockReset();
|
|
170
|
+
ledgerMocks.getSynchronizerId.mockReset();
|
|
171
|
+
mockNetworkStatus.mockReset();
|
|
172
|
+
mockNetworkStatus.mockResolvedValue({
|
|
173
|
+
isConnected: true,
|
|
174
|
+
reason: undefined,
|
|
175
|
+
cantonVersion: '3.4',
|
|
176
|
+
});
|
|
177
|
+
walletAllocationMocks.createWallet.mockReset();
|
|
178
|
+
walletAllocationMocks.allocateParty.mockReset();
|
|
179
|
+
walletSyncMocks.syncWallets.mockReset();
|
|
180
|
+
walletSyncMocks.syncWallets.mockResolvedValue({
|
|
181
|
+
added: [],
|
|
182
|
+
updated: [],
|
|
183
|
+
disabled: [],
|
|
184
|
+
});
|
|
185
|
+
walletSyncMocks.isWalletSyncNeeded.mockReset();
|
|
186
|
+
walletSyncMocks.isWalletSyncNeeded.mockResolvedValue(false);
|
|
187
|
+
transactionServiceMocks.sign.mockReset();
|
|
188
|
+
transactionServiceMocks.execute.mockReset();
|
|
189
|
+
});
|
|
190
|
+
afterEach(() => {
|
|
191
|
+
vi.clearAllMocks();
|
|
192
|
+
});
|
|
193
|
+
describe('getUser', () => {
|
|
194
|
+
it('returns user id and isAdmin false for a regular user', async () => {
|
|
195
|
+
const store = await createStore(logger, auth);
|
|
196
|
+
const controller = createController(store, notificationService, logger, auth, {}, adminUserId);
|
|
197
|
+
await expect(controller.getUser()).resolves.toEqual({
|
|
198
|
+
userId: regularUserId,
|
|
199
|
+
isAdmin: false,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it('returns isAdmin true when the user matches adminUserId', async () => {
|
|
203
|
+
const store = await createStore(logger, adminAuth);
|
|
204
|
+
const controller = createController(store, notificationService, logger, adminAuth, {}, adminUserId);
|
|
205
|
+
await expect(controller.getUser()).resolves.toEqual({
|
|
206
|
+
userId: adminUserId,
|
|
207
|
+
isAdmin: true,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
it('throws when auth context is missing', async () => {
|
|
211
|
+
const store = await createStore(logger, auth, {
|
|
212
|
+
withSession: false,
|
|
213
|
+
});
|
|
214
|
+
const controller = createController(store, notificationService, logger, undefined);
|
|
215
|
+
await expect(controller.getUser()).rejects.toThrow();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('networks', () => {
|
|
219
|
+
it('rejects addNetwork for non-admin users', async () => {
|
|
220
|
+
const store = await createStore(logger, auth);
|
|
221
|
+
const controller = createController(store, notificationService, logger, auth, {}, adminUserId);
|
|
222
|
+
await expect(controller.addNetwork({
|
|
223
|
+
network: {
|
|
224
|
+
id: 'net-new',
|
|
225
|
+
name: 'New Net',
|
|
226
|
+
synchronizerId: 'sync::fingerprint',
|
|
227
|
+
identityProviderId: 'idp1',
|
|
228
|
+
ledgerApi: 'http://ledger.new',
|
|
229
|
+
auth: storeNetwork.auth,
|
|
230
|
+
description: 'description',
|
|
231
|
+
},
|
|
232
|
+
})).rejects.toThrow('Unauthorized');
|
|
233
|
+
});
|
|
234
|
+
it('adds a new network for the admin user', async () => {
|
|
235
|
+
const store = await createStore(logger, adminAuth);
|
|
236
|
+
const addSpy = vi.spyOn(store, 'addNetwork');
|
|
237
|
+
const controller = createController(store, notificationService, logger, adminAuth, {}, adminUserId);
|
|
238
|
+
await controller.addNetwork({
|
|
239
|
+
network: {
|
|
240
|
+
id: 'net-new',
|
|
241
|
+
name: 'New Net',
|
|
242
|
+
synchronizerId: 'sync::fingerprint',
|
|
243
|
+
identityProviderId: 'idp1',
|
|
244
|
+
ledgerApi: 'http://ledger.new',
|
|
245
|
+
auth: storeNetwork.auth,
|
|
246
|
+
description: 'description',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
expect(addSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
250
|
+
id: 'net-new',
|
|
251
|
+
ledgerApi: { baseUrl: 'http://ledger.new' },
|
|
252
|
+
}));
|
|
253
|
+
});
|
|
254
|
+
it('updates an existing network for the admin user', async () => {
|
|
255
|
+
const store = await createStore(logger, adminAuth);
|
|
256
|
+
const updateSpy = vi.spyOn(store, 'updateNetwork');
|
|
257
|
+
const controller = createController(store, notificationService, logger, adminAuth, {}, adminUserId);
|
|
258
|
+
await controller.addNetwork({
|
|
259
|
+
network: {
|
|
260
|
+
id: 'network1',
|
|
261
|
+
name: 'Renamed Net',
|
|
262
|
+
synchronizerId: storeNetwork.synchronizerId,
|
|
263
|
+
identityProviderId: 'idp1',
|
|
264
|
+
ledgerApi: 'http://ledger.updated',
|
|
265
|
+
auth: storeNetwork.auth,
|
|
266
|
+
description: 'description',
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
expect(updateSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
270
|
+
id: 'network1',
|
|
271
|
+
name: 'Renamed Net',
|
|
272
|
+
}));
|
|
273
|
+
});
|
|
274
|
+
it('removes a network for the admin user', async () => {
|
|
275
|
+
const store = await createStore(logger, adminAuth);
|
|
276
|
+
const removeSpy = vi.spyOn(store, 'removeNetwork');
|
|
277
|
+
const controller = createController(store, notificationService, logger, adminAuth, {}, adminUserId);
|
|
278
|
+
await controller.removeNetwork({ networkName: 'testnet' });
|
|
279
|
+
expect(removeSpy).toHaveBeenCalledWith('testnet');
|
|
280
|
+
});
|
|
281
|
+
it('lists networks with ledgerApi as a string', async () => {
|
|
282
|
+
const store = await createStore(logger, auth);
|
|
283
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
284
|
+
const result = await controller.listNetworks();
|
|
285
|
+
expect(result.networks).toHaveLength(1);
|
|
286
|
+
expect(result.networks[0]).toMatchObject({
|
|
287
|
+
id: 'network1',
|
|
288
|
+
ledgerApi: 'http://ledger.test',
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
describe('idps', () => {
|
|
293
|
+
it('adds and removes an idp for the admin user', async () => {
|
|
294
|
+
const store = await createStore(logger, adminAuth);
|
|
295
|
+
const addIdpSpy = vi.spyOn(store, 'addIdp');
|
|
296
|
+
const removeIdpSpy = vi.spyOn(store, 'removeIdp');
|
|
297
|
+
const controller = createController(store, notificationService, logger, adminAuth, {}, adminUserId);
|
|
298
|
+
const newIdp = {
|
|
299
|
+
id: 'idp-new',
|
|
300
|
+
type: 'self_signed',
|
|
301
|
+
issuer: 'self',
|
|
302
|
+
};
|
|
303
|
+
await controller.addIdp({ idp: newIdp });
|
|
304
|
+
expect(addIdpSpy).toHaveBeenCalledWith(newIdp);
|
|
305
|
+
await controller.removeIdp({ identityProviderId: 'idp-new' });
|
|
306
|
+
expect(removeIdpSpy).toHaveBeenCalledWith('idp-new');
|
|
307
|
+
});
|
|
308
|
+
it('updates an existing idp for the admin user', async () => {
|
|
309
|
+
const store = await createStore(logger, adminAuth);
|
|
310
|
+
const updateIdpSpy = vi.spyOn(store, 'updateIdp');
|
|
311
|
+
const controller = createController(store, notificationService, logger, adminAuth, {}, adminUserId);
|
|
312
|
+
const updatedIdp = {
|
|
313
|
+
...idp,
|
|
314
|
+
issuer: 'http://auth-updated',
|
|
315
|
+
};
|
|
316
|
+
await controller.addIdp({ idp: updatedIdp });
|
|
317
|
+
expect(updateIdpSpy).toHaveBeenCalledWith(updatedIdp);
|
|
318
|
+
});
|
|
319
|
+
it('lists idps from the store', async () => {
|
|
320
|
+
const store = await createStore(logger, auth);
|
|
321
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
322
|
+
await expect(controller.listIdps()).resolves.toEqual({
|
|
323
|
+
idps: [idp],
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
describe('setPrimaryWallet', () => {
|
|
328
|
+
it('sets the primary wallet and emits accountsChanged', async () => {
|
|
329
|
+
const store = await createStore(logger, auth);
|
|
330
|
+
const setPrimarySpy = vi.spyOn(store, 'setPrimaryWallet');
|
|
331
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
332
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
333
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
334
|
+
await controller.setPrimaryWallet({ partyId: 'party::namespace' });
|
|
335
|
+
expect(setPrimarySpy).toHaveBeenCalledWith('party::namespace');
|
|
336
|
+
expect(emitSpy).toHaveBeenCalledWith('accountsChanged', expect.arrayContaining([
|
|
337
|
+
expect.objectContaining({ partyId: 'party::namespace' }),
|
|
338
|
+
]));
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
describe('listWallets', () => {
|
|
342
|
+
it('returns wallets from the store', async () => {
|
|
343
|
+
const store = await createStore(logger, auth);
|
|
344
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
345
|
+
const wallets = await controller.listWallets({});
|
|
346
|
+
expect(wallets).toHaveLength(1);
|
|
347
|
+
expect(wallets[0]?.partyId).toBe('party::namespace');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
describe('message signing', () => {
|
|
351
|
+
const pendingMessage = {
|
|
352
|
+
id: 'msg-1',
|
|
353
|
+
status: 'pending',
|
|
354
|
+
userId: auth.userId,
|
|
355
|
+
partyId: primaryWallet.partyId,
|
|
356
|
+
publicKey: primaryWallet.publicKey,
|
|
357
|
+
message: 'Sign this',
|
|
358
|
+
origin: 'https://dapp.example',
|
|
359
|
+
createdAt: new Date('2026-01-01T00:00:00.000Z'),
|
|
360
|
+
};
|
|
361
|
+
async function storeWithMessage(message = pendingMessage) {
|
|
362
|
+
const store = await createStore(logger, auth);
|
|
363
|
+
await store.setMessageRaw(message);
|
|
364
|
+
return store;
|
|
365
|
+
}
|
|
366
|
+
it('returns message details via getMessageToSign', async () => {
|
|
367
|
+
const store = await storeWithMessage();
|
|
368
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
369
|
+
const result = await controller.getMessageToSign({
|
|
370
|
+
messageId: 'msg-1',
|
|
371
|
+
});
|
|
372
|
+
expect(result.message).toMatchObject({
|
|
373
|
+
id: 'msg-1',
|
|
374
|
+
status: 'pending',
|
|
375
|
+
message: 'Sign this',
|
|
376
|
+
origin: 'https://dapp.example',
|
|
377
|
+
createdAt: pendingMessage.createdAt.toISOString(),
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
it('lists all messages to sign', async () => {
|
|
381
|
+
const store = await storeWithMessage();
|
|
382
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
383
|
+
const result = await controller.listMessagesToSign();
|
|
384
|
+
expect(result.messages).toHaveLength(1);
|
|
385
|
+
expect(result.messages[0]?.id).toBe('msg-1');
|
|
386
|
+
});
|
|
387
|
+
it('deletes a pending message owned by the user', async () => {
|
|
388
|
+
const store = await storeWithMessage();
|
|
389
|
+
const removeSpy = vi.spyOn(store, 'removeMessageRaw');
|
|
390
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
391
|
+
await controller.deleteMessageToSign({ messageId: 'msg-1' });
|
|
392
|
+
expect(removeSpy).toHaveBeenCalledWith('msg-1');
|
|
393
|
+
});
|
|
394
|
+
it('rejects delete when the message is not pending', async () => {
|
|
395
|
+
const store = await storeWithMessage({
|
|
396
|
+
...pendingMessage,
|
|
397
|
+
status: 'signed',
|
|
398
|
+
});
|
|
399
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
400
|
+
await expect(controller.deleteMessageToSign({ messageId: 'msg-1' })).rejects.toThrow("Cannot delete message with status 'signed'");
|
|
401
|
+
});
|
|
402
|
+
it('rejects delete when the message belongs to another user', async () => {
|
|
403
|
+
const store = await storeWithMessage();
|
|
404
|
+
vi.spyOn(store, 'getMessageRaw').mockResolvedValue({
|
|
405
|
+
...pendingMessage,
|
|
406
|
+
userId: 'other-user',
|
|
407
|
+
});
|
|
408
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
409
|
+
await expect(controller.deleteMessageToSign({ messageId: 'msg-1' })).rejects.toThrow('not owned by user');
|
|
410
|
+
});
|
|
411
|
+
it('signs a pending WALLET_KERNEL message and emits messageSignature', async () => {
|
|
412
|
+
const store = await storeWithMessage();
|
|
413
|
+
const mockSignMessage = vi.fn().mockResolvedValue({
|
|
414
|
+
signature: 'signature',
|
|
415
|
+
});
|
|
416
|
+
const drivers = {
|
|
417
|
+
[SigningProvider.WALLET_KERNEL]: {
|
|
418
|
+
controller: vi.fn(() => ({ signMessage: mockSignMessage })),
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
422
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
423
|
+
const controller = createController(store, notificationService, logger, auth, drivers);
|
|
424
|
+
const result = await controller.signMessage({ messageId: 'msg-1' });
|
|
425
|
+
expect(mockSignMessage).toHaveBeenCalledWith({
|
|
426
|
+
message: 'Sign this',
|
|
427
|
+
keyIdentifier: { publicKey: primaryWallet.publicKey },
|
|
428
|
+
});
|
|
429
|
+
expect(result).toEqual({
|
|
430
|
+
signature: 'signature',
|
|
431
|
+
publicKey: primaryWallet.publicKey,
|
|
432
|
+
});
|
|
433
|
+
expect(emitSpy).toHaveBeenCalledWith('messageSignature', {
|
|
434
|
+
status: 'signed',
|
|
435
|
+
messageId: 'msg-1',
|
|
436
|
+
signature: 'signature',
|
|
437
|
+
});
|
|
438
|
+
const updated = await store.getMessageRaw('msg-1');
|
|
439
|
+
expect(updated?.status).toBe('signed');
|
|
440
|
+
expect(updated?.signature).toBe('signature');
|
|
441
|
+
});
|
|
442
|
+
it('rejects signMessage for non-WALLET_KERNEL wallets', async () => {
|
|
443
|
+
const store = await createStore(logger, auth);
|
|
444
|
+
await store.removeWallet(primaryWallet.partyId);
|
|
445
|
+
await store.addWallet({
|
|
446
|
+
...primaryWallet,
|
|
447
|
+
signingProviderId: SigningProvider.FIREBLOCKS,
|
|
448
|
+
});
|
|
449
|
+
await store.setMessageRaw(pendingMessage);
|
|
450
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
451
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
452
|
+
const controller = createController(store, notificationService, logger, auth, {
|
|
453
|
+
[SigningProvider.WALLET_KERNEL]: {
|
|
454
|
+
controller: vi.fn(() => ({
|
|
455
|
+
signMessage: vi.fn(),
|
|
456
|
+
})),
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
await expect(controller.signMessage({ messageId: 'msg-1' })).rejects.toThrow('only supported for wallet-kernel');
|
|
460
|
+
expect(emitSpy).toHaveBeenCalledWith('messageSignature', {
|
|
461
|
+
status: 'failed',
|
|
462
|
+
messageId: 'msg-1',
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
describe('transactions', () => {
|
|
467
|
+
const transaction = pendingTransaction;
|
|
468
|
+
it('returns transaction details via getTransaction', async () => {
|
|
469
|
+
const store = await createStore(logger, auth);
|
|
470
|
+
await store.setTransaction(transaction);
|
|
471
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
472
|
+
const result = await controller.getTransaction({
|
|
473
|
+
transactionId: 'tx-1',
|
|
474
|
+
});
|
|
475
|
+
expect(result).toMatchObject({
|
|
476
|
+
id: 'tx-1',
|
|
477
|
+
commandId: 'cmd-1',
|
|
478
|
+
status: 'pending',
|
|
479
|
+
createdAt: transaction.createdAt.toISOString(),
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
it('lists transactions', async () => {
|
|
483
|
+
const store = await createStore(logger, auth);
|
|
484
|
+
await store.setTransaction(transaction);
|
|
485
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
486
|
+
const result = await controller.listTransactions();
|
|
487
|
+
expect(result.transactions).toHaveLength(1);
|
|
488
|
+
expect(result.transactions[0]?.id).toBe('tx-1');
|
|
489
|
+
});
|
|
490
|
+
it('deletes a pending transaction', async () => {
|
|
491
|
+
const store = await createStore(logger, auth);
|
|
492
|
+
await store.setTransaction(transaction);
|
|
493
|
+
const removeSpy = vi.spyOn(store, 'removeTransaction');
|
|
494
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
495
|
+
await controller.deleteTransaction({ transactionId: 'tx-1' });
|
|
496
|
+
expect(removeSpy).toHaveBeenCalledWith('tx-1');
|
|
497
|
+
});
|
|
498
|
+
it('rejects delete when the transaction is not pending', async () => {
|
|
499
|
+
const store = await createStore(logger, auth);
|
|
500
|
+
await store.setTransaction({ ...transaction, status: 'signed' });
|
|
501
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
502
|
+
await expect(controller.deleteTransaction({ transactionId: 'tx-1' })).rejects.toThrow("Cannot delete transaction with status 'signed'");
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
describe('sign', () => {
|
|
506
|
+
it('throws when there is no network session', async () => {
|
|
507
|
+
const store = await createStore(logger, auth, {
|
|
508
|
+
withSession: false,
|
|
509
|
+
});
|
|
510
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
511
|
+
await expect(controller.sign({
|
|
512
|
+
transactionId: 'tx-1',
|
|
513
|
+
partyId: primaryWallet.partyId,
|
|
514
|
+
})).rejects.toThrow('No session found');
|
|
515
|
+
});
|
|
516
|
+
it('delegates to TransactionService.sign', async () => {
|
|
517
|
+
const store = await createStore(logger, auth);
|
|
518
|
+
await store.removeWallet(primaryWallet.partyId);
|
|
519
|
+
await store.addWallet(participantWallet);
|
|
520
|
+
transactionServiceMocks.sign.mockReturnValue({
|
|
521
|
+
status: 'signed',
|
|
522
|
+
signature: 'none',
|
|
523
|
+
signedBy: 'namespace',
|
|
524
|
+
partyId: participantWallet.partyId,
|
|
525
|
+
});
|
|
526
|
+
const controller = createController(store, notificationService, logger, auth, {
|
|
527
|
+
[SigningProvider.PARTICIPANT]: {
|
|
528
|
+
controller: vi.fn(() => ({})),
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
const signParams = {
|
|
532
|
+
transactionId: 'tx-1',
|
|
533
|
+
partyId: participantWallet.partyId,
|
|
534
|
+
};
|
|
535
|
+
const result = await controller.sign(signParams);
|
|
536
|
+
expect(transactionServiceMocks.sign).toHaveBeenCalledWith(auth, participantWallet, signParams);
|
|
537
|
+
expect(result).toMatchObject({ status: 'signed' });
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
describe('execute', () => {
|
|
541
|
+
const executeParams = {
|
|
542
|
+
signature: 'sig',
|
|
543
|
+
partyId: primaryWallet.partyId,
|
|
544
|
+
transactionId: pendingTransaction.id,
|
|
545
|
+
signedBy: primaryWallet.namespace,
|
|
546
|
+
};
|
|
547
|
+
it('delegates to TransactionService.execute', async () => {
|
|
548
|
+
const store = await createStore(logger, auth);
|
|
549
|
+
await store.removeWallet(primaryWallet.partyId);
|
|
550
|
+
await store.addWallet(participantWallet);
|
|
551
|
+
await store.setTransaction(pendingTransaction);
|
|
552
|
+
transactionServiceMocks.execute.mockResolvedValue({
|
|
553
|
+
commandId: pendingTransaction.commandId,
|
|
554
|
+
});
|
|
555
|
+
const controller = createController(store, notificationService, logger, auth, {
|
|
556
|
+
[SigningProvider.PARTICIPANT]: {
|
|
557
|
+
controller: vi.fn(() => ({})),
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
const params = {
|
|
561
|
+
...executeParams,
|
|
562
|
+
partyId: participantWallet.partyId,
|
|
563
|
+
};
|
|
564
|
+
const result = await controller.execute(params);
|
|
565
|
+
expect(transactionServiceMocks.execute).toHaveBeenCalledWith(auth.userId, participantWallet, expect.objectContaining({ id: pendingTransaction.id }), params, expect.objectContaining({
|
|
566
|
+
getWithRetry: ledgerMocks.getWithRetry,
|
|
567
|
+
}), expect.objectContaining({ id: storeNetwork.id }));
|
|
568
|
+
expect(result).toEqual({ commandId: pendingTransaction.commandId });
|
|
569
|
+
});
|
|
570
|
+
it('delegates execute for external signing providers', async () => {
|
|
571
|
+
const store = await createStore(logger, auth);
|
|
572
|
+
await store.setTransaction(pendingTransaction);
|
|
573
|
+
transactionServiceMocks.execute.mockResolvedValue({
|
|
574
|
+
commandId: pendingTransaction.commandId,
|
|
575
|
+
});
|
|
576
|
+
const controller = createController(store, notificationService, logger, auth, {
|
|
577
|
+
[SigningProvider.WALLET_KERNEL]: {
|
|
578
|
+
controller: vi.fn(() => ({})),
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
const result = await controller.execute(executeParams);
|
|
582
|
+
expect(transactionServiceMocks.execute).toHaveBeenCalledWith(auth.userId, primaryWallet, expect.objectContaining({ id: pendingTransaction.id }), executeParams, expect.objectContaining({
|
|
583
|
+
getWithRetry: ledgerMocks.getWithRetry,
|
|
584
|
+
}), expect.objectContaining({ id: storeNetwork.id }));
|
|
585
|
+
expect(result).toEqual({ commandId: pendingTransaction.commandId });
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
describe('sessions', () => {
|
|
589
|
+
it('returns an empty list when there is no session', async () => {
|
|
590
|
+
const store = await createStore(logger, auth, {
|
|
591
|
+
withSession: false,
|
|
592
|
+
});
|
|
593
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
594
|
+
await expect(controller.listSessions()).resolves.toEqual({
|
|
595
|
+
sessions: [],
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
it('lists the current session with network status', async () => {
|
|
599
|
+
const store = await createStore(logger, auth);
|
|
600
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
601
|
+
const result = await controller.listSessions();
|
|
602
|
+
expect(result.sessions).toHaveLength(1);
|
|
603
|
+
expect(result.sessions[0]).toMatchObject({
|
|
604
|
+
id: 'session-1',
|
|
605
|
+
status: 'connected',
|
|
606
|
+
network: expect.objectContaining({
|
|
607
|
+
id: 'network1',
|
|
608
|
+
ledgerApi: 'http://ledger.test',
|
|
609
|
+
}),
|
|
610
|
+
idp,
|
|
611
|
+
});
|
|
612
|
+
expect(mockNetworkStatus).toHaveBeenCalled();
|
|
613
|
+
});
|
|
614
|
+
it('removeSession clears the session and emits statusChanged', async () => {
|
|
615
|
+
const store = await createStore(logger, auth);
|
|
616
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
617
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
618
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
619
|
+
await controller.removeSession();
|
|
620
|
+
await expect(store.getSession()).resolves.toBeUndefined();
|
|
621
|
+
expect(emitSpy).toHaveBeenCalledWith('statusChanged', expect.objectContaining({
|
|
622
|
+
connection: expect.objectContaining({
|
|
623
|
+
isConnected: false,
|
|
624
|
+
reason: 'disconnect',
|
|
625
|
+
}),
|
|
626
|
+
userUrl: `${userUrl}/login/`,
|
|
627
|
+
}));
|
|
628
|
+
});
|
|
629
|
+
it('addSession creates a session and emits connected', async () => {
|
|
630
|
+
const store = await createStore(logger, auth, { withWallet: false });
|
|
631
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
632
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
633
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
634
|
+
const result = await controller.addSession({
|
|
635
|
+
networkId: 'network1',
|
|
636
|
+
});
|
|
637
|
+
expect(result.network.id).toBe('network1');
|
|
638
|
+
expect(result.status).toBe('connected');
|
|
639
|
+
expect(emitSpy).toHaveBeenCalledWith('connected', expect.objectContaining({
|
|
640
|
+
session: {
|
|
641
|
+
accessToken: auth.accessToken,
|
|
642
|
+
userId: auth.userId,
|
|
643
|
+
},
|
|
644
|
+
}));
|
|
645
|
+
await expect(store.getSession()).resolves.toMatchObject({
|
|
646
|
+
network: 'network1',
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
describe('createWallet', () => {
|
|
651
|
+
const walletKernelDriver = {
|
|
652
|
+
[SigningProvider.WALLET_KERNEL]: { controller: vi.fn() },
|
|
653
|
+
};
|
|
654
|
+
it('throws when no network session exists', async () => {
|
|
655
|
+
const store = await createStore(logger, auth, {
|
|
656
|
+
withSession: false,
|
|
657
|
+
});
|
|
658
|
+
const controller = createController(store, notificationService, logger, auth, { [SigningProvider.WALLET_KERNEL]: { controller: vi.fn() } });
|
|
659
|
+
await expect(controller.createWallet({
|
|
660
|
+
partyHint: 'alice',
|
|
661
|
+
signingProviderId: SigningProvider.WALLET_KERNEL,
|
|
662
|
+
})).rejects.toThrow('No session found');
|
|
663
|
+
});
|
|
664
|
+
it('throws when the signing provider is not configured', async () => {
|
|
665
|
+
const store = await createStore(logger, auth);
|
|
666
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
667
|
+
await expect(controller.createWallet({
|
|
668
|
+
partyHint: 'alice',
|
|
669
|
+
signingProviderId: SigningProvider.WALLET_KERNEL,
|
|
670
|
+
})).rejects.toThrow('Signing provider wallet-kernel not supported');
|
|
671
|
+
});
|
|
672
|
+
it('creates a wallet when the signing provider is configured', async () => {
|
|
673
|
+
const authWithEmail = { ...auth, email: 'user@example.com' };
|
|
674
|
+
const store = await createStore(logger, authWithEmail);
|
|
675
|
+
const newWallet = {
|
|
676
|
+
...primaryWallet,
|
|
677
|
+
partyId: 'party::new',
|
|
678
|
+
primary: false,
|
|
679
|
+
};
|
|
680
|
+
walletAllocationMocks.createWallet.mockResolvedValue(newWallet);
|
|
681
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
682
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
683
|
+
const controller = createController(store, notificationService, logger, authWithEmail, walletKernelDriver);
|
|
684
|
+
const result = await controller.createWallet({
|
|
685
|
+
partyHint: 'alice',
|
|
686
|
+
signingProviderId: SigningProvider.WALLET_KERNEL,
|
|
687
|
+
primary: false,
|
|
688
|
+
});
|
|
689
|
+
expect(walletAllocationMocks.createWallet).toHaveBeenCalledWith(authWithEmail, 'alice', false, SigningProvider.WALLET_KERNEL, undefined);
|
|
690
|
+
expect(walletSyncMocks.syncWallets).toHaveBeenCalled();
|
|
691
|
+
expect(emitSpy).toHaveBeenCalledWith('accountsChanged', expect.any(Array));
|
|
692
|
+
expect(result.wallet).toEqual(newWallet);
|
|
693
|
+
});
|
|
694
|
+
it('passes auth context to wallet allocation service', async () => {
|
|
695
|
+
const authWithEmail = { ...auth, email: 'direct@example.com' };
|
|
696
|
+
const store = await createStore(logger, authWithEmail);
|
|
697
|
+
walletAllocationMocks.createWallet.mockResolvedValue(primaryWallet);
|
|
698
|
+
const controller = createController(store, notificationService, logger, authWithEmail, walletKernelDriver);
|
|
699
|
+
await controller.createWallet({
|
|
700
|
+
partyHint: 'bob',
|
|
701
|
+
signingProviderId: SigningProvider.WALLET_KERNEL,
|
|
702
|
+
});
|
|
703
|
+
expect(walletAllocationMocks.createWallet).toHaveBeenCalledWith(authWithEmail, 'bob', false, SigningProvider.WALLET_KERNEL, undefined);
|
|
704
|
+
});
|
|
705
|
+
it('throws when no current network is configured', async () => {
|
|
706
|
+
const store = await createStore(logger, auth);
|
|
707
|
+
// @ts-expect-error type doesn't allow empty return. override to validate guard clause
|
|
708
|
+
vi.spyOn(store, 'getCurrentNetwork').mockResolvedValue(undefined);
|
|
709
|
+
const controller = createController(store, notificationService, logger, auth, walletKernelDriver);
|
|
710
|
+
await expect(controller.createWallet({
|
|
711
|
+
partyHint: 'alice',
|
|
712
|
+
signingProviderId: SigningProvider.WALLET_KERNEL,
|
|
713
|
+
})).rejects.toThrow('No network session found');
|
|
714
|
+
expect(walletAllocationMocks.createWallet).not.toHaveBeenCalled();
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
describe('allocatePartyForWallet', () => {
|
|
718
|
+
const walletKernelDriver = {
|
|
719
|
+
[SigningProvider.WALLET_KERNEL]: { controller: vi.fn() },
|
|
720
|
+
};
|
|
721
|
+
it('allocates a party for an existing wallet', async () => {
|
|
722
|
+
const authWithEmail = { ...auth, email: 'user@example.com' };
|
|
723
|
+
const store = await createStore(logger, authWithEmail);
|
|
724
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
725
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
726
|
+
const controller = createController(store, notificationService, logger, authWithEmail, walletKernelDriver);
|
|
727
|
+
const result = await controller.allocatePartyForWallet({
|
|
728
|
+
partyId: primaryWallet.partyId,
|
|
729
|
+
});
|
|
730
|
+
expect(walletAllocationMocks.allocateParty).toHaveBeenCalledWith(authWithEmail, primaryWallet, SigningProvider.WALLET_KERNEL);
|
|
731
|
+
expect(walletSyncMocks.syncWallets).toHaveBeenCalled();
|
|
732
|
+
expect(emitSpy).toHaveBeenCalledWith('accountsChanged', expect.any(Array));
|
|
733
|
+
expect(result.wallet).toMatchObject({
|
|
734
|
+
partyId: primaryWallet.partyId,
|
|
735
|
+
networkId: storeNetwork.id,
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
it('throws when the wallet is not found', async () => {
|
|
739
|
+
const store = await createStore(logger, auth);
|
|
740
|
+
const controller = createController(store, notificationService, logger, auth, walletKernelDriver);
|
|
741
|
+
await expect(controller.allocatePartyForWallet({
|
|
742
|
+
partyId: 'party::missing',
|
|
743
|
+
})).rejects.toThrow('Wallet not found for party party::missing');
|
|
744
|
+
});
|
|
745
|
+
it('throws when the signing provider is not configured', async () => {
|
|
746
|
+
const store = await createStore(logger, auth);
|
|
747
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
748
|
+
await expect(controller.allocatePartyForWallet({
|
|
749
|
+
partyId: primaryWallet.partyId,
|
|
750
|
+
})).rejects.toThrow('Signing provider wallet-kernel not supported');
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
describe('syncWallets', () => {
|
|
754
|
+
it('syncs wallets and returns the result without emitting when nothing changed', async () => {
|
|
755
|
+
const store = await createStore(logger, auth);
|
|
756
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
757
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
758
|
+
const emptyResult = { added: [], updated: [], disabled: [] };
|
|
759
|
+
walletSyncMocks.syncWallets.mockResolvedValue(emptyResult);
|
|
760
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
761
|
+
const result = await controller.syncWallets();
|
|
762
|
+
expect(walletSyncMocks.syncWallets).toHaveBeenCalledOnce();
|
|
763
|
+
expect(result).toEqual(emptyResult);
|
|
764
|
+
expect(emitSpy).not.toHaveBeenCalledWith('accountsChanged', expect.anything());
|
|
765
|
+
});
|
|
766
|
+
it('throws when admin auth is not configured', async () => {
|
|
767
|
+
const networkWithoutAdmin = {
|
|
768
|
+
...storeNetwork,
|
|
769
|
+
adminAuth: undefined,
|
|
770
|
+
};
|
|
771
|
+
const store = new StoreInternal({ idps: [idp], networks: [networkWithoutAdmin] }, logger, auth);
|
|
772
|
+
await store.setSession(session);
|
|
773
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
774
|
+
await expect(controller.syncWallets()).rejects.toThrow('No admin auth configured');
|
|
775
|
+
expect(walletSyncMocks.syncWallets).not.toHaveBeenCalled();
|
|
776
|
+
});
|
|
777
|
+
it('throws when auth context is missing', async () => {
|
|
778
|
+
const store = await createStore(logger, auth, {
|
|
779
|
+
withSession: false,
|
|
780
|
+
});
|
|
781
|
+
const controller = createController(store, notificationService, logger, undefined);
|
|
782
|
+
await expect(controller.syncWallets()).rejects.toThrow();
|
|
783
|
+
expect(walletSyncMocks.syncWallets).not.toHaveBeenCalled();
|
|
784
|
+
});
|
|
785
|
+
it('emits accountsChanged when wallets are added and disabled during sync', async () => {
|
|
786
|
+
const store = await createStore(logger, auth);
|
|
787
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
788
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
789
|
+
const addedWallet = {
|
|
790
|
+
...primaryWallet,
|
|
791
|
+
partyId: 'party::added',
|
|
792
|
+
};
|
|
793
|
+
const disabledWallet = {
|
|
794
|
+
...primaryWallet,
|
|
795
|
+
partyId: 'party::disabled',
|
|
796
|
+
status: 'removed',
|
|
797
|
+
};
|
|
798
|
+
const syncResult = {
|
|
799
|
+
added: [addedWallet],
|
|
800
|
+
updated: [],
|
|
801
|
+
disabled: [disabledWallet],
|
|
802
|
+
};
|
|
803
|
+
walletSyncMocks.syncWallets.mockResolvedValue(syncResult);
|
|
804
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
805
|
+
const result = await controller.syncWallets();
|
|
806
|
+
expect(result).toEqual(syncResult);
|
|
807
|
+
expect(emitSpy).toHaveBeenCalledWith('accountsChanged', expect.arrayContaining([
|
|
808
|
+
expect.objectContaining({ partyId: primaryWallet.partyId }),
|
|
809
|
+
]));
|
|
810
|
+
});
|
|
811
|
+
it('does not emit accountsChanged when only wallets are added', async () => {
|
|
812
|
+
const store = await createStore(logger, auth);
|
|
813
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
814
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
815
|
+
walletSyncMocks.syncWallets.mockResolvedValue({
|
|
816
|
+
added: [{ ...primaryWallet, partyId: 'party::added' }],
|
|
817
|
+
updated: [],
|
|
818
|
+
disabled: [],
|
|
819
|
+
});
|
|
820
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
821
|
+
await controller.syncWallets();
|
|
822
|
+
expect(emitSpy).not.toHaveBeenCalledWith('accountsChanged', expect.anything());
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
describe('isWalletSyncNeeded', () => {
|
|
826
|
+
it('returns false when wallet sync is not needed', async () => {
|
|
827
|
+
const store = await createStore(logger, auth);
|
|
828
|
+
walletSyncMocks.isWalletSyncNeeded.mockResolvedValue(false);
|
|
829
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
830
|
+
const result = await controller.isWalletSyncNeeded();
|
|
831
|
+
expect(walletSyncMocks.isWalletSyncNeeded).toHaveBeenCalledOnce();
|
|
832
|
+
expect(result).toEqual({ walletSyncNeeded: false });
|
|
833
|
+
});
|
|
834
|
+
it('returns true when wallet sync is needed', async () => {
|
|
835
|
+
const store = await createStore(logger, auth);
|
|
836
|
+
walletSyncMocks.isWalletSyncNeeded.mockResolvedValue(true);
|
|
837
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
838
|
+
const result = await controller.isWalletSyncNeeded();
|
|
839
|
+
expect(walletSyncMocks.isWalletSyncNeeded).toHaveBeenCalledOnce();
|
|
840
|
+
expect(result).toEqual({ walletSyncNeeded: true });
|
|
841
|
+
});
|
|
842
|
+
it('throws when admin auth is not configured', async () => {
|
|
843
|
+
const networkWithoutAdmin = {
|
|
844
|
+
...storeNetwork,
|
|
845
|
+
adminAuth: undefined,
|
|
846
|
+
};
|
|
847
|
+
const store = new StoreInternal({ idps: [idp], networks: [networkWithoutAdmin] }, logger, auth);
|
|
848
|
+
await store.setSession(session);
|
|
849
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
850
|
+
await expect(controller.isWalletSyncNeeded()).rejects.toThrow('No admin auth configured');
|
|
851
|
+
expect(walletSyncMocks.isWalletSyncNeeded).not.toHaveBeenCalled();
|
|
852
|
+
});
|
|
853
|
+
it('throws when auth context is missing', async () => {
|
|
854
|
+
const store = await createStore(logger, auth, {
|
|
855
|
+
withSession: false,
|
|
856
|
+
});
|
|
857
|
+
const controller = createController(store, notificationService, logger, undefined);
|
|
858
|
+
await expect(controller.isWalletSyncNeeded()).rejects.toThrow();
|
|
859
|
+
expect(walletSyncMocks.isWalletSyncNeeded).not.toHaveBeenCalled();
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
});
|