@canton-network/wallet-gateway-remote 0.20.0 → 0.22.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 (84) hide show
  1. package/README.md +1 -3
  2. package/dist/config/Config.d.ts +145 -0
  3. package/dist/config/Config.d.ts.map +1 -1
  4. package/dist/config/Config.js +18 -2
  5. package/dist/config/Config.test.js +3 -0
  6. package/dist/config/ConfigUtils.d.ts.map +1 -1
  7. package/dist/config/ConfigUtils.js +41 -2
  8. package/dist/dapp-api/controller.d.ts.map +1 -1
  9. package/dist/dapp-api/controller.js +7 -14
  10. package/dist/dapp-api/rpc-gen/typings.d.ts +1 -1
  11. package/dist/dapp-api/rpc-gen/typings.d.ts.map +1 -1
  12. package/dist/env.d.ts +19 -0
  13. package/dist/env.d.ts.map +1 -0
  14. package/dist/env.js +16 -0
  15. package/dist/example-config.d.ts +3 -1
  16. package/dist/example-config.d.ts.map +1 -1
  17. package/dist/example-config.js +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +4 -2
  20. package/dist/init.d.ts.map +1 -1
  21. package/dist/init.js +8 -18
  22. package/dist/ledger/party-allocation-service.d.ts +1 -2
  23. package/dist/ledger/party-allocation-service.d.ts.map +1 -1
  24. package/dist/ledger/party-allocation-service.js +5 -4
  25. package/dist/ledger/party-allocation-service.test.js +7 -4
  26. package/dist/ledger/transaction-service.d.ts +22 -0
  27. package/dist/ledger/transaction-service.d.ts.map +1 -0
  28. package/dist/ledger/transaction-service.js +296 -0
  29. package/dist/ledger/wallet-allocation/signing-providers/blockdaemon-wallet-allocator.d.ts +17 -0
  30. package/dist/ledger/wallet-allocation/signing-providers/blockdaemon-wallet-allocator.d.ts.map +1 -0
  31. package/dist/ledger/wallet-allocation/signing-providers/blockdaemon-wallet-allocator.js +146 -0
  32. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.d.ts +17 -0
  33. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.d.ts.map +1 -0
  34. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.js +140 -0
  35. package/dist/ledger/wallet-allocation/signing-providers/kernel-wallet-allocator.d.ts +17 -0
  36. package/dist/ledger/wallet-allocation/signing-providers/kernel-wallet-allocator.d.ts.map +1 -0
  37. package/dist/ledger/wallet-allocation/signing-providers/kernel-wallet-allocator.js +79 -0
  38. package/dist/ledger/wallet-allocation/signing-providers/participant-wallet-allocator.d.ts +15 -0
  39. package/dist/ledger/wallet-allocation/signing-providers/participant-wallet-allocator.d.ts.map +1 -0
  40. package/dist/ledger/wallet-allocation/signing-providers/participant-wallet-allocator.js +37 -0
  41. package/dist/ledger/wallet-allocation/wallet-allocation-service.d.ts +20 -0
  42. package/dist/ledger/wallet-allocation/wallet-allocation-service.d.ts.map +1 -0
  43. package/dist/ledger/wallet-allocation/wallet-allocation-service.js +70 -0
  44. package/dist/ledger/wallet-allocation/wallet-allocation-service.test.d.ts +2 -0
  45. package/dist/ledger/wallet-allocation/wallet-allocation-service.test.d.ts.map +1 -0
  46. package/dist/ledger/wallet-allocation/wallet-allocation-service.test.js +301 -0
  47. package/dist/ledger/wallet-sync-service.d.ts +7 -12
  48. package/dist/ledger/wallet-sync-service.d.ts.map +1 -1
  49. package/dist/ledger/wallet-sync-service.js +128 -100
  50. package/dist/ledger/wallet-sync-service.test.js +194 -46
  51. package/dist/user-api/controller.d.ts +1 -0
  52. package/dist/user-api/controller.d.ts.map +1 -1
  53. package/dist/user-api/controller.js +77 -406
  54. package/dist/user-api/rpc-gen/index.d.ts +3 -0
  55. package/dist/user-api/rpc-gen/index.d.ts.map +1 -1
  56. package/dist/user-api/rpc-gen/index.js +1 -0
  57. package/dist/user-api/rpc-gen/typings.d.ts +61 -40
  58. package/dist/user-api/rpc-gen/typings.d.ts.map +1 -1
  59. package/dist/web/frontend/404/index.html +2 -2
  60. package/dist/web/frontend/approve/index.html +4 -3
  61. package/dist/web/frontend/assets/404-DzH9sSlT.js +8 -0
  62. package/dist/web/frontend/assets/approve-Duv1K5LE.js +17 -0
  63. package/dist/web/frontend/assets/callback-D_VLeaX-.js +1 -0
  64. package/dist/web/frontend/assets/index-Bj5VTWmh.js +1686 -0
  65. package/dist/web/frontend/assets/login-B-jF6DLr.js +7 -0
  66. package/dist/web/frontend/assets/{settings-Br8FgNOa.js → settings-DZpeOwSh.js} +2 -2
  67. package/dist/web/frontend/assets/{state-1o1CuDWy.js → state-PjJJ3Anb.js} +1 -1
  68. package/dist/web/frontend/assets/transactions-DGdh8VAO.js +28 -0
  69. package/dist/web/frontend/assets/utils-D5kQDwtZ.js +1 -0
  70. package/dist/web/frontend/assets/wallets-CjjRt-cQ.js +62 -0
  71. package/dist/web/frontend/callback/index.html +2 -2
  72. package/dist/web/frontend/index.html +1 -1
  73. package/dist/web/frontend/login/index.html +3 -3
  74. package/dist/web/frontend/settings/index.html +3 -3
  75. package/dist/web/frontend/transactions/index.html +4 -3
  76. package/dist/web/frontend/wallets/index.html +4 -3
  77. package/package.json +20 -19
  78. package/dist/web/frontend/assets/404-CKmKuu6H.js +0 -8
  79. package/dist/web/frontend/assets/approve-MzXNo4XO.js +0 -17
  80. package/dist/web/frontend/assets/callback-CcXLRr7Q.js +0 -1
  81. package/dist/web/frontend/assets/index-BnEARqU4.js +0 -1679
  82. package/dist/web/frontend/assets/login-Btlo0Zla.js +0 -7
  83. package/dist/web/frontend/assets/transactions-Cnu9eUFe.js +0 -28
  84. package/dist/web/frontend/assets/wallets-Dj5yeBJd.js +0 -63
