@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.
Files changed (141) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/jwt-auth-service.d.ts.map +1 -1
  3. package/dist/auth/jwt-auth-service.js +6 -5
  4. package/dist/auth/jwt-auth-service.test.d.ts +2 -0
  5. package/dist/auth/jwt-auth-service.test.d.ts.map +1 -0
  6. package/dist/auth/jwt-auth-service.test.js +259 -0
  7. package/dist/auth/jwt-unsafe-auth-service.test.d.ts +2 -0
  8. package/dist/auth/jwt-unsafe-auth-service.test.d.ts.map +1 -0
  9. package/dist/auth/jwt-unsafe-auth-service.test.js +120 -0
  10. package/dist/config/Config.d.ts +64 -8
  11. package/dist/config/Config.d.ts.map +1 -1
  12. package/dist/config/Config.js +1 -0
  13. package/dist/config/ConfigUtils.d.ts.map +1 -1
  14. package/dist/config/ConfigUtils.js +3 -0
  15. package/dist/dapp-api/controller.d.ts +1 -0
  16. package/dist/dapp-api/controller.d.ts.map +1 -1
  17. package/dist/dapp-api/controller.js +87 -26
  18. package/dist/dapp-api/controller.test.d.ts +2 -0
  19. package/dist/dapp-api/controller.test.d.ts.map +1 -0
  20. package/dist/dapp-api/controller.test.js +516 -0
  21. package/dist/dapp-api/rpc-gen/index.d.ts +3 -0
  22. package/dist/dapp-api/rpc-gen/index.d.ts.map +1 -1
  23. package/dist/dapp-api/rpc-gen/index.js +1 -0
  24. package/dist/dapp-api/rpc-gen/typings.d.ts +99 -14
  25. package/dist/dapp-api/rpc-gen/typings.d.ts.map +1 -1
  26. package/dist/dapp-api/server.d.ts.map +1 -1
  27. package/dist/dapp-api/server.js +5 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +10 -0
  30. package/dist/init.d.ts.map +1 -1
  31. package/dist/init.js +4 -6
  32. package/dist/ledger/transaction-service.d.ts +14 -11
  33. package/dist/ledger/transaction-service.d.ts.map +1 -1
  34. package/dist/ledger/transaction-service.js +95 -8
  35. package/dist/ledger/transaction-service.test.d.ts +2 -0
  36. package/dist/ledger/transaction-service.test.d.ts.map +1 -0
  37. package/dist/ledger/transaction-service.test.js +356 -0
  38. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.d.ts +5 -2
  39. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.d.ts.map +1 -1
  40. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.js +7 -6
  41. package/dist/ledger/wallet-allocation/wallet-allocation-service.d.ts +11 -5
  42. package/dist/ledger/wallet-allocation/wallet-allocation-service.d.ts.map +1 -1
  43. package/dist/ledger/wallet-allocation/wallet-allocation-service.js +28 -14
  44. package/dist/ledger/wallet-allocation/wallet-allocation-service.test.js +550 -19
  45. package/dist/ledger/wallet-sync-service.js +3 -3
  46. package/dist/middleware/apiKeyAuth.d.ts +6 -0
  47. package/dist/middleware/apiKeyAuth.d.ts.map +1 -0
  48. package/dist/middleware/apiKeyAuth.js +88 -0
  49. package/dist/middleware/apiKeyAuth.test.d.ts +2 -0
  50. package/dist/middleware/apiKeyAuth.test.d.ts.map +1 -0
  51. package/dist/middleware/apiKeyAuth.test.js +126 -0
  52. package/dist/middleware/jsonRpcHandler.d.ts +11 -1
  53. package/dist/middleware/jsonRpcHandler.d.ts.map +1 -1
  54. package/dist/middleware/jsonRpcHandler.js +14 -15
  55. package/dist/middleware/jsonRpcHandler.test.d.ts +2 -0
  56. package/dist/middleware/jsonRpcHandler.test.d.ts.map +1 -0
  57. package/dist/middleware/jsonRpcHandler.test.js +254 -0
  58. package/dist/middleware/jwtAuth.d.ts.map +1 -1
  59. package/dist/middleware/jwtAuth.js +4 -0
  60. package/dist/middleware/jwtAuth.test.d.ts +2 -0
  61. package/dist/middleware/jwtAuth.test.d.ts.map +1 -0
  62. package/dist/middleware/jwtAuth.test.js +93 -0
  63. package/dist/middleware/rateLimit.js +2 -2
  64. package/dist/middleware/rateLimit.test.js +26 -0
  65. package/dist/middleware/sessionHandler.js +1 -1
  66. package/dist/middleware/sessionHandler.test.d.ts +2 -0
  67. package/dist/middleware/sessionHandler.test.d.ts.map +1 -0
  68. package/dist/middleware/sessionHandler.test.js +121 -0
  69. package/dist/notification/NotificationService.test.d.ts +2 -0
  70. package/dist/notification/NotificationService.test.d.ts.map +1 -0
  71. package/dist/notification/NotificationService.test.js +110 -0
  72. package/dist/signing/signing-drivers.d.ts +7 -0
  73. package/dist/signing/signing-drivers.d.ts.map +1 -0
  74. package/dist/signing/signing-drivers.js +3 -0
  75. package/dist/user-api/controller.d.ts +12 -4
  76. package/dist/user-api/controller.d.ts.map +1 -1
  77. package/dist/user-api/controller.js +330 -95
  78. package/dist/user-api/controller.test.d.ts +2 -0
  79. package/dist/user-api/controller.test.d.ts.map +1 -0
  80. package/dist/user-api/controller.test.js +862 -0
  81. package/dist/user-api/rpc-gen/index.d.ts +30 -0
  82. package/dist/user-api/rpc-gen/index.d.ts.map +1 -1
  83. package/dist/user-api/rpc-gen/index.js +10 -0
  84. package/dist/user-api/rpc-gen/typings.d.ts +170 -25
  85. package/dist/user-api/rpc-gen/typings.d.ts.map +1 -1
  86. package/dist/user-api/server.test.js +53 -0
  87. package/dist/utils.d.ts +19 -5
  88. package/dist/utils.d.ts.map +1 -1
  89. package/dist/utils.js +53 -2
  90. package/dist/utils.test.d.ts +2 -0
  91. package/dist/utils.test.d.ts.map +1 -0
  92. package/dist/utils.test.js +24 -0
  93. package/dist/web/frontend/404/index.html +2 -2
  94. package/dist/web/frontend/activities/index.html +3 -3
  95. package/dist/web/frontend/approve/index.html +4 -5
  96. package/dist/web/frontend/assets/404-MVLWUPwN.js +5 -0
  97. package/dist/web/frontend/assets/activities-DaWkmGqA.js +61 -0
  98. package/dist/web/frontend/assets/addIdentityProvider--rBb0ihb.js +28 -0
  99. package/dist/web/frontend/assets/addNetwork-C6UDsRkU.js +38 -0
  100. package/dist/web/frontend/assets/addParty-B3fFD1eM.js +45 -0
  101. package/dist/web/frontend/assets/approve-WbGL4E82.js +21 -0
  102. package/dist/web/frontend/assets/callback--tihEryB.js +1 -0
  103. package/dist/web/frontend/assets/identityProviders-Dra4qG6t.js +71 -0
  104. package/dist/web/frontend/assets/index-Bm76pXA_.js +91 -0
  105. package/dist/web/frontend/assets/index-D1dC4Cl9.js +4477 -0
  106. package/dist/web/frontend/assets/login-BC8a-kD_.js +10 -0
  107. package/dist/web/frontend/assets/networks-B3ztsogL.js +73 -0
  108. package/dist/web/frontend/assets/reviewIdentityProvider-Co420Dbk.js +43 -0
  109. package/dist/web/frontend/assets/reviewNetwork-B-Sh8pCH.js +43 -0
  110. package/dist/web/frontend/assets/{settings-B1ga2TK0.js → settings-DvyUjqLl.js} +1 -1
  111. package/dist/web/frontend/assets/state-Bq65GwdF.js +1 -0
  112. package/dist/web/frontend/assets/{utils-CT9Hzi7v.js → utils-B2iziW8V.js} +1 -1
  113. package/dist/web/frontend/callback/index.html +2 -2
  114. package/dist/web/frontend/identity-providers/add/index.html +3 -3
  115. package/dist/web/frontend/identity-providers/index.html +3 -3
  116. package/dist/web/frontend/identity-providers/review/index.html +3 -3
  117. package/dist/web/frontend/index.html +1 -1
  118. package/dist/web/frontend/login/index.html +3 -4
  119. package/dist/web/frontend/networks/add/index.html +3 -3
  120. package/dist/web/frontend/networks/index.html +3 -3
  121. package/dist/web/frontend/networks/review/index.html +3 -3
  122. package/dist/web/frontend/parties/add/index.html +5 -5
  123. package/dist/web/frontend/parties/index.html +4 -4
  124. package/dist/web/frontend/settings/index.html +3 -3
  125. package/package.json +40 -33
  126. package/dist/web/frontend/assets/404-B-o9ppJB.js +0 -5
  127. package/dist/web/frontend/assets/activities-CGjCIjUH.js +0 -61
  128. package/dist/web/frontend/assets/addIdentityProvider-CR4Wm9Tl.js +0 -28
  129. package/dist/web/frontend/assets/addNetwork-Dx0-SN4j.js +0 -38
  130. package/dist/web/frontend/assets/addParty-COhk_rFn.js +0 -41
  131. package/dist/web/frontend/assets/approve-B2w66l0J.js +0 -21
  132. package/dist/web/frontend/assets/callback-BTVon_yQ.js +0 -1
  133. package/dist/web/frontend/assets/identityProviders-CK8zSrd3.js +0 -71
  134. package/dist/web/frontend/assets/index-CF4BKzgl.js +0 -91
  135. package/dist/web/frontend/assets/index-DWz_3f3y.js +0 -1
  136. package/dist/web/frontend/assets/index-_pMHlJoE.js +0 -4042
  137. package/dist/web/frontend/assets/login-CVoPNVDw.js +0 -10
  138. package/dist/web/frontend/assets/networks-D1nPvUzM.js +0 -73
  139. package/dist/web/frontend/assets/reviewIdentityProvider-CNSf2qQv.js +0 -43
  140. package/dist/web/frontend/assets/reviewNetwork-DGLK-Ume.js +0 -43
  141. package/dist/web/frontend/assets/state-B2k3ak7d.js +0 -1
