@canton-network/wallet-gateway-remote 0.1.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 (96) hide show
  1. package/README.md +25 -0
  2. package/dist/auth/jwt-auth-service.d.ts +11 -0
  3. package/dist/auth/jwt-auth-service.d.ts.map +1 -0
  4. package/dist/auth/jwt-auth-service.js +50 -0
  5. package/dist/config/Config.d.ts +590 -0
  6. package/dist/config/Config.d.ts.map +1 -0
  7. package/dist/config/Config.js +19 -0
  8. package/dist/config/Config.test.d.ts +2 -0
  9. package/dist/config/Config.test.d.ts.map +1 -0
  10. package/dist/config/Config.test.js +19 -0
  11. package/dist/config/ConfigUtils.d.ts +5 -0
  12. package/dist/config/ConfigUtils.d.ts.map +1 -0
  13. package/dist/config/ConfigUtils.js +14 -0
  14. package/dist/dapp-api/controller.d.ts +18 -0
  15. package/dist/dapp-api/controller.d.ts.map +1 -0
  16. package/dist/dapp-api/controller.js +101 -0
  17. package/dist/dapp-api/rpc-gen/index.d.ts +36 -0
  18. package/dist/dapp-api/rpc-gen/index.d.ts.map +1 -0
  19. package/dist/dapp-api/rpc-gen/index.js +17 -0
  20. package/dist/dapp-api/rpc-gen/typings.d.ts +337 -0
  21. package/dist/dapp-api/rpc-gen/typings.d.ts.map +1 -0
  22. package/dist/dapp-api/rpc-gen/typings.js +3 -0
  23. package/dist/dapp-api/server.d.ts +6 -0
  24. package/dist/dapp-api/server.d.ts.map +1 -0
  25. package/dist/dapp-api/server.js +62 -0
  26. package/dist/dapp-api/server.test.d.ts +2 -0
  27. package/dist/dapp-api/server.test.d.ts.map +1 -0
  28. package/dist/dapp-api/server.test.js +45 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +30 -0
  32. package/dist/init.d.ts +9 -0
  33. package/dist/init.d.ts.map +1 -0
  34. package/dist/init.js +77 -0
  35. package/dist/ledger/party-allocation-service.d.ts +34 -0
  36. package/dist/ledger/party-allocation-service.d.ts.map +1 -0
  37. package/dist/ledger/party-allocation-service.js +50 -0
  38. package/dist/ledger/party-allocation-service.test.d.ts +2 -0
  39. package/dist/ledger/party-allocation-service.test.d.ts.map +1 -0
  40. package/dist/ledger/party-allocation-service.test.js +85 -0
  41. package/dist/ledger/wallet-sync-service.d.ts +18 -0
  42. package/dist/ledger/wallet-sync-service.d.ts.map +1 -0
  43. package/dist/ledger/wallet-sync-service.js +90 -0
  44. package/dist/middleware/jsonRpcHandler.d.ts +9 -0
  45. package/dist/middleware/jsonRpcHandler.d.ts.map +1 -0
  46. package/dist/middleware/jsonRpcHandler.js +102 -0
  47. package/dist/middleware/jwtAuth.d.ts +5 -0
  48. package/dist/middleware/jwtAuth.d.ts.map +1 -0
  49. package/dist/middleware/jwtAuth.js +20 -0
  50. package/dist/middleware/rateLimit.d.ts +2 -0
  51. package/dist/middleware/rateLimit.d.ts.map +1 -0
  52. package/dist/middleware/rateLimit.js +9 -0
  53. package/dist/notification/NotificationService.d.ts +11 -0
  54. package/dist/notification/NotificationService.d.ts.map +1 -0
  55. package/dist/notification/NotificationService.js +5 -0
  56. package/dist/user-api/controller.d.ts +23 -0
  57. package/dist/user-api/controller.d.ts.map +1 -0
  58. package/dist/user-api/controller.js +336 -0
  59. package/dist/user-api/rpc-gen/index.d.ts +42 -0
  60. package/dist/user-api/rpc-gen/index.d.ts.map +1 -0
  61. package/dist/user-api/rpc-gen/index.js +19 -0
  62. package/dist/user-api/rpc-gen/typings.d.ts +297 -0
  63. package/dist/user-api/rpc-gen/typings.d.ts.map +1 -0
  64. package/dist/user-api/rpc-gen/typings.js +3 -0
  65. package/dist/user-api/server.d.ts +7 -0
  66. package/dist/user-api/server.d.ts.map +1 -0
  67. package/dist/user-api/server.js +20 -0
  68. package/dist/user-api/server.test.d.ts +2 -0
  69. package/dist/user-api/server.test.d.ts.map +1 -0
  70. package/dist/user-api/server.test.js +38 -0
  71. package/dist/web/frontend/404/index.html +20 -0
  72. package/dist/web/frontend/approve/index.html +23 -0
  73. package/dist/web/frontend/assets/404-BHkjVWlW.js +16 -0
  74. package/dist/web/frontend/assets/approve-lRsfAWmm.js +157 -0
  75. package/dist/web/frontend/assets/callback-QrXhW3mX.js +1 -0
  76. package/dist/web/frontend/assets/handle-errors-BcwHAkCd.js +1 -0
  77. package/dist/web/frontend/assets/index-BknZMPaI.css +5 -0
  78. package/dist/web/frontend/assets/index-BxdGgjHv.js +1 -0
  79. package/dist/web/frontend/assets/index-D-GexOrJ.js +697 -0
  80. package/dist/web/frontend/assets/index-TZrNw7dA.css +1 -0
  81. package/dist/web/frontend/assets/login-HUymBqli.js +159 -0
  82. package/dist/web/frontend/assets/networks-BZihbVwK.js +221 -0
  83. package/dist/web/frontend/assets/rpc-client-CCUlY3sp.js +1 -0
  84. package/dist/web/frontend/assets/state-DKGJ6EmM.js +9 -0
  85. package/dist/web/frontend/assets/state-manager-BNW0y5PZ.js +23 -0
  86. package/dist/web/frontend/assets/wallets-BoN6kUME.js +214 -0
  87. package/dist/web/frontend/callback/index.html +13 -0
  88. package/dist/web/frontend/icon.png +0 -0
  89. package/dist/web/frontend/index.html +19 -0
  90. package/dist/web/frontend/login/index.html +21 -0
  91. package/dist/web/frontend/networks/index.html +20 -0
  92. package/dist/web/frontend/wallets/index.html +22 -0
  93. package/dist/web/server.d.ts +2 -0
  94. package/dist/web/server.d.ts.map +1 -0
  95. package/dist/web/server.js +30 -0
  96. package/package.json +86 -0
