@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.
- package/README.md +25 -0
- package/dist/auth/jwt-auth-service.d.ts +11 -0
- package/dist/auth/jwt-auth-service.d.ts.map +1 -0
- package/dist/auth/jwt-auth-service.js +50 -0
- package/dist/config/Config.d.ts +590 -0
- package/dist/config/Config.d.ts.map +1 -0
- package/dist/config/Config.js +19 -0
- package/dist/config/Config.test.d.ts +2 -0
- package/dist/config/Config.test.d.ts.map +1 -0
- package/dist/config/Config.test.js +19 -0
- package/dist/config/ConfigUtils.d.ts +5 -0
- package/dist/config/ConfigUtils.d.ts.map +1 -0
- package/dist/config/ConfigUtils.js +14 -0
- package/dist/dapp-api/controller.d.ts +18 -0
- package/dist/dapp-api/controller.d.ts.map +1 -0
- package/dist/dapp-api/controller.js +101 -0
- package/dist/dapp-api/rpc-gen/index.d.ts +36 -0
- package/dist/dapp-api/rpc-gen/index.d.ts.map +1 -0
- package/dist/dapp-api/rpc-gen/index.js +17 -0
- package/dist/dapp-api/rpc-gen/typings.d.ts +337 -0
- package/dist/dapp-api/rpc-gen/typings.d.ts.map +1 -0
- package/dist/dapp-api/rpc-gen/typings.js +3 -0
- package/dist/dapp-api/server.d.ts +6 -0
- package/dist/dapp-api/server.d.ts.map +1 -0
- package/dist/dapp-api/server.js +62 -0
- package/dist/dapp-api/server.test.d.ts +2 -0
- package/dist/dapp-api/server.test.d.ts.map +1 -0
- package/dist/dapp-api/server.test.js +45 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/init.d.ts +9 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +77 -0
- package/dist/ledger/party-allocation-service.d.ts +34 -0
- package/dist/ledger/party-allocation-service.d.ts.map +1 -0
- package/dist/ledger/party-allocation-service.js +50 -0
- package/dist/ledger/party-allocation-service.test.d.ts +2 -0
- package/dist/ledger/party-allocation-service.test.d.ts.map +1 -0
- package/dist/ledger/party-allocation-service.test.js +85 -0
- package/dist/ledger/wallet-sync-service.d.ts +18 -0
- package/dist/ledger/wallet-sync-service.d.ts.map +1 -0
- package/dist/ledger/wallet-sync-service.js +90 -0
- package/dist/middleware/jsonRpcHandler.d.ts +9 -0
- package/dist/middleware/jsonRpcHandler.d.ts.map +1 -0
- package/dist/middleware/jsonRpcHandler.js +102 -0
- package/dist/middleware/jwtAuth.d.ts +5 -0
- package/dist/middleware/jwtAuth.d.ts.map +1 -0
- package/dist/middleware/jwtAuth.js +20 -0
- package/dist/middleware/rateLimit.d.ts +2 -0
- package/dist/middleware/rateLimit.d.ts.map +1 -0
- package/dist/middleware/rateLimit.js +9 -0
- package/dist/notification/NotificationService.d.ts +11 -0
- package/dist/notification/NotificationService.d.ts.map +1 -0
- package/dist/notification/NotificationService.js +5 -0
- package/dist/user-api/controller.d.ts +23 -0
- package/dist/user-api/controller.d.ts.map +1 -0
- package/dist/user-api/controller.js +336 -0
- package/dist/user-api/rpc-gen/index.d.ts +42 -0
- package/dist/user-api/rpc-gen/index.d.ts.map +1 -0
- package/dist/user-api/rpc-gen/index.js +19 -0
- package/dist/user-api/rpc-gen/typings.d.ts +297 -0
- package/dist/user-api/rpc-gen/typings.d.ts.map +1 -0
- package/dist/user-api/rpc-gen/typings.js +3 -0
- package/dist/user-api/server.d.ts +7 -0
- package/dist/user-api/server.d.ts.map +1 -0
- package/dist/user-api/server.js +20 -0
- package/dist/user-api/server.test.d.ts +2 -0
- package/dist/user-api/server.test.d.ts.map +1 -0
- package/dist/user-api/server.test.js +38 -0
- package/dist/web/frontend/404/index.html +20 -0
- package/dist/web/frontend/approve/index.html +23 -0
- package/dist/web/frontend/assets/404-BHkjVWlW.js +16 -0
- package/dist/web/frontend/assets/approve-lRsfAWmm.js +157 -0
- package/dist/web/frontend/assets/callback-QrXhW3mX.js +1 -0
- package/dist/web/frontend/assets/handle-errors-BcwHAkCd.js +1 -0
- package/dist/web/frontend/assets/index-BknZMPaI.css +5 -0
- package/dist/web/frontend/assets/index-BxdGgjHv.js +1 -0
- package/dist/web/frontend/assets/index-D-GexOrJ.js +697 -0
- package/dist/web/frontend/assets/index-TZrNw7dA.css +1 -0
- package/dist/web/frontend/assets/login-HUymBqli.js +159 -0
- package/dist/web/frontend/assets/networks-BZihbVwK.js +221 -0
- package/dist/web/frontend/assets/rpc-client-CCUlY3sp.js +1 -0
- package/dist/web/frontend/assets/state-DKGJ6EmM.js +9 -0
- package/dist/web/frontend/assets/state-manager-BNW0y5PZ.js +23 -0
- package/dist/web/frontend/assets/wallets-BoN6kUME.js +214 -0
- package/dist/web/frontend/callback/index.html +13 -0
- package/dist/web/frontend/icon.png +0 -0
- package/dist/web/frontend/index.html +19 -0
- package/dist/web/frontend/login/index.html +21 -0
- package/dist/web/frontend/networks/index.html +20 -0
- package/dist/web/frontend/wallets/index.html +22 -0
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +30 -0
- 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 @@
|
|
|
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
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
};
|