@canton-network/wallet-gateway-remote 0.9.2 → 0.11.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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/config/Config.test.js +3 -3
  3. package/dist/dapp-api/controller.d.ts +1 -1
  4. package/dist/dapp-api/controller.d.ts.map +1 -1
  5. package/dist/dapp-api/controller.js +39 -19
  6. package/dist/dapp-api/rpc-gen/typings.d.ts +68 -25
  7. package/dist/dapp-api/rpc-gen/typings.d.ts.map +1 -1
  8. package/dist/dapp-api/server.d.ts.map +1 -1
  9. package/dist/dapp-api/server.js +36 -33
  10. package/dist/dapp-api/server.test.js +10 -18
  11. package/dist/init.d.ts.map +1 -1
  12. package/dist/init.js +7 -23
  13. package/dist/ledger/party-allocation-service.d.ts +7 -0
  14. package/dist/ledger/party-allocation-service.d.ts.map +1 -1
  15. package/dist/ledger/party-allocation-service.js +37 -1
  16. package/dist/ledger/party-allocation-service.test.js +3 -1
  17. package/dist/ledger/wallet-sync-service.d.ts +11 -1
  18. package/dist/ledger/wallet-sync-service.d.ts.map +1 -1
  19. package/dist/ledger/wallet-sync-service.js +121 -10
  20. package/dist/ledger/wallet-sync-service.test.d.ts +2 -0
  21. package/dist/ledger/wallet-sync-service.test.d.ts.map +1 -0
  22. package/dist/ledger/wallet-sync-service.test.js +163 -0
  23. package/dist/middleware/jsonRpcHandler.d.ts.map +1 -1
  24. package/dist/middleware/jsonRpcHandler.js +8 -0
  25. package/dist/middleware/sessionHandler.d.ts +13 -0
  26. package/dist/middleware/sessionHandler.d.ts.map +1 -0
  27. package/dist/middleware/sessionHandler.js +37 -0
  28. package/dist/notification/NotificationService.d.ts +8 -1
  29. package/dist/notification/NotificationService.d.ts.map +1 -1
  30. package/dist/notification/NotificationService.js +22 -1
  31. package/dist/user-api/controller.d.ts.map +1 -1
  32. package/dist/user-api/controller.js +40 -9
  33. package/dist/user-api/rpc-gen/typings.d.ts +10 -0
  34. package/dist/user-api/rpc-gen/typings.d.ts.map +1 -1
  35. package/dist/user-api/server.test.js +5 -9
  36. package/dist/web/frontend/404/index.html +2 -2
  37. package/dist/web/frontend/approve/index.html +3 -3
  38. package/dist/web/frontend/assets/{404-Dget2-k2.js → 404-m2H-YHg-.js} +1 -1
  39. package/dist/web/frontend/assets/{approve-ChuS996j.js → approve-D-yQkaxd.js} +13 -11
  40. package/dist/web/frontend/assets/callback-Da6RLMl5.js +1 -0
  41. package/dist/web/frontend/assets/index-DWw9PrWk.js +1011 -0
  42. package/dist/web/frontend/assets/login-DeTFkeYw.js +186 -0
  43. package/dist/web/frontend/assets/{settings-DTCtvDW7.js → settings-DdQ6zt_Y.js} +1 -1
  44. package/dist/web/frontend/assets/{state-Ck_F88ae.js → state-Wv0NKnXW.js} +1 -1
  45. package/dist/web/frontend/assets/{wallets-DcuGVzJf.js → wallets-ySaBy7V_.js} +1 -1
  46. package/dist/web/frontend/callback/index.html +2 -2
  47. package/dist/web/frontend/index.html +1 -1
  48. package/dist/web/frontend/login/index.html +3 -3
  49. package/dist/web/frontend/settings/index.html +3 -3
  50. package/dist/web/frontend/wallets/index.html +3 -3
  51. package/package.json +17 -17
  52. package/dist/web/frontend/assets/callback-2I6lJV7y.js +0 -1
  53. package/dist/web/frontend/assets/index-DyLgNi-5.js +0 -1011
  54. package/dist/web/frontend/assets/login-DvWCqhsk.js +0 -186
@@ -1,7 +1,9 @@
1
1
  import { LedgerClient } from '@canton-network/core-ledger-client';
2
2
  import { AuthContext } from '@canton-network/core-wallet-auth';
3
3
  import { Store, Wallet } from '@canton-network/core-wallet-store';
4
+ import { SigningDriverInterface, SigningProvider } from '@canton-network/core-signing-lib';
4
5
  import { Logger } from 'pino';