@@ -0,0 +1,62 @@
1
+ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import express from 'express';
4
+ import { dappController } from './controller.js';
5
+ import { pino } from 'pino';
6
+ import { jsonRpcHandler } from '../middleware/jsonRpcHandler.js';
7
+ import { jwtAuth } from '../middleware/jwtAuth.js';
8
+ import { rpcRateLimit } from '../middleware/rateLimit.js';
9
+ import cors from 'cors';
10
+ import { createServer } from 'http';
11
+ import { Server } from 'socket.io';
12
+ const logger = pino({ name: 'main', level: 'debug' });
13
+ export const dapp = (kernelInfo, notificationService, authService, store) => {
14
+ const app = express();
15
+ app.use(cors());
16
+ app.use(express.json());
17
+ app.use('/rpc', rpcRateLimit, jwtAuth(authService, logger), (req, res, next) => jsonRpcHandler({
18
+ controller: dappController(kernelInfo, store.withAuthContext(req.authContext), notificationService, logger, req.authContext),
19
+ logger,
20
+ })(req, res, next));
21
+ const server = createServer(app);
22
+ const io = new Server(server, {
23
+ cors: {
24
+ origin: '*',
25
+ methods: ['GET', 'POST'],
26
+ },
27
+ });
28
+ io.on('connection', (socket) => {
29
+ logger.info('Socket.io client connected');
30
+ let notifier = undefined;
31
+ const onAccountsChanged = (...event) => {
32
+ io.emit('accountsChanged', ...event);
33
+ };
34
+ const onConnected = (...event) => {
35
+ io.emit('onConnected', ...event);
36
+ };
37
+ const onTxChanged = (...event) => {
38
+ io.emit('txChanged', ...event);
39
+ };
40
+ authService
41
+ .verifyToken(socket.handshake.auth.token)
42
+ .then((authContext) => {
43
+ const userId = authContext?.userId;
44
+ if (!userId) {
45
+ return;
46
+ }
47
+ notifier = notificationService.getNotifier(userId);
48
+ notifier.on('accountsChanged', onAccountsChanged);
49
+ notifier.on('onConnected', onConnected);
50
+ notifier.on('txChanged', onTxChanged);
51
+ });
52
+ socket.on('disconnect', () => {
53
+ logger.info('Socket.io client disconnected');
54
+ if (notifier) {
55
+ notifier.removeListener('accountsChanged', onAccountsChanged);
56
+ notifier.removeListener('onConnected', onConnected);
57
+ notifier.removeListener('txChanged', onTxChanged);
58
+ }
59
+ });
60
+ });
61
+ return server;
62
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=server.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.test.d.ts","sourceRoot":"","sources":["../../src/dapp-api/server.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,45 @@
1
+ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { expect, test, jest } from '@jest/globals';
4
+ import request from 'supertest';
5
+ import { dapp } from './server.js';
6
+ import { StoreInternal } from '@canton-network/core-wallet-store-inmemory';
7
+ import { ConfigUtils } from '../config/ConfigUtils.js';
8
+ import { pino } from 'pino';
9
+ import { sink } from 'pino-test';
10
+ const authService = {
11
+ verifyToken: async () => {
12
+ return new Promise((resolve) => resolve({ userId: 'user123', accessToken: 'token123' }));
13
+ },
14
+ };
15
+ const configPath = '../test/config.json';
16
+ const config = ConfigUtils.loadConfigFile(configPath);
17
+ const store = new StoreInternal(config.store, pino(sink()));
18
+ const notificationService = {
19
+ getNotifier: jest.fn().mockReturnValue({
20
+ on: jest.fn(),
21
+ emit: jest.fn(),
22
+ removeListener: jest.fn(),
23
+ }),
24
+ };
25
+ test('call connect rpc', async () => {
26
+ const response = await request(dapp(config.kernel, notificationService, authService, store))
27
+ .post('/rpc')
28
+ .send({ jsonrpc: '2.0', id: 0, method: 'connect', params: [] })
29
+ .set('Accept', 'application/json');
30
+ expect(response.statusCode).toBe(200);
31
+ expect(response.body).toEqual({
32
+ id: 0,
33
+ jsonrpc: '2.0',
34
+ result: {
35
+ kernel: {
36
+ id: 'remote-da',
37
+ clientType: 'remote',
38
+ url: 'http://localhost:3008/rpc',
39
+ userUrl: 'http://localhost:3002',
40
+ },
41
+ isConnected: false,
42
+ userUrl: 'http://localhost:3002/login/',
43
+ },
44
+ });
45
+ });
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { Option, Command } from '@commander-js/extra-typings';
5
+ import { initialize } from './init.js';
6
+ import { createCLI } from '@canton-network/core-wallet-store-sql';
7
+ import { ConfigUtils } from './config/ConfigUtils.js';
8
+ const program = new Command()
9
+ .name('wallet-gateway-remote')
10
+ .description('Run a remotely hosted Wallet Gateway')
11
+ .option('-c, --config <path>', 'set config path', '../test/config.json')
12
+ .addOption(new Option('-f, --log-format <format>', 'set log format')
13
+ .choices(['json', 'pretty'])
14
+ .default('json'))
15
+ .addOption(new Option('-s, --store-type <type>', 'set store type')
16
+ .choices(['sqlite', 'postgres'])
17
+ .default('sqlite'))
18
+ .action((opts) => {
19
+ // Initialize the database with the provided config
20
+ initialize(opts);
21
+ });
22
+ // Parse only the options (without executing commands) to get config path
23
+ program.parseOptions(process.argv);
24
+ const options = program.opts();
25
+ const config = ConfigUtils.loadConfigFile(options.config);
26
+ // Add the `db` command now, before final parse
27
+ const cli = createCLI(config.store);
28
+ program.addCommand(cli.name('db'));
29
+ // Now parse normally for execution/help
30
+ program.parseAsync(process.argv);
package/dist/init.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export declare function initialize(opts: {
2
+ config: string;
3
+ logFormat: 'pretty' | 'json';
4
+ }): Promise<{
5
+ dAppServer: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
6
+ userServer: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
7
+ webServer: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
8
+ }>;
9
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAmDA,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACnC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,QAAQ,GAAG,MAAM,CAAA;CAC/B;;;;GA4EA"}
package/dist/init.js ADDED
@@ -0,0 +1,77 @@
1
+ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { dapp } from './dapp-api/server.js';
4
+ import { user } from './user-api/server.js';
5
+ import { web } from './web/server.js';
6
+ import { pino } from 'pino';
7
+ import ViteExpress from 'vite-express';
8
+ import { StoreSql, connection } from '@canton-network/core-wallet-store-sql';
9
+ import { ConfigUtils } from './config/ConfigUtils.js';
10
+ import EventEmitter from 'events';
11
+ import { SigningProvider } from '@canton-network/core-signing-lib';
12
+ import { ParticipantSigningDriver } from '@canton-network/core-signing-participant';
13
+ import { InternalSigningDriver } from '@canton-network/core-signing-internal';
14
+ import { jwtAuthService } from './auth/jwt-auth-service.js';
15
+ import path, { dirname } from 'path';
16
+ import { fileURLToPath } from 'url';
17
+ import express from 'express';
18
+ const dAppPort = Number(process.env.DAPP_API_PORT) || 3008;
19
+ const userPort = Number(process.env.USER_API_PORT) || 3001;
20
+ const webPort = Number(process.env.WEB_PORT) || 3002;
21
+ class NotificationService {
22
+ constructor(logger) {
23
+ this.logger = logger;
24
+ this.notifiers = new Map();
25
+ }
26
+ getNotifier(notifierId) {
27
+ const logger = this.logger;
28
+ let notifier = this.notifiers.get(notifierId);
29
+ if (!notifier) {
30
+ notifier = new EventEmitter();
31
+ // Wrap all events to log with pino
32
+ const originalEmit = notifier.emit;
33
+ notifier.emit = function (event, ...args) {
34
+ logger.debug({ event, args }, `Notifier emitted event: ${event}`);
35
+ return originalEmit.apply(this, [event, ...args]);
36
+ };
37
+ this.notifiers.set(notifierId, notifier);
38
+ }
39
+ return notifier;
40
+ }
41
+ }
42
+ export async function initialize(opts) {
43
+ const logger = pino({
44
+ name: 'main',
45
+ level: 'debug',
46
+ ...(opts.logFormat === 'pretty'
47
+ ? {
48
+ transport: {
49
+ target: 'pino-pretty',
50
+ },
51
+ }
52
+ : {}),
53
+ });
54
+ const notificationService = new NotificationService(logger);
55
+ // TODO: make the default config path point to ${PWD}/config.json
56
+ const defaultConfig = path.join(dirname(fileURLToPath(import.meta.url)), '..', 'test', 'config.json');
57
+ const configPath = opts.config || defaultConfig;
58
+ const config = ConfigUtils.loadConfigFile(configPath);
59
+ const store = new StoreSql(connection(config.store), logger);
60
+ const authService = jwtAuthService(store, logger);
61
+ const drivers = {
62
+ [SigningProvider.PARTICIPANT]: new ParticipantSigningDriver(),
63
+ [SigningProvider.WALLET_KERNEL]: new InternalSigningDriver(),
64
+ };
65
+ const dAppServer = dapp(config.kernel, notificationService, authService, store).listen(dAppPort, () => {
66
+ logger.info(`dApp Server running at http://localhost:${dAppPort}`);
67
+ });
68
+ const userServer = user(config.kernel, notificationService, authService, drivers, store).listen(userPort, () => {
69
+ logger.info(`User Server running at http://localhost:${userPort}`);
70
+ });
71
+ const webServer = process.env.NODE_ENV === 'development'
72
+ ? ViteExpress.listen(web, webPort, () => logger.info(`Web server running at http://localhost:${webPort}`))
73
+ : web
74
+ .use(express.static(path.resolve(dirname(fileURLToPath(import.meta.url)), '../dist/web/frontend')))
75
+ .listen(webPort, () => logger.info(`Web server running at http://localhost:${webPort}`));
76
+ return { dAppServer, userServer, webServer };
77
+ }
@@ -0,0 +1,34 @@
1
+ import { Logger } from 'pino';
2
+ export type AllocatedParty = {
3
+ partyId: string;
4
+ hint: string;
5
+ namespace: string;
6
+ };
7
+ type SigningCbFn = (hash: string) => Promise<string>;
8
+ /**
9
+ * This service provides an abstraction for Canton party allocation that seamlessly handles both internal and external parties.
10
+ */
11
+ export declare class PartyAllocationService {
12
+ private synchronizerId;
13
+ private logger;
14
+ private ledgerClient;
15
+ constructor(synchronizerId: string, adminToken: string, httpLedgerUrl: string, logger: Logger);
16
+ /**
17
+ * Allocates an internal participant party for a user.
18
+ * @param userId The ID of the user.
19
+ * @param hint A hint for the party ID.
20
+ */
21
+ allocateParty(userId: string, hint: string): Promise<AllocatedParty>;
22
+ /**
23
+ * Allocates an externally signed party for a user.
24
+ * @param userId The ID of the user.
25
+ * @param hint A hint for the party ID.
26
+ * @param publicKey The public key of the user.
27
+ * @param signingCallback A callback function that asynchronously signs the onboarding request hash.
28
+ */
29
+ allocateParty(userId: string, hint: string, publicKey: string, signingCallback: SigningCbFn): Promise<AllocatedParty>;
30
+ private allocateInternalParty;
31
+ private allocateExternalParty;
32
+ }
33
+ export {};
34
+ //# sourceMappingURL=party-allocation-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"party-allocation-service.d.ts","sourceRoot":"","sources":["../../src/ledger/party-allocation-service.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAE7B,MAAM,MAAM,cAAc,GAAG;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,KAAK,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;AAEpD;;GAEG;AACH,qBAAa,sBAAsB;IAI3B,OAAO,CAAC,cAAc;IAGtB,OAAO,CAAC,MAAM;IANlB,OAAO,CAAC,YAAY,CAAc;gBAGtB,cAAc,EAAE,MAAM,EAC9B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACb,MAAM,EAAE,MAAM;IAS1B;;;;OAIG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAE1E;;;;;;OAMG;IACG,aAAa,CACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,WAAW,GAC7B,OAAO,CAAC,cAAc,CAAC;YAoBZ,qBAAqB;YAqBrB,qBAAqB;CAmCtC"}
@@ -0,0 +1,50 @@
1
+ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { LedgerClient, TopologyWriteService, } from '@canton-network/core-ledger-client';
4
+ /**
5
+ * This service provides an abstraction for Canton party allocation that seamlessly handles both internal and external parties.
6
+ */
7
+ export class PartyAllocationService {
8
+ constructor(synchronizerId, adminToken, httpLedgerUrl, logger) {
9
+ this.synchronizerId = synchronizerId;
10
+ this.logger = logger;
11
+ this.ledgerClient = new LedgerClient(new URL(httpLedgerUrl), adminToken, this.logger);
12
+ }
13
+ async allocateParty(userId, hint, publicKey, signingCallback) {
14
+ if (publicKey !== undefined && signingCallback !== undefined) {
15
+ return this.allocateExternalParty(userId, hint, publicKey, signingCallback);
16
+ }
17
+ else {
18
+ return this.allocateInternalParty(userId, hint);
19
+ }
20
+ }
21
+ async allocateInternalParty(userId, hint) {
22
+ const { participantId: namespace } = await this.ledgerClient.get('/v2/parties/participant-id');
23
+ const res = await this.ledgerClient.post('/v2/parties', {
24
+ partyIdHint: hint,
25
+ identityProviderId: '',
26
+ });
27
+ if (!res.partyDetails?.party) {
28
+ throw new Error('Failed to allocate party');
29
+ }
30
+ await this.ledgerClient.grantUserRights(userId, res.partyDetails.party);
31
+ return { hint, namespace, partyId: res.partyDetails.party };
32
+ }
33
+ async allocateExternalParty(userId, hint, publicKey, signingCallback) {
34
+ const namespace = TopologyWriteService.createFingerprintFromKey(publicKey);
35
+ const transactions = await this.ledgerClient.generateTopology(this.synchronizerId, publicKey, hint);
36
+ const signature = await signingCallback(transactions.multiHash);
37
+ const res = await this.ledgerClient.allocateExternalParty(this.synchronizerId, transactions.topologyTransactions.map((transaction) => ({
38
+ transaction,
39
+ })), [
40
+ {
41
+ format: 'SIGNATURE_FORMAT_CONCAT',
42
+ signature: signature,
43
+ signedBy: namespace,
44
+ signingAlgorithmSpec: 'SIGNING_ALGORITHM_SPEC_ED25519',
45
+ },
46
+ ]);
47
+ await this.ledgerClient.grantUserRights(userId, res.partyId);
48
+ return { hint, partyId: res.partyId, namespace };
49
+ }
50
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=party-allocation-service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"party-allocation-service.test.d.ts","sourceRoot":"","sources":["../../src/ledger/party-allocation-service.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,85 @@
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 } from '@jest/globals';
4
+ import { pino } from 'pino';
5
+ import { sink } from 'pino-test';
6
+ const mockLedgerGet = jest.fn();
7
+ const mockLedgerPost = jest.fn();
8
+ const mockLedgerGrantUserRights = jest.fn();
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ const MockTopologyWriteService = jest.fn();
11
+ // Add static method to the mock class
12
+ MockTopologyWriteService.createFingerprintFromKey = jest
13
+ .fn()
14
+ .mockReturnValue('mypublickey');
15
+ jest.unstable_mockModule('@canton-network/core-ledger-client', () => ({
16
+ Signature: jest.fn(),
17
+ SignatureFormat: jest.fn(),
18
+ SigningAlgorithmSpec: jest.fn(),
19
+ MultiTransactionSignatures: jest.fn(),
20
+ SignedTopologyTransaction: jest.fn(),
21
+ LedgerClient: jest.fn().mockImplementation(() => {
22
+ return {
23
+ get: mockLedgerGet,
24
+ post: mockLedgerPost,
25
+ grantUserRights: mockLedgerGrantUserRights,
26
+ generateTopology: jest.fn().mockResolvedValue({
27
+ partyId: 'party2::mypublickey',
28
+ publicKeyFingerprint: 'mypublickey',
29
+ topologyTransactions: ['tx1'],
30
+ multiHash: 'combinedHash',
31
+ }),
32
+ allocateExternalParty: jest
33
+ .fn()
34
+ .mockResolvedValue({ partyId: 'party2::mypublickey' }),
35
+ };
36
+ }),
37
+ TopologyWriteService: MockTopologyWriteService,
38
+ }));
39
+ describe('PartyAllocationService', () => {
40
+ const network = {
41
+ name: 'test',
42
+ chainId: 'chain-id',
43
+ synchronizerId: 'sync-id',
44
+ description: 'desc',
45
+ ledgerApi: {
46
+ baseUrl: 'http://ledger',
47
+ },
48
+ auth: {
49
+ identityProviderId: 'idp',
50
+ type: 'implicit',
51
+ issuer: 'http://idp',
52
+ configUrl: 'http://idp/.well-known/openid-configuration',
53
+ audience: 'aud',
54
+ scope: 'scope',
55
+ clientId: 'cid',
56
+ admin: { clientId: 'cid', clientSecret: 'secret' },
57
+ },
58
+ };
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ let service;
61
+ beforeEach(async () => {
62
+ const mockLogger = pino(sink());
63
+ const pas = await import('./party-allocation-service.js');
64
+ service = new pas.PartyAllocationService(network.synchronizerId, 'admin.jwt', network.ledgerApi.baseUrl, mockLogger);
65
+ });
66
+ it('allocates an internal party', async () => {
67
+ mockLedgerGet.mockResolvedValueOnce({ participantId: 'participantid' });
68
+ mockLedgerPost.mockResolvedValueOnce({
69
+ partyDetails: { party: 'party1::participantid' },
70
+ });
71
+ await expect(service.allocateParty('user1', 'party1')).resolves.toStrictEqual({
72
+ hint: 'party1',
73
+ partyId: 'party1::participantid',
74
+ namespace: 'participantid',
75
+ });
76
+ });
77
+ it('allocates an external party', async () => {
78
+ const publicKey = 'mypublickey';
79
+ await expect(service.allocateParty('user1', 'party2', publicKey, async () => 'mysignedhash')).resolves.toStrictEqual({
80
+ hint: 'party2',
81
+ partyId: `party2::${publicKey}`,
82
+ namespace: publicKey,
83
+ });
84
+ });
85
+ });
@@ -0,0 +1,18 @@
1
+ import { LedgerClient } from '@canton-network/core-ledger-client';
2
+ import { AuthContext } from '@canton-network/core-wallet-auth';
3
+ import { Store, Wallet } from '@canton-network/core-wallet-store';
4
+ import { Logger } from 'pino';
5
+ export type WalletSyncReport = {
6
+ added: Wallet[];
7
+ removed: Wallet[];
8
+ };
9
+ export declare class WalletSyncService {
10
+ private store;
11
+ private ledgerClient;
12
+ private authContext;
13
+ private logger;
14
+ constructor(store: Store, ledgerClient: LedgerClient, authContext: AuthContext, logger: Logger);
15
+ run(timeoutMs: number): Promise<void>;
16
+ syncWallets(): Promise<WalletSyncReport>;
17
+ }
18
+ //# sourceMappingURL=wallet-sync-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wallet-sync-service.d.ts","sourceRoot":"","sources":["../../src/ledger/wallet-sync-service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAA;AACjE,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;CA6FjD"}
@@ -0,0 +1,90 @@
1
+ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export class WalletSyncService {
4
+ constructor(store, ledgerClient, authContext, logger) {
5
+ this.store = store;
6
+ this.ledgerClient = ledgerClient;
7
+ this.authContext = authContext;
8
+ this.logger = logger;
9
+ }
10
+ async run(timeoutMs) {
11
+ this.logger.info(`Starting wallet sync service with ${timeoutMs}ms interval`);
12
+ while (true) {
13
+ await this.syncWallets();
14
+ await new Promise((res) => setTimeout(res, timeoutMs));
15
+ }
16
+ }
17
+ async syncWallets() {
18
+ this.logger.info('Starting wallet sync...');
19
+ try {
20
+ const network = await this.store.getCurrentNetwork();
21
+ this.logger.info(network, 'Current network');
22
+ // Get existing parties from participant
23
+ const rights = await this.ledgerClient.get('/v2/users/{user-id}/rights', {
24
+ path: {
25
+ 'user-id': this.authContext.userId,
26
+ },
27
+ });
28
+ const partiesWithRights = new Map();
29
+ rights.rights?.forEach((right) => {
30
+ let party;
31
+ let rightType;
32
+ if ('CanActAs' in right.kind) {
33
+ party = right.kind.CanActAs.value.party;
34
+ rightType = 'CanActAs';
35
+ }
36
+ else if ('CanExecuteAs' in right.kind) {
37
+ party = right.kind.CanExecuteAs.value.party;
38
+ rightType = 'CanExecuteAs';
39
+ }
40
+ else if ('CanReadAs' in right.kind) {
41
+ party = right.kind.CanReadAs.value.party;
42
+ rightType = 'CanReadAs';
43
+ }
44
+ if (party !== undefined &&
45
+ rightType !== undefined &&
46
+ !partiesWithRights.has(party))
47
+ partiesWithRights.set(party, rightType);
48
+ });
49
+ this.logger.info(partiesWithRights, 'Found new parties to sync with Wallet Gateway');
50
+ // Add new Wallets given the found parties
51
+ const existingWallets = await this.store.getWallets();
52
+ this.logger.info(existingWallets, 'Existing wallets');
53
+ const existingPartyIdToSigningProvider = new Map(existingWallets.map((w) => [w.partyId, w.signingProviderId]));
54
+ const newParticipantWallets = Array.from(partiesWithRights.keys())
55
+ ?.filter((party) => !existingPartyIdToSigningProvider.has(party)
56
+ // todo: filter on idp id
57
+ )
58
+ .map((party) => {
59
+ const [hint, namespace] = party.split('::');
60
+ return {
61
+ primary: false,
62
+ partyId: party,
63
+ hint: hint,
64
+ publicKey: namespace,
65
+ namespace: namespace,
66
+ chainId: network.chainId,
67
+ signingProviderId: 'participant', // todo: determine based on partyDetails.isLocal
68
+ };
69
+ }) || [];
70
+ await Promise.all(newParticipantWallets.map((wallet) => this.store.addWallet(wallet)));
71
+ this.logger.info(newParticipantWallets, 'Created new wallets');
72
+ // Set primary wallet if none exists
73
+ const wallets = await this.store.getWallets();
74
+ const hasPrimary = wallets.some((w) => w.primary);
75
+ if (!hasPrimary && wallets.length > 0) {
76
+ this.store.setPrimaryWallet(wallets[0].partyId);
77
+ this.logger.info(`Set ${wallets[0].partyId} as primary wallet`);
78
+ }
79
+ this.logger.debug(wallets, 'Wallet sync completed.');
80
+ return {
81
+ added: newParticipantWallets,
82
+ removed: [],
83
+ };
84
+ }
85
+ catch (err) {
86
+ this.logger.error({ err }, 'Wallet sync failed.');
87
+ throw err;
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,9 @@
1
+ import { NextFunction, Request, Response } from 'express';
2
+ import { Logger } from 'pino';
3
+ interface JsonRpcHttpOptions<T> {
4
+ logger: Logger;
5
+ controller: T;
6
+ }
7
+ export declare const jsonRpcHandler: <T extends Record<string, (...args: any[]) => any>>({ controller, logger: _logger, }: JsonRpcHttpOptions<T>) => (req: Request, res: Response, next: NextFunction) => void;
8
+ export {};
9
+ //# sourceMappingURL=jsonRpcHandler.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,102 @@
1
+ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { JsonRpcError, rpcErrors, toHttpErrorCode, } from '@canton-network/core-rpc-errors';
4
+ import { ErrorResponse, JsonRpcRequest, jsonRpcResponse, } from '@canton-network/core-types';
5
+ /**
6
+ * Handles JSON-RPC errors and maps them to HTTP responses.
7
+ * @param error The error that occurred.
8
+ * @param id The JSON-RPC request ID.
9
+ * @param logger The logger instance.
10
+ * @param method The name of the JSON-RPC method being called.
11
+ * @returns A tuple containing the HTTP status code and the JSON-RPC response.
12
+ */
13
+ const handleRpcError = (error, id, logger, method) => {
14
+ const genericMessage = method
15
+ ? `Something went wrong while calling ${method}`
16
+ : 'Something went wrong';
17
+ let response = {
18
+ error: {
19
+ ...rpcErrors.internal(),
20
+ message: genericMessage,
21
+ data: error,
22
+ },
23
+ };
24
+ if (error instanceof JsonRpcError) {
25
+ response.error = error;
26
+ const httpCode = toHttpErrorCode(error.code);
27
+ return [httpCode, jsonRpcResponse(id, response)];
28
+ }
29
+ if (error instanceof Error) {
30
+ response.error.message = error.message;
31
+ }
32
+ else if (typeof error === 'string') {
33
+ response.error.message = error;
34
+ }
35
+ else if (ErrorResponse.safeParse(error).success) {
36
+ response = error;
37
+ }
38
+ else if (
39
+ // Check for a Ledger API error format
40
+ typeof error === 'object' &&
41
+ error !== null &&
42
+ 'cause' in error &&
43
+ 'code' in error) {
44
+ response.error.message = error.cause;
45
+ response.error.data = error;
46
+ }
47
+ const jsonResponse = jsonRpcResponse(id, response);
48
+ logger.error(jsonResponse, 'RPC response');
49
+ return [500, jsonResponse];
50
+ };
51
+ export const jsonRpcHandler =
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ ({ controller, logger: _logger, }) => {
54
+ const logger = _logger.child({ component: 'json-rpc-http' });
55
+ return (req, res, next) => {
56
+ if (req.method !== 'POST') {
57
+ next();
58
+ }
59
+ const parsed = JsonRpcRequest.safeParse(req.body);
60
+ if (!parsed.success) {
61
+ logger.error({
62
+ request: req.body,
63
+ error: parsed.error,
64
+ }, 'RPC request: Invalid request format');
65
+ const [status, response] = handleRpcError(rpcErrors.invalidRequest({
66
+ message: 'Invalid JSON-RPC request format',
67
+ }), null, logger);
68
+ res.status(status).json(response);
69
+ }
70
+ else {
71
+ const { method, params, id = null } = parsed.data;
72
+ logger.debug({
73
+ request: {
74
+ id: id,
75
+ method: method,
76
+ params: params,
77
+ authContext: req.authContext,
78
+ },
79
+ }, `RPC request: ${method}`);
80
+ const methodFn = controller[method];
81
+ if (!methodFn) {
82
+ const [status, response] = handleRpcError(rpcErrors.methodNotFound({
83
+ message: `Method ${method} not found`,
84
+ }), null, logger, method);
85
+ res.status(status).json(response);
86
+ }
87
+ // TODO: validate params match the expected schema for the method
88
+ methodFn(params)
89
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
+ .then((result) => {
91
+ const response = jsonRpcResponse(id, { result });
92
+ logger.debug(response, 'RPC response');
93
+ res.json(response);
94
+ })
95
+ .catch((error) => {
96
+ const [status, response] = handleRpcError(error, id, logger, method);
97
+ logger.error(response, 'RPC response');
98
+ res.status(status).json(response);
99
+ });
100
+ }
101
+ };
102
+ };