@@ -2,14 +2,11 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { defaultRetryableOptions, } from '@canton-network/core-ledger-client';
4
4
  import { SigningProvider, } from '@canton-network/core-signing-lib';
5
- export const WALLET_DISABLED_REASON = {
6
- NO_SIGNING_PROVIDER_MATCHED: 'no signing provider matched',
7
- };
5
+ import { WALLET_DISABLED_REASON } from '@canton-network/core-types';
8
6
  export class WalletSyncService {
9
- constructor(store, ledgerClient, adminLedgerClient, authContext, logger, signingDrivers = {}, partyAllocator) {
7
+ constructor(store, ledgerClient, authContext, logger, signingDrivers = {}, partyAllocator) {
10
8
  this.store = store;
11
9
  this.ledgerClient = ledgerClient;
12
- this.adminLedgerClient = adminLedgerClient;
13
10
  this.authContext = authContext;
14
11
  this.logger = logger;
15
12
  this.signingDrivers = signingDrivers;
@@ -28,7 +25,7 @@ export class WalletSyncService {
28
25
  // (participant parties have namespace === participantId's namespace)
29
26
  let participantNamespace;
30
27
  try {
31
- const { participantId } = await this.adminLedgerClient.getWithRetry('/v2/parties/participant-id', defaultRetryableOptions);
28
+ const { participantId } = await this.ledgerClient.getWithRetry('/v2/parties/participant-id', defaultRetryableOptions);
32
29
  // Extract the namespace part from participantId
33
30
  // Format is hint::namespace
34
31
  const [, extractedNamespace] = participantId.split('::');
@@ -62,7 +59,7 @@ export class WalletSyncService {
62
59
  const result = await controller.getKeys();
63
60
  // In case of error getKeys resolve Promise but with error object
64
61
  if ('error' in result) {
65
- this.logger.debug({
62
+ this.logger.warn({
66
63
  providerId,
67
64
  error: result.error,
68
65
  error_description: result.error_description,
@@ -77,7 +74,7 @@ export class WalletSyncService {
77
74
  continue;
78
75
  const keyNamespace = this.partyAllocator.createFingerprintFromKey(normalizedKey);
79
76
  if (keyNamespace === namespace) {
80
- this.logger.debug({
77
+ this.logger.info({
81
78
  namespace,
82
79
  providerId,
83
80
  keyId: key.id,
@@ -93,7 +90,7 @@ export class WalletSyncService {
93
90
  }
94
91
  }
95
92
  catch (err) {
96
- this.logger.debug({ err, providerId }, 'Error getting keys from signing provider');
93
+ this.logger.error({ err, providerId }, 'Error getting keys from signing provider');
97
94
  // Continue to next signing provider
98
95
  }
99
96
  }
@@ -113,50 +110,47 @@ export class WalletSyncService {
113
110
  };
114
111
  }
115
112
  }
116
- async getPartiesRightsMap() {
113
+ async getPartiesWithRights() {
117
114
  const rights = await this.ledgerClient.getWithRetry('/v2/users/{user-id}/rights', defaultRetryableOptions, {
118
115
  path: {
119
116
  'user-id': this.authContext.userId,
120
117
  },
121
118
  });
122
- const partiesWithRights = new Map();
119
+ const parties = new Set();
123
120
  rights.rights?.forEach((right) => {
124
121
  let party;
125
- let rightType;
126
122
  if ('CanActAs' in right.kind) {
127
123
  party = right.kind.CanActAs.value.party;
128
- rightType = 'CanActAs';
129
124
  }
130
125
  else if ('CanExecuteAs' in right.kind) {
131
126
  party = right.kind.CanExecuteAs.value.party;
132
- rightType = 'CanExecuteAs';
133
127
  }
134
128
  else if ('CanReadAs' in right.kind) {
135
129
  party = right.kind.CanReadAs.value.party;
136
- rightType = 'CanReadAs';
137
130
  }
138
- if (party !== undefined &&
139
- rightType !== undefined &&
140
- !partiesWithRights.has(party))
141
- partiesWithRights.set(party, rightType);
131
+ if (party !== undefined) {
132
+ parties.add(party);
133
+ }
142
134
  });
143
- return partiesWithRights;
135
+ return Array.from(parties);
144
136
  }
145
137
  async isWalletSyncNeeded() {
146
138
  try {
147
139
  const network = await this.store.getCurrentNetwork();
148
140
  const existingWallets = await this.store.getWallets();
149
- const hasDisabledWallets = existingWallets.some((w) => w.disabled);
150
- if (hasDisabledWallets) {
151
- return true;
152
- }
153
- const partiesWithRights = await this.getPartiesRightsMap();
141
+ const partiesWithRights = await this.getPartiesWithRights();
154
142
  // Treat disabled wallets as if they don't exist, so they can be re-synced
155
143
  const enabledWallets = existingWallets.filter((w) => !w.disabled);
156
144
  // Track by (partyId, networkId) combination to handle multi-hosted parties
157
145
  const existingPartyNetworkPairs = new Set(enabledWallets.map((w) => `${w.partyId}:${w.networkId}`));
158
146
  // Check if there are parties on ledger that aren't in store for this network
159
- return Array.from(partiesWithRights.keys()).some((party) => !existingPartyNetworkPairs.has(`${party}:${network.id}`));
147
+ const hasNewPartiesOnLedger = partiesWithRights.some((party) => !existingPartyNetworkPairs.has(`${party}:${network.id}`));
148
+ if (hasNewPartiesOnLedger)
149
+ return true;
150
+ // Check if there are allocated wallets in store whose party is not on ledger
151
+ const hasWalletsWithoutParty = enabledWallets.some((wallet) => wallet.status === 'allocated' &&
152
+ !partiesWithRights.includes(wallet.partyId));
153
+ return hasWalletsWithoutParty;
160
154
  }
161
155
  catch (err) {
162
156
  this.logger.error({ err }, 'Error checking if sync is needed');
@@ -164,94 +158,128 @@ export class WalletSyncService {
164
158
  throw err;
165
159
  }
166
160
  }
161
+ // Participant wallets: disable when party not on ledger (participant node reset, namespace changed).
162
+ // Other wallets: mark as initialized so user can re-allocate (e.g. after external signing).
163
+ async handleWalletsWithoutParty(enabledWallets, partiesWithRights) {
164
+ const walletsWithoutParty = enabledWallets.filter((wallet) => !partiesWithRights.includes(wallet.partyId));
165
+ const markedForAllocateWallets = [];
166
+ const disabledExistingWallets = [];
167
+ for (const wallet of walletsWithoutParty) {
168
+ if (wallet.status !== 'allocated')
169
+ continue;
170
+ try {
171
+ if (wallet.signingProviderId === SigningProvider.PARTICIPANT) {
172
+ this.logger.warn({
173
+ partyId: wallet.partyId,
174
+ signingProviderId: wallet.signingProviderId,
175
+ }, 'Participant wallet party not on ledger, disabling (participant namespace changed)');
176
+ await this.store.updateWallet({
177
+ partyId: wallet.partyId,
178
+ networkId: wallet.networkId,
179
+ disabled: true,
180
+ reason: WALLET_DISABLED_REASON.PARTICIPANT_NAMESPACE_CHANGED,
181
+ ...(wallet.primary && { primary: false }),
182
+ });
183
+ disabledExistingWallets.push(wallet);
184
+ }
185
+ else {
186
+ this.logger.info({
187
+ partyId: wallet.partyId,
188
+ signingProviderId: wallet.signingProviderId,
189
+ }, 'Party not found on participant, marking wallet as initialized');
190
+ await this.store.updateWallet({
191
+ partyId: wallet.partyId,
192
+ networkId: wallet.networkId,
193
+ status: 'initialized',
194
+ ...(wallet.primary && { primary: false }),
195
+ });
196
+ markedForAllocateWallets.push(wallet);
197
+ }
198
+ }
199
+ catch (err) {
200
+ this.logger.warn({ err, partyId: wallet.partyId }, 'Failed to update wallet');
201
+ }
202
+ }
203
+ return {
204
+ markedForAllocateWallets,
205
+ walletsWithoutParty,
206
+ disabledExistingWallets,
207
+ };
208
+ }
209
+ // Creates wallets for parties user has rights to
210
+ async handlePartiesWithoutWallet(newParties, networkId) {
211
+ return await Promise.all(newParties.map(async (partyId) => {
212
+ const [hint, namespace] = partyId.split('::');
213
+ const resolvedSigningProvider = await this.resolveSigningProvider(namespace);
214
+ const isMatched = resolvedSigningProvider.matched;
215
+ const walletPublicKey = resolvedSigningProvider.signingProviderId ===
216
+ SigningProvider.PARTICIPANT
217
+ ? namespace
218
+ : 'publicKey' in resolvedSigningProvider
219
+ ? resolvedSigningProvider.publicKey
220
+ : namespace;
221
+ const wallet = {
222
+ primary: false,
223
+ status: 'allocated',
224
+ partyId,
225
+ hint,
226
+ publicKey: walletPublicKey,
227
+ namespace,
228
+ networkId,
229
+ signingProviderId: resolvedSigningProvider.signingProviderId,
230
+ disabled: !isMatched,
231
+ ...(!isMatched && {
232
+ reason: WALLET_DISABLED_REASON.NO_SIGNING_PROVIDER_MATCHED,
233
+ }),
234
+ };
235
+ this.logger.info({ ...wallet }, 'Wallet sync result');
236
+ await this.store.addWallet(wallet);
237
+ return wallet;
238
+ }));
239
+ }
167
240
  async syncWallets() {
168
241
  this.logger.info('Starting wallet sync...');
169
242
  try {
170
243
  const network = await this.store.getCurrentNetwork();
171
244
  this.logger.info(network, 'Current network');
172
- const partiesWithRights = await this.getPartiesRightsMap();
173
- // Add new Wallets given the found parties
174
- // Only check wallets in the current network
245
+ const partiesWithRights = await this.getPartiesWithRights();
175
246
  const existingWallets = await this.store.getWallets();
176
247
  this.logger.info(existingWallets, 'Existing wallets');
177
- // Treat disabled wallets as if they don't exist, so they can be re-synced
178
- const enabledWallets = existingWallets.filter((w) => !w.disabled);
179
- // Track by (partyId, networkId) combination
180
- const existingPartyNetworkToSigningProvider = new Map(enabledWallets.map((w) => [
181
- `${w.partyId}:${w.networkId}`,
182
- w.signingProviderId,
183
- ]));
184
- // Track disabled wallets by (partyId, networkId) combination
185
- const disabledPartyNetworkPairs = new Set(existingWallets
186
- .filter((w) => w.disabled)
187
- .map((w) => `${w.partyId}:${w.networkId}`));
188
- // Resolve signing providers for all new parties
189
- // Check if (partyId, networkId) combination already exists
190
- const newParties = Array.from(partiesWithRights.keys()).filter((party) => !existingPartyNetworkToSigningProvider.has(`${party}:${network.id}`)
248
+ // Skips wallets for which we didn't allocate a party
249
+ const existingAllocatedWallets = existingWallets.filter((w) => w.status === 'allocated');
250
+ const existingPartiesOnNetwork = new Set(existingAllocatedWallets.map((w) => `${w.partyId}:${w.networkId}`));
251
+ const newParties = partiesWithRights.filter((party) => !existingPartiesOnNetwork.has(`${party}:${network.id}`)
191
252
  // todo: filter on idp id
192
253
  );
193
- this.logger.info({ newParties }, 'Found new parties to sync with Wallet Gateway');
194
- const newParticipantWallets = await Promise.all(newParties.map(async (party) => {
195
- const [hint, namespace] = party.split('::');
196
- const resolvedSigningProvider = await this.resolveSigningProvider(namespace);
197
- // resolvedSigningProvider is never null (participant is default)
198
- const isMatched = resolvedSigningProvider.matched;
199
- // Namespace is saved as public key in case of participant
200
- const walletPublicKey = resolvedSigningProvider.signingProviderId ===
201
- SigningProvider.PARTICIPANT
202
- ? namespace
203
- : 'publicKey' in resolvedSigningProvider
204
- ? resolvedSigningProvider.publicKey
205
- : namespace;
206
- const wallet = {
207
- primary: false,
208
- status: 'allocated',
209
- partyId: party,
210
- hint: hint,
211
- publicKey: walletPublicKey,
212
- namespace: namespace,
213
- networkId: network.id,
214
- signingProviderId: resolvedSigningProvider.signingProviderId,
215
- disabled: !isMatched,
216
- ...(!isMatched && {
217
- reason: WALLET_DISABLED_REASON.NO_SIGNING_PROVIDER_MATCHED,
218
- }),
219
- };
220
- this.logger.info({
221
- ...wallet,
222
- }, 'Wallet sync result');
223
- return wallet;
224
- }));
225
- // Remove disabled wallets that are being re-synced before adding them back
226
- // Filter by (partyId, networkId) combination
227
- await Promise.all(newParticipantWallets
228
- .filter((wallet) => disabledPartyNetworkPairs.has(`${wallet.partyId}:${wallet.networkId}`))
229
- .map((wallet) => {
230
- this.logger.info({
231
- partyId: wallet.partyId,
232
- networkId: wallet.networkId,
233
- }, 'Removing disabled wallet for re-sync');
234
- return this.store.removeWallet(wallet.partyId);
235
- }));
236
- await Promise.all(newParticipantWallets.map((wallet) => this.store.addWallet(wallet)));
237
- this.logger.info({ newParticipantWallets }, 'Created new wallets');
254
+ const { markedForAllocateWallets, walletsWithoutParty, disabledExistingWallets, } = await this.handleWalletsWithoutParty(existingAllocatedWallets, partiesWithRights);
238
255
  this.logger.info({
239
- totalProcessed: newParties.length,
240
- added: newParticipantWallets.length,
241
- disabled: newParticipantWallets.filter((w) => w.disabled)
242
- .length,
243
- }, 'Wallet sync summary');
244
- // Set primary wallet if none exists in current network
256
+ newParties,
257
+ markedForAllocate: markedForAllocateWallets.map((w) => w.partyId),
258
+ }, 'Wallets without parties');
259
+ const newParticipantWallets = await this.handlePartiesWithoutWallet(newParties, network.id);
260
+ // Set primary wallet if none exists, or if primary is on an initialized wallet
245
261
  const networkWallets = await this.store.getWallets();
246
- const hasPrimary = networkWallets.some((w) => w.primary);
247
- if (!hasPrimary && networkWallets.length > 0) {
248
- this.store.setPrimaryWallet(networkWallets[0].partyId);
249
- this.logger.info(`Set ${networkWallets[0].partyId} as primary wallet in network ${network.id}`);
262
+ const primaryWallet = networkWallets.find((w) => w.primary);
263
+ const allocatedWallets = networkWallets.filter((w) => w.status === 'allocated' && !w.disabled);
264
+ const needsPrimaryReset = primaryWallet?.status === 'initialized' ||
265
+ (!primaryWallet && allocatedWallets.length > 0);
266
+ if (needsPrimaryReset && allocatedWallets.length > 0) {
267
+ this.store.setPrimaryWallet(allocatedWallets[0].partyId);
268
+ this.logger.info(`Set ${allocatedWallets[0].partyId} as primary wallet in network ${network.id}`);
250
269
  }
251
- this.logger.debug({ wallets: newParticipantWallets }, 'Wallet sync completed.');
270
+ const disabled = [
271
+ ...newParticipantWallets.filter((wallet) => wallet.disabled),
272
+ ...disabledExistingWallets,
273
+ ];
274
+ this.logger.info({
275
+ added: newParticipantWallets,
276
+ updated: walletsWithoutParty,
277
+ disabled: disabled,
278
+ }, 'Wallet sync completed.');
252
279
  return {
253
280
  added: newParticipantWallets,
254
- removed: [],
281
+ updated: walletsWithoutParty,
282
+ disabled: disabled,
255
283
  };
256
284
  }
257
285
  catch (err) {
@@ -25,6 +25,13 @@ class TestableWalletSyncService extends WalletSyncService {
25
25
  return super.resolveSigningProvider(namespace);
26
26
  }
27
27
  }
28
+ const testATP = (user, token) => ({
29
+ getAccessToken: async () => token,
30
+ getAuthContext: async () => ({
31
+ userId: user,
32
+ accessToken: token,
33
+ }),
34
+ });
28
35
  describe('WalletSyncService - resolveSigningProvider', () => {
29
36
  const authContext = {
30
37
  userId: 'test-user-id',
@@ -57,10 +64,7 @@ describe('WalletSyncService - resolveSigningProvider', () => {
57
64
  // Create real PartyAllocationService
58
65
  partyAllocator = new PartyAllocationService({
59
66
  synchronizerId: 'test-sync-id',
60
- accessTokenProvider: {
61
- getUserAccessToken: async () => 'user.jwt',
62
- getAdminAccessToken: async () => 'admin.jwt',
63
- },
67
+ accessTokenProvider: testATP('admin', 'admin.jwt'),
64
68
  httpLedgerUrl: 'http://test',
65
69
  logger: mockLogger,
66
70
  });
@@ -69,13 +73,10 @@ describe('WalletSyncService - resolveSigningProvider', () => {
69
73
  ledgerClient = new ledgerModule.LedgerClient({
70
74
  baseUrl: new URL('http://test'),
71
75
  logger: mockLogger,
72
- accessTokenProvider: {
73
- getUserAccessToken: async () => 'token',
74
- getAdminAccessToken: async () => 'token',
75
- },
76
+ accessTokenProvider: testATP('token', 'token'),
76
77
  });
77
78
  // Create service with real drivers
78
- service = new TestableWalletSyncService(store, ledgerClient, ledgerClient, authContext, mockLogger, {
79
+ service = new TestableWalletSyncService(store, ledgerClient, authContext, mockLogger, {
79
80
  [SigningProvider.WALLET_KERNEL]: internalDriver,
80
81
  [SigningProvider.PARTICIPANT]: new ParticipantSigningDriver(),
81
82
  }, partyAllocator);
@@ -142,7 +143,7 @@ describe('WalletSyncService - resolveSigningProvider', () => {
142
143
  partyMode: 'EXTERNAL',
143
144
  signingProvider: SigningProvider.FIREBLOCKS,
144
145
  };
145
- const serviceWithFireblocks = new TestableWalletSyncService(store, ledgerClient, ledgerClient, authContext, mockLogger, {
146
+ const serviceWithFireblocks = new TestableWalletSyncService(store, ledgerClient, authContext, mockLogger, {
146
147
  [SigningProvider.FIREBLOCKS]: mockFireblocksDriver,
147
148
  }, partyAllocator);
148
149
  mockLedgerGet.mockResolvedValueOnce({
@@ -177,8 +178,8 @@ describe('WalletSyncService - multi-network features', () => {
177
178
  let mockLogger;
178
179
  let store;
179
180
  let mockLedgerClient;
180
- let mockAdminLedgerClient;
181
181
  let partyAllocator;
182
+ let service;
182
183
  const createNetwork = (id) => ({
183
184
  id,
184
185
  name: `Network ${id}`,
@@ -193,10 +194,10 @@ describe('WalletSyncService - multi-network features', () => {
193
194
  audience: 'aud',
194
195
  },
195
196
  });
196
- const createWallet = (partyId, networkId, disabled = false) => ({
197
+ const createWallet = (partyId, networkId, disabled = false, status = 'allocated') => ({
197
198
  primary: false,
198
199
  partyId,
199
- status: 'allocated',
200
+ status,
200
201
  hint: partyId.split('::')[0],
201
202
  signingProviderId: 'internal',
202
203
  publicKey: 'publicKey',
@@ -237,10 +238,7 @@ describe('WalletSyncService - multi-network features', () => {
237
238
  }
238
239
  partyAllocator = new PartyAllocationService({
239
240
  synchronizerId: 'test-sync-id',
240
- accessTokenProvider: {
241
- getUserAccessToken: async () => 'user.jwt',
242
- getAdminAccessToken: async () => 'admin.jwt',
243
- },
241
+ accessTokenProvider: testATP('admin', 'admin.jwt'),
244
242
  httpLedgerUrl: 'http://test',
245
243
  logger: mockLogger,
246
244
  });
@@ -248,20 +246,9 @@ describe('WalletSyncService - multi-network features', () => {
248
246
  mockLedgerClient = new ledgerModule.LedgerClient({
249
247
  baseUrl: new URL('http://test'),
250
248
  logger: mockLogger,
251
- accessTokenProvider: {
252
- getUserAccessToken: async () => 'token',
253
- getAdminAccessToken: async () => 'token',
254
- },
255
- });
256
- mockAdminLedgerClient = new ledgerModule.LedgerClient({
257
- baseUrl: new URL('http://test'),
258
- logger: mockLogger,
259
- isAdmin: true,
260
- accessTokenProvider: {
261
- getUserAccessToken: async () => 'token',
262
- getAdminAccessToken: async () => 'token',
263
- },
249
+ accessTokenProvider: testATP('token', 'token'),
264
250
  });
251
+ service = new WalletSyncService(store, mockLedgerClient, authContext, mockLogger, {}, partyAllocator);
265
252
  });
266
253
  afterEach(() => {
267
254
  jest.restoreAllMocks();
@@ -286,7 +273,6 @@ describe('WalletSyncService - multi-network features', () => {
286
273
  },
287
274
  ],
288
275
  });
289
- const service = new WalletSyncService(store, mockLedgerClient, mockAdminLedgerClient, authContext, mockLogger, {}, partyAllocator);
290
276
  const syncNeeded = await service.isWalletSyncNeeded();
291
277
  // Should return false because party1 already exists in network1
292
278
  expect(syncNeeded).toBe(false);
@@ -308,7 +294,6 @@ describe('WalletSyncService - multi-network features', () => {
308
294
  },
309
295
  ],
310
296
  });
311
- const service = new WalletSyncService(store, mockLedgerClient, mockAdminLedgerClient, authContext, mockLogger, {}, partyAllocator);
312
297
  const syncNeeded = await service.isWalletSyncNeeded();
313
298
  // Should return true because party1 exists on ledger but not in store for network1
314
299
  expect(syncNeeded).toBe(true);
@@ -345,7 +330,6 @@ describe('WalletSyncService - multi-network features', () => {
345
330
  .mockResolvedValueOnce({
346
331
  participantId: 'participant1::namespace',
347
332
  });
348
- const service = new WalletSyncService(store, mockLedgerClient, mockAdminLedgerClient, authContext, mockLogger, {}, partyAllocator);
349
333
  await service.syncWallets();
350
334
  // Should only add wallet for party3 (party1 already exists)
351
335
  const wallets = await store.getAllWallets({ networkIds: ['network1'] });
@@ -374,7 +358,6 @@ describe('WalletSyncService - multi-network features', () => {
374
358
  .mockResolvedValueOnce({
375
359
  participantId: 'participant1::namespace',
376
360
  });
377
- const service = new WalletSyncService(store, mockLedgerClient, mockAdminLedgerClient, authContext, mockLogger, {}, partyAllocator);
378
361
  await service.syncWallets();
379
362
  // Should add party1 for network1
380
363
  const wallets = await store.getAllWallets({ networkIds: ['network1'] });
@@ -386,7 +369,6 @@ describe('WalletSyncService - multi-network features', () => {
386
369
  await store.addNetwork(network1);
387
370
  await store.addNetwork(network2);
388
371
  await store.addWallet(createWallet('party1::namespace', 'network1'));
389
- const service = new WalletSyncService(store, mockLedgerClient, mockAdminLedgerClient, authContext, mockLogger, {}, partyAllocator);
390
372
  // Mock ledger client to return rights for party1 (multi-hosted party) for network1 check
391
373
  mockLedgerGet.mockResolvedValueOnce({
392
374
  rights: [
@@ -432,7 +414,6 @@ describe('WalletSyncService - multi-network features', () => {
432
414
  // Add wallet to network1 (simulating it was synced there previously)
433
415
  await setSession('network1');
434
416
  await store.addWallet(createWallet('party1::namespace', 'network1'));
435
- const service = new WalletSyncService(store, mockLedgerClient, mockAdminLedgerClient, authContext, mockLogger, {}, partyAllocator);
436
417
  // Sync on network1 (party already exists, should not add)
437
418
  await setSession('network1');
438
419
  // Only need one mock since resolveSigningProvider won't be called if party already exists
@@ -492,15 +473,182 @@ describe('WalletSyncService - multi-network features', () => {
492
473
  expect(party1Wallet?.networkId).toBe('network2');
493
474
  expect(party1Wallet?.disabled).toBe(false);
494
475
  });
495
- // TODO maybe now that we never block sync button don't consider disabled wallet as sync is needed?
496
- it('isWalletSyncNeeded should return true when disabled wallets exist', async () => {
497
- const network1 = createNetwork('network1');
498
- await store.addNetwork(network1);
499
- await setSession('network1');
500
- await store.addWallet(createWallet('party1::namespace', 'network1', true));
501
- const service = new WalletSyncService(store, mockLedgerClient, mockAdminLedgerClient, authContext, mockLogger, {}, partyAllocator);
502
- const syncNeeded = await service.isWalletSyncNeeded();
503
- // Should return true because there's a disabled wallet
504
- expect(syncNeeded).toBe(true);
476
+ describe('Wallet sync - handling wallet not having a party', () => {
477
+ // When party is not on ledger (e.g. after participant reset), sync marks wallet
478
+ // status as 'initialized' so the user can manually re-allocate each one with "Allocate" button
479
+ it('isWalletSyncNeeded should return true when allocated wallet exists but has no party', async () => {
480
+ const network1 = createNetwork('network1');
481
+ await store.addNetwork(network1);
482
+ await setSession('network1');
483
+ await store.addWallet(createWallet('party1::namespace', 'network1', undefined, 'allocated'));
484
+ await store.addWallet(createWallet('party2::namespace', 'network1', undefined, 'allocated'));
485
+ mockLedgerGet.mockResolvedValueOnce({
486
+ rights: [
487
+ {
488
+ kind: {
489
+ CanActAs: {
490
+ value: {
491
+ party: 'party2::namespace',
492
+ },
493
+ },
494
+ },
495
+ },
496
+ ],
497
+ });
498
+ const syncNeeded = await service.isWalletSyncNeeded();
499
+ expect(syncNeeded).toBe(true);
500
+ });
501
+ it('isWalletSyncNeeded should return false when initialized wallet exists and has no party', async () => {
502
+ const network1 = createNetwork('network1');
503
+ await store.addNetwork(network1);
504
+ await setSession('network1');
505
+ await store.addWallet(createWallet('party1::namespace', 'network1', undefined, 'initialized'));
506
+ await store.addWallet(createWallet('party2::namespace', 'network1', undefined, 'allocated'));
507
+ mockLedgerGet.mockResolvedValueOnce({
508
+ rights: [
509
+ {
510
+ kind: {
511
+ CanActAs: {
512
+ value: {
513
+ party: 'party2::namespace',
514
+ },
515
+ },
516
+ },
517
+ },
518
+ ],
519
+ });
520
+ const syncNeeded = await service.isWalletSyncNeeded();
521
+ expect(syncNeeded).toBe(false);
522
+ });
523
+ it('syncWallets marks allocated wallet as initialized when party not on ledger', async () => {
524
+ const network1 = createNetwork('network1');
525
+ await store.addNetwork(network1);
526
+ await setSession('network1');
527
+ await store.addWallet(createWallet('party1::namespace', 'network1'));
528
+ mockLedgerGet
529
+ .mockResolvedValueOnce({
530
+ rights: [
531
+ {
532
+ kind: {
533
+ CanActAs: {
534
+ value: { party: 'party2::namespace' },
535
+ },
536
+ },
537
+ },
538
+ ],
539
+ })
540
+ .mockResolvedValueOnce({
541
+ participantId: 'participant1::namespace',
542
+ });
543
+ const updateWalletSpy = jest.spyOn(store, 'updateWallet');
544
+ await service.syncWallets();
545
+ expect(updateWalletSpy).toHaveBeenCalledWith(expect.objectContaining({
546
+ partyId: 'party1::namespace',
547
+ networkId: 'network1',
548
+ status: 'initialized',
549
+ }));
550
+ const wallets = await store.getWallets();
551
+ const party1Wallet = wallets.find((w) => w.partyId === 'party1::namespace');
552
+ expect(party1Wallet?.status).toBe('initialized');
553
+ });
554
+ it('syncWallets skips wallet when status is already initialized', async () => {
555
+ const network1 = createNetwork('network1');
556
+ await store.addNetwork(network1);
557
+ await setSession('network1');
558
+ const initializedWallet = createWallet('party1::namespace', 'network1');
559
+ initializedWallet.status = 'initialized';
560
+ await store.addWallet(initializedWallet);
561
+ mockLedgerGet
562
+ .mockResolvedValueOnce({
563
+ rights: [
564
+ {
565
+ kind: {
566
+ CanActAs: {
567
+ value: { party: 'party2::namespace' },
568
+ },
569
+ },
570
+ },
571
+ ],
572
+ })
573
+ .mockResolvedValueOnce({
574
+ participantId: 'participant1::namespace',
575
+ });
576
+ const updateWalletSpy = jest.spyOn(store, 'updateWallet');
577
+ await service.syncWallets();
578
+ expect(updateWalletSpy).not.toHaveBeenCalled();
579
+ const wallets = await store.getWallets();
580
+ const party1Wallet = wallets.find((w) => w.partyId === 'party1::namespace');
581
+ expect(party1Wallet?.status).toBe('initialized');
582
+ });
583
+ it('syncWallets marks multiple wallets as initialized when parties not on ledger', async () => {
584
+ const network1 = createNetwork('network1');
585
+ await store.addNetwork(network1);
586
+ await setSession('network1');
587
+ await store.addWallet(createWallet('party1::namespace', 'network1'));
588
+ await store.addWallet(createWallet('party2::namespace', 'network1'));
589
+ mockLedgerGet
590
+ .mockResolvedValueOnce({
591
+ rights: [
592
+ {
593
+ kind: {
594
+ CanActAs: {
595
+ value: { party: 'party3::namespace' },
596
+ },
597
+ },
598
+ },
599
+ ],
600
+ })
601
+ .mockResolvedValueOnce({
602
+ participantId: 'participant1::namespace',
603
+ });
604
+ const updateWalletSpy = jest.spyOn(store, 'updateWallet');
605
+ await service.syncWallets();
606
+ expect(updateWalletSpy).toHaveBeenCalledTimes(2);
607
+ expect(updateWalletSpy).toHaveBeenCalledWith(expect.objectContaining({
608
+ partyId: 'party1::namespace',
609
+ networkId: 'network1',
610
+ status: 'initialized',
611
+ }));
612
+ expect(updateWalletSpy).toHaveBeenCalledWith(expect.objectContaining({
613
+ partyId: 'party2::namespace',
614
+ networkId: 'network1',
615
+ status: 'initialized',
616
+ }));
617
+ });
618
+ it('syncWallets disables participant wallet when party not on ledger', async () => {
619
+ const network1 = createNetwork('network1');
620
+ await store.addNetwork(network1);
621
+ await setSession('network1');
622
+ const participantWallet = createWallet('party1::namespace', 'network1');
623
+ participantWallet.signingProviderId = SigningProvider.PARTICIPANT;
624
+ await store.addWallet(participantWallet);
625
+ mockLedgerGet
626
+ .mockResolvedValueOnce({
627
+ rights: [
628
+ {
629
+ kind: {
630
+ CanActAs: {
631
+ value: { party: 'party2::namespace' },
632
+ },
633
+ },
634
+ },
635
+ ],
636
+ })
637
+ .mockResolvedValueOnce({
638
+ participantId: 'participant1::namespace',
639
+ });
640
+ const updateWalletSpy = jest.spyOn(store, 'updateWallet');
641
+ await service.syncWallets();
642
+ expect(updateWalletSpy).toHaveBeenCalledWith(expect.objectContaining({
643
+ partyId: 'party1::namespace',
644
+ networkId: 'network1',
645
+ disabled: true,
646
+ reason: 'participant namespace changed',
647
+ }));
648
+ const wallets = await store.getWallets();
649
+ const party1Wallet = wallets.find((w) => w.partyId === 'party1::namespace');
650
+ expect(party1Wallet?.disabled).toBe(true);
651
+ expect(party1Wallet?.reason).toBe('participant namespace changed');
652
+ });
505
653
  });
506
654
  });