6
+ import { PartyAllocationService } from './party-allocation-service.js';
5
7
  export type WalletSyncReport = {
6
8
  added: Wallet[];
7
9
  removed: Wallet[];
@@ -11,8 +13,16 @@ export declare class WalletSyncService {
11
13
  private ledgerClient;
12
14
  private authContext;
13
15
  private logger;
14
- constructor(store: Store, ledgerClient: LedgerClient, authContext: AuthContext, logger: Logger);
16
+ private signingDrivers;
17
+ private partyAllocator;
18
+ constructor(store: Store, ledgerClient: LedgerClient, authContext: AuthContext, logger: Logger, signingDrivers: Partial<Record<SigningProvider, SigningDriverInterface>> | undefined, partyAllocator: PartyAllocationService);
15
19
  run(timeoutMs: number): Promise<void>;
20
+ protected resolveSigningProvider(namespace: string): Promise<{
21
+ signingProviderId: SigningProvider.PARTICIPANT;
22
+ } | {
23
+ signingProviderId: Exclude<SigningProvider, SigningProvider.PARTICIPANT>;
24
+ publicKey: string;
25
+ } | null>;
16
26
  syncWallets(): Promise<WalletSyncReport>;
17
27
  }
18
28
  //# sourceMappingURL=wallet-sync-service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wallet-sync-service.d.ts","sourceRoot":"","sources":["../../src/ledger/wallet-sync-service.ts"],"names":[],"mappings":"AAGA,OAAO,EACH,YAAY,EAEf,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAC9D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAE7B,MAAM,MAAM,gBAAgB,GAAG;IAC3B,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB,CAAA;AACD,qBAAa,iBAAiB;IAEtB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,MAAM;gBAHN,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,MAAM;IAGpB,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrC,WAAW,IAAI,OAAO,CAAC,gBAAgB,CAAC;CA+FjD"}
1
+ {"version":3,"file":"wallet-sync-service.d.ts","sourceRoot":"","sources":["../../src/ledger/wallet-sync-service.ts"],"names":[],"mappings":"AAGA,OAAO,EACH,YAAY,EAEf,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAC9D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AACjE,OAAO,EACH,sBAAsB,EACtB,eAAe,EAClB,MAAM,kCAAkC,CAAA;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA;AAEtE,MAAM,MAAM,gBAAgB,GAAG;IAC3B,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB,CAAA;AACD,qBAAa,iBAAiB;IAEtB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,cAAc;IAGtB,OAAO,CAAC,cAAc;gBAPd,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,OAAO,CAC3B,MAAM,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAClD,YAAK,EACE,cAAc,EAAE,sBAAsB;IAG5C,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;cAU3B,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAC5D;QAAE,iBAAiB,EAAE,eAAe,CAAC,WAAW,CAAA;KAAE,GAClD;QACI,iBAAiB,EAAE,OAAO,CACtB,eAAe,EACf,eAAe,CAAC,WAAW,CAC9B,CAAA;QACD,SAAS,EAAE,MAAM,CAAA;KACpB,GACD,IAAI,CACT;IAiHK,WAAW,IAAI,OAAO,CAAC,gBAAgB,CAAC;CAkJjD"}
@@ -1,12 +1,15 @@
1
1
  // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { defaultRetryableOptions, } from '@canton-network/core-ledger-client';
4
+ import { SigningProvider, } from '@canton-network/core-signing-lib';
4
5
  export class WalletSyncService {
5
- constructor(store, ledgerClient, authContext, logger) {
6
+ constructor(store, ledgerClient, authContext, logger, signingDrivers = {}, partyAllocator) {
6
7
  this.store = store;
7
8
  this.ledgerClient = ledgerClient;
8
9
  this.authContext = authContext;
9
10
  this.logger = logger;
11
+ this.signingDrivers = signingDrivers;
12
+ this.partyAllocator = partyAllocator;
10
13
  }
11
14
  async run(timeoutMs) {
12
15
  this.logger.info(`Starting wallet sync service with ${timeoutMs}ms interval`);
@@ -15,6 +18,86 @@ export class WalletSyncService {
15
18
  await new Promise((res) => setTimeout(res, timeoutMs));
16
19
  }
17
20
  }
21
+ async resolveSigningProvider(namespace) {
22
+ try {
23
+ // Check if namespace matches participant namespace first
24
+ // (participant parties have namespace === participantId's namespace)
25
+ let participantNamespace;
26
+ try {
27
+ const { participantId } = await this.ledgerClient.getWithRetry('/v2/parties/participant-id', defaultRetryableOptions);
28
+ // Extract the namespace part from participantId
29
+ // Format is hint::namespace
30
+ const [, extractedNamespace] = participantId.split('::');
31
+ if (extractedNamespace) {
32
+ participantNamespace = extractedNamespace;
33
+ }
34
+ else {
35
+ this.logger.warn({ participantId }, `Invalid participantId format: expected "hint::namespace", got "${participantId}"`);
36
+ }
37
+ }
38
+ catch (err) {
39
+ this.logger.warn({ err }, 'Failed to get participant namespace');
40
+ }
41
+ if (participantNamespace && namespace === participantNamespace) {
42
+ return { signingProviderId: SigningProvider.PARTICIPANT };
43
+ }
44
+ // Get keys from signing providers try to match
45
+ const userId = this.authContext?.userId;
46
+ for (const [providerId, driver] of Object.entries(this.signingDrivers)) {
47
+ if (!driver)
48
+ continue;
49
+ // Participant already handled above
50
+ if (providerId === SigningProvider.PARTICIPANT) {
51
+ continue;
52
+ }
53
+ try {
54
+ const controller = driver.controller(userId);
55
+ const result = await controller.getKeys();
56
+ // In case of error getKeys resolve Promise but with error object
57
+ if ('error' in result) {
58
+ this.logger.debug({
59
+ providerId,
60
+ error: result.error,
61
+ error_description: result.error_description,
62
+ }, 'Failed to get keys from signing provider');
63
+ continue;
64
+ }
65
+ // Try to match namespace with public keys
66
+ if (result.keys) {
67
+ for (const key of result.keys) {
68
+ const normalizedKey = this.partyAllocator.normalizePublicKeyToBase64(key.publicKey);
69
+ if (!normalizedKey)
70
+ continue;
71
+ const keyNamespace = this.partyAllocator.createFingerprintFromKey(normalizedKey);
72
+ if (keyNamespace === namespace) {
73
+ this.logger.debug({
74
+ namespace,
75
+ providerId,
76
+ keyId: key.id,
77
+ publicKey: key.publicKey,
78
+ }, 'Matched namespace with signing provider');
79
+ return {
80
+ signingProviderId: providerId,
81
+ publicKey: key.publicKey,
82
+ };
83
+ }
84
+ }
85
+ }
86
+ }
87
+ catch (err) {
88
+ this.logger.debug({ err, providerId }, 'Error getting keys from signing provider');
89
+ // Continue to next signing provider
90
+ }
91
+ }
92
+ // No match found - reject this wallet
93
+ this.logger.warn({ namespace }, 'No signing provider match found for namespace, rejecting wallet');
94
+ return null;
95
+ }
96
+ catch (err) {
97
+ this.logger.error({ err, namespace }, 'Error resolving signing provider, rejecting wallet');
98
+ return null;
99
+ }
100
+ }
18
101
  async syncWallets() {
19
102
  this.logger.info('Starting wallet sync...');
20
103
  try {
@@ -47,30 +130,58 @@ export class WalletSyncService {
47
130
  !partiesWithRights.has(party))
48
131
  partiesWithRights.set(party, rightType);
49
132
  });
50
- this.logger.info(partiesWithRights, 'Found new parties to sync with Wallet Gateway');
133
+ this.logger.info([...partiesWithRights], 'Found new parties to sync with Wallet Gateway');
51
134
  // Add new Wallets given the found parties
52
135
  const existingWallets = await this.store.getWallets();
53
136
  this.logger.info(existingWallets, 'Existing wallets');
54
137
  const existingPartyIdToSigningProvider = new Map(existingWallets.map((w) => [w.partyId, w.signingProviderId]));
55
- const newParticipantWallets = Array.from(partiesWithRights.keys())
56
- ?.filter((party) => !existingPartyIdToSigningProvider.has(party)
138
+ // Resolve signing providers for all new parties
139
+ const newParties = Array.from(partiesWithRights.keys()).filter((party) => !existingPartyIdToSigningProvider.has(party)
57
140
  // todo: filter on idp id
58
- )
59
- .map((party) => {
141
+ );
142
+ const walletResults = await Promise.all(newParties.map(async (party) => {
60
143
  const [hint, namespace] = party.split('::');
144
+ const resolvedSigningProvider = await this.resolveSigningProvider(namespace);
145
+ // Reject wallets where no signing provider match was found
146
+ if (!resolvedSigningProvider) {
147
+ this.logger.warn({ party, hint, namespace }, 'Rejecting wallet - no signing provider match found');
148
+ return null;
149
+ }
150
+ // Namespace is saved as public key in case of participant
151
+ const walletPublicKey = resolvedSigningProvider.signingProviderId ===
152
+ SigningProvider.PARTICIPANT
153
+ ? namespace
154
+ : resolvedSigningProvider.publicKey;
155
+ this.logger.info({
156
+ primary: false,
157
+ status: 'allocated',
158
+ partyId: party,
159
+ hint: hint,
160
+ publicKey: walletPublicKey,
161
+ namespace: namespace,
162
+ networkId: network.id,
163
+ signingProviderId: resolvedSigningProvider.signingProviderId,
164
+ }, 'Wallet sync result');
61
165
  return {
62
166
  primary: false,
63
167
  status: 'allocated',
64
168
  partyId: party,
65
169
  hint: hint,
66
- publicKey: namespace,
170
+ publicKey: walletPublicKey,
67
171
  namespace: namespace,
68
172
  networkId: network.id,
69
- signingProviderId: 'participant', // todo: determine based on partyDetails.isLocal
173
+ signingProviderId: resolvedSigningProvider.signingProviderId,
70
174
  };
71
- }) || [];
175
+ }));
176
+ // Filter out rejected wallets
177
+ const newParticipantWallets = walletResults.filter((wallet) => wallet !== null);
72
178
  await Promise.all(newParticipantWallets.map((wallet) => this.store.addWallet(wallet)));
73
- this.logger.info(newParticipantWallets, 'Created new wallets');
179
+ this.logger.info({ newParticipantWallets }, 'Created new wallets');
180
+ this.logger.info({
181
+ totalProcessed: newParties.length,
182
+ rejected: newParties.length - newParticipantWallets.length,
183
+ added: newParticipantWallets.length,
184
+ }, 'Wallet sync summary');
74
185
  // Set primary wallet if none exists
75
186
  const wallets = await this.store.getWallets();
76
187
  const hasPrimary = wallets.some((w) => w.primary);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=wallet-sync-service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wallet-sync-service.test.d.ts","sourceRoot":"","sources":["../../src/ledger/wallet-sync-service.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,163 @@
1
+ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { jest, describe, it, expect, beforeEach, afterEach, } from '@jest/globals';
4
+ import { pino } from 'pino';
5
+ import { sink } from 'pino-test';
6
+ import { SigningProvider, } from '@canton-network/core-signing-lib';
7
+ import { InternalSigningDriver } from '@canton-network/core-signing-internal';
8
+ import { ParticipantSigningDriver } from '@canton-network/core-signing-participant';
9
+ import { StoreSql, connection, migrator, } from '@canton-network/core-signing-store-sql';
10
+ import { WalletSyncService } from './wallet-sync-service.js';
11
+ import { PartyAllocationService } from './party-allocation-service.js';
12
+ const mockLedgerGet = jest.fn();
13
+ jest.unstable_mockModule('@canton-network/core-ledger-client', () => ({
14
+ LedgerClient: jest.fn().mockImplementation(() => {
15
+ return {
16
+ getWithRetry: mockLedgerGet,
17
+ };
18
+ }),
19
+ defaultRetryableOptions: {},
20
+ }));
21
+ // Test subclass to expose protected method
22
+ class TestableWalletSyncService extends WalletSyncService {
23
+ async resolveSigningProvider(namespace) {
24
+ return super.resolveSigningProvider(namespace);
25
+ }
26
+ }
27
+ describe('WalletSyncService - resolveSigningProvider', () => {
28
+ const authContext = {
29
+ userId: 'test-user-id',
30
+ accessToken: 'test-access-token',
31
+ };
32
+ let mockLogger;
33
+ let store;
34
+ let ledgerClient;
35
+ let partyAllocator;
36
+ let service;
37
+ beforeEach(async () => {
38
+ mockLogger = pino(sink());
39
+ // Create in-memory SQLite store for InternalSigningDriver
40
+ const db = connection({
41
+ connection: {
42
+ type: 'sqlite',
43
+ database: ':memory:',
44
+ },
45
+ });
46
+ const umzug = migrator(db);
47
+ const pending = await umzug.pending();
48
+ if (pending.length > 0) {
49
+ await umzug.up();
50
+ }
51
+ const signingStore = new StoreSql(db, mockLogger, authContext);
52
+ // Create real InternalSigningDriver with real store
53
+ const internalDriver = new InternalSigningDriver(signingStore);
54
+ // Store is not used in resolveSigningProvider tests
55
+ store = {};
56
+ // Create real PartyAllocationService
57
+ partyAllocator = new PartyAllocationService({
58
+ synchronizerId: 'test-sync-id',
59
+ accessTokenProvider: {
60
+ getUserAccessToken: async () => 'user.jwt',
61
+ getAdminAccessToken: async () => 'admin.jwt',
62
+ },
63
+ httpLedgerUrl: 'http://test',
64
+ logger: mockLogger,
65
+ });
66
+ // Create mocked ledger client (whole module is already mocked)
67
+ const ledgerModule = await import('@canton-network/core-ledger-client');
68
+ ledgerClient = new ledgerModule.LedgerClient({
69
+ baseUrl: new URL('http://test'),
70
+ logger: mockLogger,
71
+ accessTokenProvider: {
72
+ getUserAccessToken: async () => 'token',
73
+ getAdminAccessToken: async () => 'token',
74
+ },
75
+ });
76
+ // Create service with real drivers
77
+ service = new TestableWalletSyncService(store, ledgerClient, authContext, mockLogger, {
78
+ [SigningProvider.WALLET_KERNEL]: internalDriver,
79
+ [SigningProvider.PARTICIPANT]: new ParticipantSigningDriver(),
80
+ }, partyAllocator);
81
+ });
82
+ afterEach(() => {
83
+ jest.restoreAllMocks();
84
+ mockLedgerGet.mockClear();
85
+ });
86
+ it('resolves participant when namespace matches participant namespace', async () => {
87
+ const participantNamespace = 'participant-namespace-123';
88
+ const participantId = `participant1::${participantNamespace}`;
89
+ mockLedgerGet.mockResolvedValueOnce({
90
+ participantId,
91
+ });
92
+ const result = await service.resolveSigningProvider(participantNamespace);
93
+ expect(result).not.toBeNull();
94
+ expect(result).toEqual({
95
+ signingProviderId: SigningProvider.PARTICIPANT,
96
+ });
97
+ if (result) {
98
+ expect('publicKey' in result).toBe(false);
99
+ }
100
+ });
101
+ it('resolves wallet-kernel when namespace matches internal key', async () => {
102
+ const internalDriver = service['signingDrivers'][SigningProvider.WALLET_KERNEL];
103
+ const controller = internalDriver.controller(authContext.userId);
104
+ const key = await controller.createKey({ name: 'test-key' });
105
+ const namespace = partyAllocator.createFingerprintFromKey(key.publicKey);
106
+ mockLedgerGet.mockResolvedValueOnce({
107
+ participantId: 'participant1::different-participant-namespace',
108
+ });
109
+ const result = await service.resolveSigningProvider(namespace);
110
+ expect(result).not.toBeNull();
111
+ if (result) {
112
+ expect(result.signingProviderId).toBe(SigningProvider.WALLET_KERNEL);
113
+ if (result.signingProviderId !== SigningProvider.PARTICIPANT) {
114
+ expect(result.publicKey).toBe(key.publicKey);
115
+ }
116
+ }
117
+ });
118
+ it('resolves fireblocks when namespace matches fireblocks key', async () => {
119
+ const fireblocksPublicKeyHex = '02fefbcc9aebc8a479f211167a9f564df53aefd603a8662d9449a98c1ead2eba';
120
+ // Convert hex to base64, then calculate namespace
121
+ const normalizedKey = partyAllocator.normalizePublicKeyToBase64(fireblocksPublicKeyHex);
122
+ const namespace = partyAllocator.createFingerprintFromKey(normalizedKey);
123
+ const mockFireblocksDriver = {
124
+ controller: jest.fn().mockReturnValue({
125
+ getKeys: jest
126
+ .fn()
127
+ .mockResolvedValue({
128
+ keys: [
129
+ {
130
+ id: '44-6767-1-0-0',
131
+ name: 'test-vault',
132
+ publicKey: fireblocksPublicKeyHex,
133
+ },
134
+ ],
135
+ }),
136
+ }),
137
+ partyMode: 'EXTERNAL',
138
+ signingProvider: SigningProvider.FIREBLOCKS,
139
+ };
140
+ const serviceWithFireblocks = new TestableWalletSyncService(store, ledgerClient, authContext, mockLogger, {
141
+ [SigningProvider.FIREBLOCKS]: mockFireblocksDriver,
142
+ }, partyAllocator);
143
+ mockLedgerGet.mockResolvedValueOnce({
144
+ participantId: 'participant1::different-participant-namespace',
145
+ });
146
+ const result = await serviceWithFireblocks.resolveSigningProvider(namespace);
147
+ expect(result).not.toBeNull();
148
+ if (result) {
149
+ expect(result.signingProviderId).toBe(SigningProvider.FIREBLOCKS);
150
+ if (result.signingProviderId !== SigningProvider.PARTICIPANT) {
151
+ expect(result.publicKey).toBe(fireblocksPublicKeyHex);
152
+ }
153
+ }
154
+ });
155
+ it('returns null when no signing provider match is found', async () => {
156
+ const unknownNamespace = 'unknown-namespace-123';
157
+ mockLedgerGet.mockResolvedValueOnce({
158
+ participantId: 'participant1::different-participant-namespace',
159
+ });
160
+ const result = await service.resolveSigningProvider(unknownNamespace);
161
+ expect(result).toBeNull();
162
+ });
163
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"jsonRpcHandler.d.ts","sourceRoot":"","sources":["../../src/middleware/jsonRpcHandler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAa7B,UAAU,kBAAkB,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,CAAC,CAAA;CAChB;AAyDD,eAAO,MAAM,cAAc,GAEtB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,EAAE,kCAGjD,kBAAkB,CAAC,CAAC,CAAC,MAMZ,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,SA4E1D,CAAA"}
1
+ {"version":3,"file":"jsonRpcHandler.d.ts","sourceRoot":"","sources":["../../src/middleware/jsonRpcHandler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAc7B,UAAU,kBAAkB,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,CAAC,CAAA;CAChB;AAiED,eAAO,MAAM,cAAc,GAEtB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,EAAE,kCAGjD,kBAAkB,CAAC,CAAC,CAAC,MAMZ,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,SA4E1D,CAAA"}
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { JsonRpcError, rpcErrors, toHttpErrorCode, } from '@canton-network/core-rpc-errors';
4
4
  import { ErrorResponse, JsonRpcRequest, jsonRpcResponse, } from '@canton-network/core-types';
5
+ import { isJsCantonError } from '@canton-network/core-ledger-client';
5
6
  /**
6
7
  * Handles JSON-RPC errors and maps them to HTTP responses.
7
8
  * @param error The error that occurred.
@@ -26,6 +27,13 @@ const handleRpcError = (error, id, logger, method) => {
26
27
  const httpCode = toHttpErrorCode(error.code);
27
28
  return [httpCode, jsonRpcResponse(id, response)];
28
29
  }
30
+ if (isJsCantonError(error)) {
31
+ response.error = {
32
+ code: rpcErrors.internal().code,
33
+ message: error.cause,
34
+ data: error,
35
+ };
36
+ }
29
37
  if (error instanceof Error) {
30
38
  response.error.message = error.message;
31
39
  }
@@ -0,0 +1,13 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import { AuthAware } from '@canton-network/core-wallet-auth';
3
+ import { Logger } from 'pino';
4
+ import { Store } from '@canton-network/core-wallet-store';
5
+ /**
6
+ * Middleware to handle session validation based on user sessions.
7
+ * @param store needs to be AuthAware
8
+ * @param allowedPaths a record of path -> list of methods which do not require authentication
9
+ * @param logger
10
+ * @returns
11
+ */
12
+ export declare function sessionHandler(store: Store & AuthAware<Store>, allowedPaths: Record<string, string[]>, logger: Logger): (req: Request, res: Response, next: NextFunction) => Promise<void>;
13
+ //# sourceMappingURL=sessionHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionHandler.d.ts","sourceRoot":"","sources":["../../src/middleware/sessionHandler.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAA;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAA;AAEzD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,EAC/B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EACtC,MAAM,EAAE,MAAM,IAEA,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBA6BhE"}
@@ -0,0 +1,37 @@
1
+ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // middleware/jwtAuth.ts
4
+ /**
5
+ * Middleware to handle session validation based on user sessions.
6
+ * @param store needs to be AuthAware
7
+ * @param allowedPaths a record of path -> list of methods which do not require authentication
8
+ * @param logger
9
+ * @returns
10
+ */
11
+ export function sessionHandler(store, allowedPaths, logger) {
12
+ return async (req, res, next) => {
13
+ const context = req.authContext;
14
+ const allowedMethods = allowedPaths[req.baseUrl];
15
+ if (req.method !== 'POST') {
16
+ logger.debug('Skipping authentication for OPTIONS request to ' + req.baseUrl);
17
+ next();
18
+ }
19
+ else if (allowedMethods &&
20
+ (allowedMethods.includes(req.body.method) ||
21
+ allowedMethods.includes('*'))) {
22
+ logger.debug(`Allowing unauthenticated access to ${req.baseUrl} for method ${req.body.method}`);
23
+ next();
24
+ }
25
+ else {
26
+ logger.debug('Checking for active session for ' + context?.userId);
27
+ const session = await store.withAuthContext(context).getSession();
28
+ if (!session) {
29
+ logger.debug('No active session found for ' + context?.userId);
30
+ res.status(401).json({ error: 'No active session found' });
31
+ }
32
+ else {
33
+ next();
34
+ }
35
+ }
36
+ };
37
+ }
@@ -1,10 +1,17 @@
1
+ import { Logger } from 'pino';
1
2
  type EventListener = (...args: unknown[]) => void;
2
3
  export interface Notifier {
3
4
  on(event: string, listener: EventListener): void;
4
5
  emit(event: string, ...args: unknown[]): boolean;
5
6
  removeListener(event: string, listenerToRemove: EventListener): void;
6
7
  }
7
- export interface NotificationService {
8
+ interface INotificationService {
9
+ getNotifier(notifierId: string): Notifier;
10
+ }
11
+ export declare class NotificationService implements INotificationService {
12
+ private logger;
13
+ private notifiers;
14
+ constructor(logger: Logger);
8
15
  getNotifier(notifierId: string): Notifier;
9
16
  }
10
17
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationService.d.ts","sourceRoot":"","sources":["../../src/notification/NotificationService.ts"],"names":[],"mappings":"AAKA,KAAK,aAAa,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;AACjD,MAAM,WAAW,QAAQ;IACrB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAEhD,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAEhD,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,aAAa,GAAG,IAAI,CAAA;CACvE;AAED,MAAM,WAAW,mBAAmB;IAChC,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,CAAA;CAC5C"}
1
+ {"version":3,"file":"NotificationService.d.ts","sourceRoot":"","sources":["../../src/notification/NotificationService.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAE7B,KAAK,aAAa,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;AACjD,MAAM,WAAW,QAAQ;IACrB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAEhD,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAEhD,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,aAAa,GAAG,IAAI,CAAA;CACvE;AAED,UAAU,oBAAoB;IAC1B,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,CAAA;CAC5C;AAED,qBAAa,mBAAoB,YAAW,oBAAoB;IAGhD,OAAO,CAAC,MAAM;IAF1B,OAAO,CAAC,SAAS,CAAmC;gBAEhC,MAAM,EAAE,MAAM;IAElC,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ;CAoB5C"}
@@ -2,4 +2,25 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  // TODO: make events type-of dApp methods (perhaps just )
4
4
  // Support event-driven notifications. We represent a notifier with a generic interface to support node and browser implementations.
5
- export {};
5
+ import EventEmitter from 'events';
6
+ export class NotificationService {
7
+ constructor(logger) {
8
+ this.logger = logger;
9
+ this.notifiers = new Map();
10
+ }
11
+ getNotifier(notifierId) {
12
+ const logger = this.logger;
13
+ let notifier = this.notifiers.get(notifierId);
14
+ if (!notifier) {
15
+ notifier = new EventEmitter();
16
+ // Wrap all events to log with pino
17
+ const originalEmit = notifier.emit;
18
+ notifier.emit = function (event, ...args) {
19
+ logger.debug({ event, args }, `Notifier emitted event: ${event} for ${notifierId}`);
20
+ return originalEmit.apply(this, [event, ...args]);
21
+ };
22
+ this.notifiers.set(notifierId, notifier);
23
+ }
24
+ return notifier;
25
+ }
26
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/user-api/controller.ts"],"names":[],"mappings":"AAyBA,OAAO,EACH,KAAK,EAIR,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAC5E,OAAO,EAGH,WAAW,EAId,MAAM,kCAAkC,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,EACH,sBAAsB,EACtB,eAAe,EAClB,MAAM,kCAAkC,CAAA;AAazC,KAAK,uBAAuB,GAAG,OAAO,CAClC,MAAM,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAClD,CAAA;AAED,eAAO,MAAM,cAAc,GACvB,YAAY,UAAU,EACtB,SAAS,MAAM,EACf,OAAO,KAAK,EACZ,qBAAqB,mBAAmB,EACxC,aAAa,WAAW,GAAG,SAAS,EACpC,SAAS,uBAAuB,EAChC,SAAS,MAAM;;;;;;;;;;;;;;;;;;;CAgpBlB,CAAA"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/user-api/controller.ts"],"names":[],"mappings":"AAyBA,OAAO,EACH,KAAK,EAIR,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAC5E,OAAO,EAGH,WAAW,EAId,MAAM,kCAAkC,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,EACH,sBAAsB,EACtB,eAAe,EAClB,MAAM,kCAAkC,CAAA;AAczC,KAAK,uBAAuB,GAAG,OAAO,CAClC,MAAM,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAClD,CAAA;AAED,eAAO,MAAM,cAAc,GACvB,YAAY,UAAU,EACtB,SAAS,MAAM,EACf,OAAO,KAAK,EACZ,qBAAqB,mBAAmB,EACxC,aAAa,WAAW,GAAG,SAAS,EACpC,SAAS,uBAAuB,EAChC,SAAS,MAAM;;;;;;;;;;;;;;;;;;;CAorBlB,CAAA"}
@@ -9,6 +9,7 @@ import { SigningProvider, } from '@canton-network/core-signing-lib';
9
9
  import { PartyAllocationService, } from '../ledger/party-allocation-service.js';
10
10
  import { WalletSyncService } from '../ledger/wallet-sync-service.js';
11
11
  import { networkStatus, ledgerPrepareParams, } from '../utils.js';
12
+ import { v4 } from 'uuid';
12
13
  export const userController = (kernelInfo, userUrl, store, notificationService, authContext, drivers, _logger) => {
13
14
  const logger = _logger.child({ component: 'user-controller' });
14
15
  return buildController({
@@ -244,7 +245,7 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
244
245
  throw new Error('Failed to sign transaction: ' +
245
246
  JSON.stringify(signature));
246
247
  }
247
- // Get existing transaction to preserve createdAt if it exists
248
+ // Get existing transaction to preserve createdAt and origin if they exist
248
249
  const existingTx = await store.getTransaction(commandId);
249
250
  const now = new Date();
250
251
  const signedTx = {
@@ -252,6 +253,7 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
252
253
  status: 'signed',
253
254
  preparedTransaction,
254
255
  preparedTransactionHash,
256
+ origin: existingTx?.origin ?? null,
255
257
  ...(existingTx?.createdAt && {
256
258
  createdAt: existingTx.createdAt,
257
259
  }),
@@ -310,7 +312,8 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
310
312
  return res;
311
313
  }
312
314
  catch (error) {
313
- throw new Error('Failed to submit transaction: ' + error);
315
+ logger.error(error, 'Failed to submit transaction');
316
+ throw error;
314
317
  }
315
318
  }
316
319
  case SigningProvider.WALLET_KERNEL: {
@@ -344,6 +347,7 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
344
347
  preparedTransaction: transaction.preparedTransaction,
345
348
  preparedTransactionHash: transaction.preparedTransactionHash,
346
349
  payload: result,
350
+ origin: transaction.origin ?? null,
347
351
  ...(transaction.createdAt && {
348
352
  createdAt: transaction.createdAt,
349
353
  }),
@@ -361,7 +365,10 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
361
365
  },
362
366
  addSession: async function (params) {
363
367
  try {
368
+ const newSessionId = v4();
369
+ logger.info(`Adding session with ID ${newSessionId} for network ${params.networkId}`);
364
370
  await store.setSession({
371
+ id: newSessionId,
365
372
  network: params.networkId,
366
373
  accessToken: authContext?.accessToken || '',
367
374
  });
@@ -377,16 +384,27 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
377
384
  });
378
385
  const status = await networkStatus(ledgerClient);
379
386
  notifier.emit('onConnected', {
380
- status: {
381
- kernel: kernelInfo,
382
- isConnected: true,
383
- isNetworkConnected: status.isConnected,
384
- networkReason: status.reason ? status.reason : 'OK',
387
+ kernel: {
388
+ ...kernelInfo,
389
+ userUrl: `${userUrl}/login/`,
390
+ },
391
+ isConnected: true,
392
+ isNetworkConnected: status.isConnected,
393
+ networkReason: status.reason ? status.reason : 'OK',
394
+ network: {
385
395
  networkId: network.id,
396
+ ledgerApi: {
397
+ baseUrl: network.ledgerApi.baseUrl,
398
+ },
399
+ },
400
+ session: {
401
+ id: newSessionId,
402
+ accessToken: accessToken,
403
+ userId: userId,
386
404
  },
387
- sessionToken: accessToken,
388
405
  });
389
406
  return Promise.resolve({
407
+ id: newSessionId,
390
408
  accessToken,
391
409
  network,
392
410
  idp,
@@ -429,6 +447,7 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
429
447
  return {
430
448
  sessions: [
431
449
  {
450
+ id: session.id,
432
451
  network,
433
452
  idp: idp,
434
453
  accessToken: authContext.accessToken,
@@ -447,11 +466,17 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
447
466
  getUserAccessToken: async () => authContext.accessToken,
448
467
  getAdminAccessToken: async () => authContext.accessToken,
449
468
  };
469
+ const partyAllocator = new PartyAllocationService({
470
+ synchronizerId: network.synchronizerId,
471
+ accessTokenProvider: userAccessTokenProvider,
472
+ httpLedgerUrl: network.ledgerApi.baseUrl,
473
+ logger,
474
+ });
450
475
  const service = new WalletSyncService(store, new LedgerClient({
451
476
  baseUrl: new URL(network.ledgerApi.baseUrl),
452
477
  logger,
453
478
  accessTokenProvider: userAccessTokenProvider,
454
- }), authContext, logger);
479
+ }), authContext, logger, drivers, partyAllocator);
455
480
  const result = await service.syncWallets();
456
481
  if (result.added.length === 0 && result.removed.length === 0) {
457
482
  return result;
@@ -474,6 +499,9 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
474
499
  payload: transaction.payload
475
500
  ? JSON.stringify(transaction.payload)
476
501
  : '',
502
+ ...(transaction.origin !== null && {
503
+ origin: transaction.origin,
504
+ }),
477
505
  ...(transaction.createdAt && {
478
506
  createdAt: transaction.createdAt.toISOString(),
479
507
  }),
@@ -492,6 +520,9 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
492
520
  payload: transaction.payload
493
521
  ? JSON.stringify(transaction.payload)
494
522
  : '',
523
+ ...(transaction.origin !== null && {
524
+ origin: transaction.origin,
525
+ }),
495
526
  ...(transaction.createdAt && {
496
527
  createdAt: transaction.createdAt.toISOString(),
497
528
  }),