@@ -24,8 +24,14 @@ const createAllocatedParty = (partyId, hint, namespace) => ({
24
24
  hint,
25
25
  namespace,
26
26
  });
27
+ const authContext = {
28
+ userId: 'user-1',
29
+ accessToken: 'access-token',
30
+ email: 'user-1@example.com',
31
+ };
32
+ const FIREBLOCKS_VAULT_NAME = 'Canton Party';
27
33
  function createFireblocksDriver(options) {
28
- const getKeysResult = {
34
+ const getKeysResult = options.getKeysResult ?? {
29
35
  keys: [{ id: 'key-1', name: 'Canton Party', publicKey: 'fb-pk' }],
30
36
  };
31
37
  const signTransactionResult = options.signTransactionResult ?? {
@@ -48,6 +54,9 @@ function createFireblocksDriver(options) {
48
54
  };
49
55
  }
50
56
  function createBlockdaemonDriver(options) {
57
+ const createKeyResult = options.createKeyResult ?? {
58
+ publicKey: 'bd-pk',
59
+ };
51
60
  const signTransactionResult = options.signTransactionResult ?? {
52
61
  status: 'pending',
53
62
  txId: 'tx-1',
@@ -57,9 +66,31 @@ function createBlockdaemonDriver(options) {
57
66
  controller: vi.fn().mockReturnValue({
58
67
  createKey: vi
59
68
  .fn()
60
- .mockResolvedValue({
61
- publicKey: 'bd-pk',
62
- }),
69
+ .mockResolvedValue(createKeyResult),
70
+ signTransaction: vi
71
+ .fn()
72
+ .mockResolvedValue(signTransactionResult),
73
+ getTransaction: vi
74
+ .fn()
75
+ .mockResolvedValue(getTransactionResult),
76
+ }),
77
+ };
78
+ }
79
+ function createDfnsDriver(options) {
80
+ const createKeyResult = options.createKeyResult ?? {
81
+ id: 'key-1',
82
+ publicKey: 'dfns-pk',
83
+ };
84
+ const signTransactionResult = options.signTransactionResult ?? {
85
+ status: 'pending',
86
+ txId: 'tx-1',
87
+ };
88
+ const getTransactionResult = options.getTransactionResult ?? signTransactionResult;
89
+ return {
90
+ controller: vi.fn().mockReturnValue({
91
+ createKey: vi
92
+ .fn()
93
+ .mockResolvedValue(createKeyResult),
63
94
  signTransaction: vi
64
95
  .fn()
65
96
  .mockResolvedValue(signTransactionResult),
@@ -129,8 +160,8 @@ describe('WalletAllocationService', () => {
129
160
  it('createWallet allocates new party and adds wallet', async () => {
130
161
  const expectedParty = createAllocatedParty('alice::participant1', 'alice', 'participant1');
131
162
  mockPartyAllocator.allocateParty.mockResolvedValue(expectedParty);
132
- const result = await service.createWallet('user-1', undefined, 'alice', false, SigningProvider.PARTICIPANT);
133
- expect(mockPartyAllocator.allocateParty).toHaveBeenCalledWith('user-1', 'alice');
163
+ const result = await service.createWallet(authContext, 'alice', false, SigningProvider.PARTICIPANT);
164
+ expect(mockPartyAllocator.allocateParty).toHaveBeenCalledWith(authContext.userId, 'alice');
134
165
  expect(mockStore.addWallet).toHaveBeenCalledWith(expect.objectContaining({
135
166
  partyId: 'alice::participant1',
136
167
  status: 'allocated',
@@ -147,8 +178,8 @@ describe('WalletAllocationService', () => {
147
178
  namespace: 'namespace',
148
179
  signingProviderId: SigningProvider.PARTICIPANT,
149
180
  });
150
- await service.allocateParty('user-1', undefined, existingWallet, SigningProvider.PARTICIPANT);
151
- expect(mockPartyAllocator.allocateParty).toHaveBeenCalledWith('user-1', 'alice');
181
+ await service.allocateParty(authContext, existingWallet, SigningProvider.PARTICIPANT);
182
+ expect(mockPartyAllocator.allocateParty).toHaveBeenCalledWith(authContext.userId, 'alice');
152
183
  expect(mockStore.updateWallet).toHaveBeenCalledWith({
153
184
  partyId: 'alice::participant1',
154
185
  networkId: 'network1',
@@ -161,7 +192,7 @@ describe('WalletAllocationService', () => {
161
192
  const expectedParty = createAllocatedParty('bob::fingerprint', 'bob', 'fingerprint');
162
193
  mockPartyAllocator.createFingerprintFromKey.mockReturnValue('fingerprint');
163
194
  mockPartyAllocator.allocateParty.mockResolvedValue(expectedParty);
164
- const result = await service.createWallet('user-1', undefined, 'bob', false, SigningProvider.WALLET_KERNEL);
195
+ const result = await service.createWallet(authContext, 'bob', false, SigningProvider.WALLET_KERNEL);
165
196
  expect(mockStore.addWallet).toHaveBeenCalledWith(expect.objectContaining({
166
197
  partyId: 'bob::fingerprint',
167
198
  status: 'allocated',
@@ -169,7 +200,7 @@ describe('WalletAllocationService', () => {
169
200
  }));
170
201
  expect(result.partyId).toBe('bob::fingerprint');
171
202
  expect(result.publicKey).toBe('new-public-key');
172
- expect(mockWalletKernelDriver.controller).toHaveBeenCalledWith('user-1');
203
+ expect(mockWalletKernelDriver.controller).toHaveBeenCalledWith(authContext.userId);
173
204
  expect(mockController.createKey).toHaveBeenCalledWith({
174
205
  name: 'bob',
175
206
  });
@@ -187,7 +218,7 @@ describe('WalletAllocationService', () => {
187
218
  publicKey: 'existing-public-key',
188
219
  signingProviderId: SigningProvider.WALLET_KERNEL,
189
220
  });
190
- await service.allocateParty('user-1', undefined, existingWallet, SigningProvider.WALLET_KERNEL);
221
+ await service.allocateParty(authContext, existingWallet, SigningProvider.WALLET_KERNEL);
191
222
  expect(mockController.signTransaction).toHaveBeenCalledWith(expect.objectContaining({
192
223
  txHash: 'test-hash',
193
224
  keyIdentifier: { publicKey: 'existing-public-key' },
@@ -200,13 +231,56 @@ describe('WalletAllocationService', () => {
200
231
  });
201
232
  it('throws when Wallet Gateway signing driver not available', async () => {
202
233
  const serviceWithoutDriver = createService({});
203
- await expect(serviceWithoutDriver.createWallet('user-1', undefined, 'bob', false, SigningProvider.WALLET_KERNEL)).rejects.toThrow('Wallet Gateway signing driver not available');
234
+ await expect(serviceWithoutDriver.createWallet(authContext, 'bob', false, SigningProvider.WALLET_KERNEL)).rejects.toThrow('Wallet Gateway signing driver not available');
235
+ });
236
+ it('throws when createKey returns a signing error', async () => {
237
+ mockController.createKey.mockResolvedValue({
238
+ error: 'denied',
239
+ error_description: 'Key creation failed',
240
+ });
241
+ await expect(service.createWallet(authContext, 'bob', false, SigningProvider.WALLET_KERNEL)).rejects.toThrow('Error from signing driver: Key creation failed');
242
+ });
243
+ it('throws when signing the topology hash returns a driver error', async () => {
244
+ mockPartyAllocator.allocateParty.mockImplementation(async (_userId, _hint, _publicKey, signingCallback) => {
245
+ await signingCallback?.('hash');
246
+ return createAllocatedParty('bob::fingerprint', 'bob', 'fingerprint');
247
+ });
248
+ mockController.signTransaction.mockResolvedValue({
249
+ error: 'denied',
250
+ error_description: 'Signing failed',
251
+ });
252
+ await expect(service.createWallet(authContext, 'bob', false, SigningProvider.WALLET_KERNEL)).rejects.toThrow('Error from signing driver: Signing failed');
253
+ });
254
+ it('throws when signing the topology hash returns no signature', async () => {
255
+ mockPartyAllocator.allocateParty.mockImplementation(async (_userId, _hint, _publicKey, signingCallback) => {
256
+ await signingCallback?.('hash');
257
+ return createAllocatedParty('bob::fingerprint', 'bob', 'fingerprint');
258
+ });
259
+ mockController.signTransaction.mockResolvedValue({
260
+ signature: undefined,
261
+ });
262
+ await expect(service.createWallet(authContext, 'bob', false, SigningProvider.WALLET_KERNEL)).rejects.toThrow('No signature returned from signing driver');
263
+ });
264
+ it('throws when allocateParty signing callback returns no signature', async () => {
265
+ mockPartyAllocator.allocateParty.mockImplementation(async (_userId, _hint, _publicKey, signingCallback) => {
266
+ await signingCallback?.('hash');
267
+ return createAllocatedParty('bob::fingerprint', 'bob', 'fingerprint');
268
+ });
269
+ mockController.signTransaction.mockResolvedValue({
270
+ signature: undefined,
271
+ });
272
+ const existingWallet = createWallet('bob::namespace', {
273
+ hint: 'bob',
274
+ publicKey: 'existing-public-key',
275
+ signingProviderId: SigningProvider.WALLET_KERNEL,
276
+ });
277
+ await expect(service.allocateParty(authContext, existingWallet, SigningProvider.WALLET_KERNEL)).rejects.toThrow('No signature returned from signing driver');
204
278
  });
205
279
  });
206
280
  describe('Fireblocks', () => {
207
281
  it('throws when Fireblocks signing driver not available', async () => {
208
282
  const serviceWithoutFireblocks = createService({});
209
- await expect(serviceWithoutFireblocks.createWallet('user-1', undefined, 'alice', false, SigningProvider.FIREBLOCKS)).rejects.toThrow('Fireblocks signing driver not available');
283
+ await expect(serviceWithoutFireblocks.createWallet(authContext, 'alice', false, SigningProvider.FIREBLOCKS)).rejects.toThrow('Fireblocks signing driver not available');
210
284
  });
211
285
  it('createWallet returns allocated when signTransaction returns signed', async () => {
212
286
  const serviceWithFireblocks = createService({
@@ -220,7 +294,7 @@ describe('WalletAllocationService', () => {
220
294
  }),
221
295
  });
222
296
  mockPartyAllocator.allocatePartyWithExistingWallet.mockResolvedValue('alice::namespace');
223
- const result = await serviceWithFireblocks.createWallet('user-1', undefined, 'alice', false, SigningProvider.FIREBLOCKS);
297
+ const result = await serviceWithFireblocks.createWallet(authContext, 'alice', false, SigningProvider.FIREBLOCKS, FIREBLOCKS_VAULT_NAME);
224
298
  expect(result.status).toBe('allocated');
225
299
  expect(result.partyId).toBe('alice::namespace');
226
300
  expect(mockPartyAllocator.allocatePartyWithExistingWallet).toHaveBeenCalled();
@@ -236,7 +310,7 @@ describe('WalletAllocationService', () => {
236
310
  },
237
311
  }),
238
312
  });
239
- const result = await serviceWithFireblocks.createWallet('user-1', undefined, 'alice', false, SigningProvider.FIREBLOCKS);
313
+ const result = await serviceWithFireblocks.createWallet(authContext, 'alice', false, SigningProvider.FIREBLOCKS, FIREBLOCKS_VAULT_NAME);
240
314
  expect(result.status).toBe('initialized');
241
315
  expect(mockPartyAllocator.allocatePartyWithExistingWallet).not.toHaveBeenCalled();
242
316
  expect(mockStore.addWallet).toHaveBeenCalled();
@@ -254,17 +328,168 @@ describe('WalletAllocationService', () => {
254
328
  },
255
329
  }),
256
330
  });
257
- const result = await serviceWithFireblocks.createWallet('user-1', undefined, 'alice', false, SigningProvider.FIREBLOCKS);
331
+ const result = await serviceWithFireblocks.createWallet(authContext, 'alice', false, SigningProvider.FIREBLOCKS, FIREBLOCKS_VAULT_NAME);
258
332
  expect(result.status).toBe('removed');
259
333
  expect(result.reason).toBe(expectedReason);
260
334
  expect(mockPartyAllocator.allocatePartyWithExistingWallet).not.toHaveBeenCalled();
261
335
  expect(mockStore.addWallet).toHaveBeenCalled();
262
336
  });
337
+ it('createWallet handles missing topology transactions from party allocator', async () => {
338
+ mockPartyAllocator.generateTopologyTransactions.mockResolvedValue({
339
+ topologyTransactions: undefined,
340
+ multiHash: 'multi-hash',
341
+ });
342
+ const serviceWithFireblocks = createService({
343
+ [SigningProvider.FIREBLOCKS]: createFireblocksDriver({
344
+ signTransactionResult: { status: 'pending', txId: 'tx-1' },
345
+ }),
346
+ });
347
+ const result = await serviceWithFireblocks.createWallet(authContext, 'alice', false, SigningProvider.FIREBLOCKS, FIREBLOCKS_VAULT_NAME);
348
+ expect(result.topologyTransactions).toBe('');
349
+ });
350
+ it('throws when getKeys returns a signing error', async () => {
351
+ const serviceWithFireblocks = createService({
352
+ [SigningProvider.FIREBLOCKS]: createFireblocksDriver({
353
+ getKeysResult: {
354
+ error: 'denied',
355
+ error_description: 'Keys unavailable',
356
+ },
357
+ }),
358
+ });
359
+ await expect(serviceWithFireblocks.createWallet(authContext, 'alice', false, SigningProvider.FIREBLOCKS, FIREBLOCKS_VAULT_NAME)).rejects.toThrow('Error from signing driver: Keys unavailable');
360
+ });
361
+ it('throws when vaultName is missing for Fireblocks', async () => {
362
+ const serviceWithFireblocks = createService({
363
+ [SigningProvider.FIREBLOCKS]: createFireblocksDriver({}),
364
+ });
365
+ await expect(serviceWithFireblocks.createWallet(authContext, 'alice', false, SigningProvider.FIREBLOCKS)).rejects.toThrow('vaultName is required for creating a wallet with Fireblocks');
366
+ });
367
+ it('throws when the selected Fireblocks vault is missing', async () => {
368
+ const serviceWithFireblocks = createService({
369
+ [SigningProvider.FIREBLOCKS]: createFireblocksDriver({
370
+ getKeysResult: { keys: [] },
371
+ }),
372
+ });
373
+ await expect(serviceWithFireblocks.createWallet(authContext, 'alice', false, SigningProvider.FIREBLOCKS, 'missing-vault')).rejects.toThrow('Fireblocks key not found');
374
+ });
375
+ it('getVaults returns vault names for Fireblocks', async () => {
376
+ const serviceWithFireblocks = createService({
377
+ [SigningProvider.FIREBLOCKS]: createFireblocksDriver({
378
+ getKeysResult: {
379
+ keys: [
380
+ {
381
+ id: 'key-1',
382
+ name: 'Vault A',
383
+ publicKey: 'fb-pk-a',
384
+ },
385
+ {
386
+ id: 'key-2',
387
+ name: 'Vault B',
388
+ publicKey: 'fb-pk-b',
389
+ },
390
+ ],
391
+ },
392
+ }),
393
+ });
394
+ const result = await serviceWithFireblocks.getVaults(authContext, SigningProvider.FIREBLOCKS);
395
+ expect(result).toEqual({ vaults: ['Vault A', 'Vault B'] });
396
+ });
397
+ it('throws when listing vaults for an unsupported signing provider', async () => {
398
+ const serviceWithFireblocks = createService({
399
+ [SigningProvider.FIREBLOCKS]: createFireblocksDriver({}),
400
+ });
401
+ await expect(serviceWithFireblocks.getVaults(authContext, SigningProvider.PARTICIPANT)).rejects.toThrow('Signing provider participant does not support listing vaults');
402
+ });
403
+ it('throws when a signed createWallet has no signature in getTransaction', async () => {
404
+ const serviceWithFireblocks = createService({
405
+ [SigningProvider.FIREBLOCKS]: createFireblocksDriver({
406
+ signTransactionResult: { status: 'signed', txId: 'tx-1' },
407
+ getTransactionResult: {
408
+ txId: 'tx-1',
409
+ status: 'signed',
410
+ },
411
+ }),
412
+ });
413
+ await expect(serviceWithFireblocks.createWallet(authContext, 'alice', false, SigningProvider.FIREBLOCKS, FIREBLOCKS_VAULT_NAME)).rejects.toThrow('Transaction signed but no signature found in result');
414
+ });
415
+ it('throws when allocateParty wallet is missing topology metadata', async () => {
416
+ const serviceWithFireblocks = createService({
417
+ [SigningProvider.FIREBLOCKS]: createFireblocksDriver({}),
418
+ });
419
+ await expect(serviceWithFireblocks.allocateParty(authContext, createWallet('alice::fingerprint', {
420
+ signingProviderId: SigningProvider.FIREBLOCKS,
421
+ topologyTransactions: undefined,
422
+ externalTxId: 'tx-1',
423
+ }), SigningProvider.FIREBLOCKS)).rejects.toThrow('Existing wallet is missing field externalTxId or topologyTransactions');
424
+ });
425
+ it('allocateParty updates wallet to pending state', async () => {
426
+ const serviceWithFireblocks = createService({
427
+ [SigningProvider.FIREBLOCKS]: createFireblocksDriver({
428
+ getTransactionResult: { txId: 'tx-1', status: 'pending' },
429
+ }),
430
+ });
431
+ await serviceWithFireblocks.allocateParty(authContext, createWallet('alice::fingerprint', {
432
+ signingProviderId: SigningProvider.FIREBLOCKS,
433
+ topologyTransactions: 'tx1',
434
+ externalTxId: 'tx-1',
435
+ }), SigningProvider.FIREBLOCKS);
436
+ expect(mockStore.updateWallet).toHaveBeenCalledWith({
437
+ partyId: 'alice::fingerprint',
438
+ networkId: 'network1',
439
+ status: 'initialized',
440
+ reason: WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_PENDING,
441
+ });
442
+ });
443
+ it('allocateParty updates wallet to allocated when transaction is signed', async () => {
444
+ const hexSignature = 'deadbeef';
445
+ const serviceWithFireblocks = createService({
446
+ [SigningProvider.FIREBLOCKS]: createFireblocksDriver({
447
+ getTransactionResult: {
448
+ txId: 'tx-1',
449
+ status: 'signed',
450
+ signature: hexSignature,
451
+ },
452
+ }),
453
+ });
454
+ mockPartyAllocator.allocatePartyWithExistingWallet.mockResolvedValue('alice::namespace');
455
+ await serviceWithFireblocks.allocateParty(authContext, createWallet('alice::fingerprint', {
456
+ signingProviderId: SigningProvider.FIREBLOCKS,
457
+ namespace: 'fingerprint',
458
+ topologyTransactions: 'tx1',
459
+ externalTxId: 'tx-1',
460
+ }), SigningProvider.FIREBLOCKS);
461
+ expect(mockPartyAllocator.allocatePartyWithExistingWallet).toHaveBeenCalledWith('fingerprint', ['tx1'], Buffer.from(hexSignature, 'hex').toString('base64'), 'user-1');
462
+ expect(mockStore.updateWallet).toHaveBeenCalledWith({
463
+ networkId: 'network1',
464
+ partyId: 'alice::namespace',
465
+ status: 'allocated',
466
+ reason: '',
467
+ });
468
+ });
469
+ it('allocateParty disables wallet when transaction is rejected', async () => {
470
+ const serviceWithFireblocks = createService({
471
+ [SigningProvider.FIREBLOCKS]: createFireblocksDriver({
472
+ getTransactionResult: { txId: 'tx-1', status: 'rejected' },
473
+ }),
474
+ });
475
+ await serviceWithFireblocks.allocateParty(authContext, createWallet('alice::fingerprint', {
476
+ signingProviderId: SigningProvider.FIREBLOCKS,
477
+ topologyTransactions: 'tx1',
478
+ externalTxId: 'tx-1',
479
+ }), SigningProvider.FIREBLOCKS);
480
+ expect(mockStore.updateWallet).toHaveBeenCalledWith({
481
+ partyId: 'alice::fingerprint',
482
+ networkId: 'network1',
483
+ status: 'removed',
484
+ disabled: true,
485
+ reason: WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_REJECTED,
486
+ });
487
+ });
263
488
  });
264
489
  describe('Blockdaemon', () => {
265
490
  it('throws when Blockdaemon signing driver not available', async () => {
266
491
  const serviceWithoutBlockdaemon = createService({});
267
- await expect(serviceWithoutBlockdaemon.createWallet('user-1', 'user-1@example.com', 'alice', false, SigningProvider.BLOCKDAEMON)).rejects.toThrow('Blockdaemon signing driver not available');
492
+ await expect(serviceWithoutBlockdaemon.createWallet(authContext, 'alice', false, SigningProvider.BLOCKDAEMON)).rejects.toThrow('Blockdaemon signing driver not available');
268
493
  });
269
494
  it('createWallet returns initialized when signTransaction returns pending', async () => {
270
495
  const serviceWithBlockdaemon = createService({
@@ -272,7 +497,7 @@ describe('WalletAllocationService', () => {
272
497
  signTransactionResult: { status: 'pending', txId: 'tx-1' },
273
498
  }),
274
499
  });
275
- const result = await serviceWithBlockdaemon.createWallet('user-1', 'user-1@example.com', 'alice', false, SigningProvider.BLOCKDAEMON);
500
+ const result = await serviceWithBlockdaemon.createWallet(authContext, 'alice', false, SigningProvider.BLOCKDAEMON);
276
501
  expect(result.status).toBe('initialized');
277
502
  expect(result.reason).toBe(WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_PENDING);
278
503
  expect(result.externalTxId).toBe('tx-1');
@@ -292,11 +517,317 @@ describe('WalletAllocationService', () => {
292
517
  }),
293
518
  });
294
519
  mockPartyAllocator.allocatePartyWithExistingWallet.mockResolvedValue('alice::namespace');
295
- const result = await serviceWithBlockdaemon.createWallet('user-1', 'user-1@example.com', 'alice', false, SigningProvider.BLOCKDAEMON);
520
+ const result = await serviceWithBlockdaemon.createWallet(authContext, 'alice', false, SigningProvider.BLOCKDAEMON);
521
+ expect(result.status).toBe('allocated');
522
+ expect(result.partyId).toBe('alice::namespace');
523
+ expect(mockPartyAllocator.allocatePartyWithExistingWallet).toHaveBeenCalled();
524
+ expect(mockStore.addWallet).toHaveBeenCalled();
525
+ });
526
+ it('throws when createKey returns an error object', async () => {
527
+ const serviceWithBlockdaemon = createService({
528
+ [SigningProvider.BLOCKDAEMON]: createBlockdaemonDriver({
529
+ createKeyResult: {
530
+ error: 'denied',
531
+ error_description: 'Cannot create key',
532
+ },
533
+ }),
534
+ });
535
+ await expect(serviceWithBlockdaemon.createWallet(authContext, 'alice', false, SigningProvider.BLOCKDAEMON)).rejects.toThrow('Failed to create key: Cannot create key');
536
+ });
537
+ it('throws when a signed createWallet has no signature in getTransaction', async () => {
538
+ const serviceWithBlockdaemon = createService({
539
+ [SigningProvider.BLOCKDAEMON]: createBlockdaemonDriver({
540
+ signTransactionResult: { status: 'signed', txId: 'tx-1' },
541
+ getTransactionResult: {
542
+ txId: 'tx-1',
543
+ status: 'signed',
544
+ },
545
+ }),
546
+ });
547
+ await expect(serviceWithBlockdaemon.createWallet(authContext, 'alice', false, SigningProvider.BLOCKDAEMON)).rejects.toThrow('Transaction signed but no signature found in result');
548
+ });
549
+ it.each([
550
+ ['failed', WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_FAILED],
551
+ ['rejected', WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_REJECTED],
552
+ ])('createWallet marks wallet removed when signTransaction returns %s', async (status, expectedReason) => {
553
+ const serviceWithBlockdaemon = createService({
554
+ [SigningProvider.BLOCKDAEMON]: createBlockdaemonDriver({
555
+ signTransactionResult: { status, txId: 'tx-1' },
556
+ }),
557
+ });
558
+ const result = await serviceWithBlockdaemon.createWallet(authContext, 'alice', false, SigningProvider.BLOCKDAEMON);
559
+ expect(result.status).toBe('removed');
560
+ expect(result.disabled).toBe(true);
561
+ expect(result.reason).toBe(expectedReason);
562
+ });
563
+ it('throws when allocateParty wallet is missing topology metadata', async () => {
564
+ const serviceWithBlockdaemon = createService({
565
+ [SigningProvider.BLOCKDAEMON]: createBlockdaemonDriver({}),
566
+ });
567
+ await expect(serviceWithBlockdaemon.allocateParty(authContext, createWallet('alice::fingerprint', {
568
+ signingProviderId: SigningProvider.BLOCKDAEMON,
569
+ topologyTransactions: 'tx1',
570
+ externalTxId: undefined,
571
+ }), SigningProvider.BLOCKDAEMON)).rejects.toThrow('Existing wallet is missing field externalTxId or topologyTransactions');
572
+ });
573
+ it('allocateParty updates wallet to allocated when transaction is signed', async () => {
574
+ const serviceWithBlockdaemon = createService({
575
+ [SigningProvider.BLOCKDAEMON]: createBlockdaemonDriver({
576
+ getTransactionResult: {
577
+ txId: 'tx-1',
578
+ status: 'signed',
579
+ signature: 'sig-base64',
580
+ },
581
+ }),
582
+ });
583
+ mockPartyAllocator.allocatePartyWithExistingWallet.mockResolvedValue('alice::namespace');
584
+ await serviceWithBlockdaemon.allocateParty(authContext, createWallet('alice::fingerprint', {
585
+ signingProviderId: SigningProvider.BLOCKDAEMON,
586
+ namespace: 'fingerprint',
587
+ topologyTransactions: 'tx1',
588
+ externalTxId: 'tx-1',
589
+ }), SigningProvider.BLOCKDAEMON);
590
+ expect(mockStore.updateWallet).toHaveBeenCalledWith({
591
+ networkId: 'network1',
592
+ partyId: 'alice::namespace',
593
+ status: 'allocated',
594
+ reason: '',
595
+ });
596
+ });
597
+ it('allocateParty updates wallet to pending state', async () => {
598
+ const serviceWithBlockdaemon = createService({
599
+ [SigningProvider.BLOCKDAEMON]: createBlockdaemonDriver({
600
+ getTransactionResult: { txId: 'tx-1', status: 'pending' },
601
+ }),
602
+ });
603
+ await serviceWithBlockdaemon.allocateParty(authContext, createWallet('alice::fingerprint', {
604
+ signingProviderId: SigningProvider.BLOCKDAEMON,
605
+ topologyTransactions: 'tx1',
606
+ externalTxId: 'tx-1',
607
+ }), SigningProvider.BLOCKDAEMON);
608
+ expect(mockStore.updateWallet).toHaveBeenCalledWith({
609
+ partyId: 'alice::fingerprint',
610
+ networkId: 'network1',
611
+ status: 'initialized',
612
+ reason: WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_PENDING,
613
+ });
614
+ });
615
+ it('allocateParty disables wallet when transaction is rejected', async () => {
616
+ const serviceWithBlockdaemon = createService({
617
+ [SigningProvider.BLOCKDAEMON]: createBlockdaemonDriver({
618
+ getTransactionResult: { txId: 'tx-1', status: 'rejected' },
619
+ }),
620
+ });
621
+ await serviceWithBlockdaemon.allocateParty(authContext, createWallet('alice::fingerprint', {
622
+ signingProviderId: SigningProvider.BLOCKDAEMON,
623
+ topologyTransactions: 'tx1',
624
+ externalTxId: 'tx-1',
625
+ }), SigningProvider.BLOCKDAEMON);
626
+ expect(mockStore.updateWallet).toHaveBeenCalledWith({
627
+ partyId: 'alice::fingerprint',
628
+ networkId: 'network1',
629
+ status: 'removed',
630
+ disabled: true,
631
+ reason: WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_REJECTED,
632
+ });
633
+ });
634
+ it('allocateParty disables wallet when transaction failed', async () => {
635
+ const warnSpy = vi.spyOn(mockLogger, 'warn');
636
+ const serviceWithBlockdaemon = createService({
637
+ [SigningProvider.BLOCKDAEMON]: createBlockdaemonDriver({
638
+ getTransactionResult: {
639
+ txId: 'tx-1',
640
+ status: 'failed',
641
+ metadata: { code: 'X' },
642
+ },
643
+ }),
644
+ });
645
+ await serviceWithBlockdaemon.allocateParty(authContext, createWallet('alice::fingerprint', {
646
+ signingProviderId: SigningProvider.BLOCKDAEMON,
647
+ topologyTransactions: 'tx1',
648
+ externalTxId: 'tx-1',
649
+ }), SigningProvider.BLOCKDAEMON);
650
+ expect(warnSpy).toHaveBeenCalled();
651
+ expect(mockStore.updateWallet).toHaveBeenCalledWith({
652
+ partyId: 'alice::fingerprint',
653
+ networkId: 'network1',
654
+ status: 'removed',
655
+ disabled: true,
656
+ reason: WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_FAILED,
657
+ });
658
+ });
659
+ });
660
+ describe('Dfns', () => {
661
+ it('throws when Dfns signing driver not available', async () => {
662
+ const serviceWithoutDfns = createService({});
663
+ await expect(serviceWithoutDfns.createWallet(authContext, 'alice', false, SigningProvider.DFNS)).rejects.toThrow('Dfns signing driver not available');
664
+ });
665
+ it('createWallet returns initialized when signTransaction returns pending', async () => {
666
+ const serviceWithDfns = createService({
667
+ [SigningProvider.DFNS]: createDfnsDriver({
668
+ signTransactionResult: { status: 'pending', txId: 'tx-1' },
669
+ getTransactionResult: { status: 'pending', txId: 'tx-1' },
670
+ }),
671
+ });
672
+ const result = await serviceWithDfns.createWallet(authContext, 'alice', false, SigningProvider.DFNS);
673
+ expect(result.status).toBe('initialized');
674
+ expect(result.reason).toBe(WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_PENDING);
675
+ expect(result.externalTxId).toBe('tx-1');
676
+ expect(result.partyId).toBe('alice::fingerprint');
677
+ expect(mockStore.addWallet).toHaveBeenCalled();
678
+ });
679
+ it('createWallet returns allocated when signTransaction returns signed', async () => {
680
+ const serviceWithDfns = createService({
681
+ [SigningProvider.DFNS]: createDfnsDriver({
682
+ createKeyResult: { id: 'key-1', publicKey: 'dfns-pk' },
683
+ signTransactionResult: { status: 'signed', txId: 'tx-1' },
684
+ getTransactionResult: {
685
+ txId: 'tx-1',
686
+ status: 'signed',
687
+ signature: 'sig-base64',
688
+ },
689
+ }),
690
+ });
691
+ mockPartyAllocator.allocatePartyWithExistingWallet.mockResolvedValue('alice::namespace');
692
+ const result = await serviceWithDfns.createWallet(authContext, 'alice', false, SigningProvider.DFNS);
296
693
  expect(result.status).toBe('allocated');
297
694
  expect(result.partyId).toBe('alice::namespace');
298
695
  expect(mockPartyAllocator.allocatePartyWithExistingWallet).toHaveBeenCalled();
299
696
  expect(mockStore.addWallet).toHaveBeenCalled();
300
697
  });
698
+ it.each([
699
+ ['failed', WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_FAILED],
700
+ ['rejected', WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_REJECTED],
701
+ ])('createWallet returns status removed with reason when signTransaction returns %s', async (status, expectedReason) => {
702
+ const serviceWithDfns = createService({
703
+ [SigningProvider.DFNS]: createDfnsDriver({
704
+ signTransactionResult: { status, txId: 'tx-1' },
705
+ getTransactionResult: { status, txId: 'tx-1' },
706
+ }),
707
+ });
708
+ const result = await serviceWithDfns.createWallet(authContext, 'alice', false, SigningProvider.DFNS);
709
+ expect(result.status).toBe('removed');
710
+ expect(result.reason).toBe(expectedReason);
711
+ expect(result.disabled).toBe(true);
712
+ expect(mockStore.addWallet).toHaveBeenCalled();
713
+ });
714
+ it('throws when createKey returns a signing error', async () => {
715
+ const serviceWithDfns = createService({
716
+ [SigningProvider.DFNS]: createDfnsDriver({
717
+ createKeyResult: {
718
+ error: 'denied',
719
+ error_description: 'Dfns key error',
720
+ },
721
+ }),
722
+ });
723
+ await expect(serviceWithDfns.createWallet(authContext, 'alice', false, SigningProvider.DFNS)).rejects.toThrow('Error from signing driver: Dfns key error');
724
+ });
725
+ it('throws when a signed createWallet has no signature in getTransaction', async () => {
726
+ const serviceWithDfns = createService({
727
+ [SigningProvider.DFNS]: createDfnsDriver({
728
+ signTransactionResult: { status: 'signed', txId: 'tx-1' },
729
+ getTransactionResult: {
730
+ txId: 'tx-1',
731
+ status: 'signed',
732
+ },
733
+ }),
734
+ });
735
+ await expect(serviceWithDfns.createWallet(authContext, 'alice', false, SigningProvider.DFNS)).rejects.toThrow('Transaction signed but no signature found in result');
736
+ });
737
+ it('throws when allocateParty wallet is missing topology metadata', async () => {
738
+ const serviceWithDfns = createService({
739
+ [SigningProvider.DFNS]: createDfnsDriver({}),
740
+ });
741
+ await expect(serviceWithDfns.allocateParty(authContext, createWallet('alice::fingerprint', {
742
+ signingProviderId: SigningProvider.DFNS,
743
+ topologyTransactions: 'tx1',
744
+ externalTxId: undefined,
745
+ }), SigningProvider.DFNS)).rejects.toThrow('Existing wallet is missing field externalTxId or topologyTransactions');
746
+ });
747
+ it('allocateParty throws when signed transaction has no signature', async () => {
748
+ const serviceWithDfns = createService({
749
+ [SigningProvider.DFNS]: createDfnsDriver({
750
+ getTransactionResult: {
751
+ txId: 'tx-1',
752
+ status: 'signed',
753
+ },
754
+ }),
755
+ });
756
+ await expect(serviceWithDfns.allocateParty(authContext, createWallet('alice::fingerprint', {
757
+ signingProviderId: SigningProvider.DFNS,
758
+ topologyTransactions: 'tx1',
759
+ externalTxId: 'tx-1',
760
+ }), SigningProvider.DFNS)).rejects.toThrow('Transaction signed but no signature found in result');
761
+ });
762
+ it('allocateParty updates wallet to allocated when transaction is signed', async () => {
763
+ const driver = createDfnsDriver({
764
+ getTransactionResult: {
765
+ txId: 'tx-1',
766
+ status: 'signed',
767
+ signature: 'sig-base64',
768
+ },
769
+ });
770
+ const serviceWithDfns = createService({
771
+ [SigningProvider.DFNS]: driver,
772
+ });
773
+ mockPartyAllocator.allocatePartyWithExistingWallet.mockResolvedValue('alice::namespace');
774
+ await serviceWithDfns.allocateParty(authContext, createWallet('alice::fingerprint', {
775
+ signingProviderId: SigningProvider.DFNS,
776
+ namespace: 'fingerprint',
777
+ topologyTransactions: 'tx1',
778
+ externalTxId: 'tx-1',
779
+ }), SigningProvider.DFNS);
780
+ expect(mockPartyAllocator.allocatePartyWithExistingWallet).toHaveBeenCalledWith('fingerprint', ['tx1'], 'sig-base64', authContext.userId);
781
+ expect(mockStore.updateWallet).toHaveBeenCalledWith({
782
+ networkId: 'network1',
783
+ partyId: 'alice::namespace',
784
+ status: 'allocated',
785
+ reason: '',
786
+ });
787
+ });
788
+ it('allocateParty updates wallet to initialized when transaction is pending', async () => {
789
+ const serviceWithDfns = createService({
790
+ [SigningProvider.DFNS]: createDfnsDriver({
791
+ getTransactionResult: { txId: 'tx-1', status: 'pending' },
792
+ }),
793
+ });
794
+ await serviceWithDfns.allocateParty(authContext, createWallet('alice::fingerprint', {
795
+ signingProviderId: SigningProvider.DFNS,
796
+ topologyTransactions: 'tx1',
797
+ externalTxId: 'tx-1',
798
+ }), SigningProvider.DFNS);
799
+ expect(mockStore.updateWallet).toHaveBeenCalledWith({
800
+ partyId: 'alice::fingerprint',
801
+ networkId: 'network1',
802
+ status: 'initialized',
803
+ reason: WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_PENDING,
804
+ });
805
+ });
806
+ it.each([
807
+ ['failed', WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_FAILED],
808
+ ['rejected', WALLET_DISABLED_REASON.TOPOLOGY_TRANSACTION_REJECTED],
809
+ ])('allocateParty disables wallet when transaction is %s', async (status, expectedReason) => {
810
+ const serviceWithDfns = createService({
811
+ [SigningProvider.DFNS]: createDfnsDriver({
812
+ getTransactionResult: {
813
+ txId: 'tx-1',
814
+ status,
815
+ metadata: { cause: 'test' },
816
+ },
817
+ }),
818
+ });
819
+ await serviceWithDfns.allocateParty(authContext, createWallet('alice::fingerprint', {
820
+ signingProviderId: SigningProvider.DFNS,
821
+ topologyTransactions: 'tx1',
822
+ externalTxId: 'tx-1',
823
+ }), SigningProvider.DFNS);
824
+ expect(mockStore.updateWallet).toHaveBeenCalledWith({
825
+ partyId: 'alice::fingerprint',
826
+ networkId: 'network1',
827
+ status: 'removed',
828
+ disabled: true,
829
+ reason: expectedReason,
830
+ });
831
+ });
301
832
  });
302
833
  });