@canton-network/wallet-gateway-remote 1.5.0 → 1.6.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 (75) hide show
  1. package/dist/config/Config.d.ts +9 -0
  2. package/dist/config/Config.d.ts.map +1 -1
  3. package/dist/config/Config.js +5 -0
  4. package/dist/dapp-api/controller.d.ts +5 -1
  5. package/dist/dapp-api/controller.d.ts.map +1 -1
  6. package/dist/dapp-api/controller.js +70 -26
  7. package/dist/dapp-api/controller.test.js +2 -2
  8. package/dist/dapp-api/server.d.ts +3 -2
  9. package/dist/dapp-api/server.d.ts.map +1 -1
  10. package/dist/dapp-api/server.js +2 -2
  11. package/dist/dapp-api/server.test.js +1 -6
  12. package/dist/example-config.d.ts +3 -0
  13. package/dist/example-config.d.ts.map +1 -1
  14. package/dist/example-config.js +3 -0
  15. package/dist/init.d.ts.map +1 -1
  16. package/dist/init.js +16 -1
  17. package/dist/ledger/transaction-service.d.ts +2 -0
  18. package/dist/ledger/transaction-service.d.ts.map +1 -1
  19. package/dist/ledger/transaction-service.js +40 -0
  20. package/dist/ledger/transaction-service.test.js +34 -0
  21. package/dist/middleware/apiKeyAuth.d.ts.map +1 -1
  22. package/dist/middleware/apiKeyAuth.js +2 -0
  23. package/dist/signing/service-account-session.d.ts +16 -0
  24. package/dist/signing/service-account-session.d.ts.map +1 -0
  25. package/dist/signing/service-account-session.js +25 -0
  26. package/dist/signing/service-account-session.test.d.ts +2 -0
  27. package/dist/signing/service-account-session.test.d.ts.map +1 -0
  28. package/dist/signing/service-account-session.test.js +89 -0
  29. package/dist/signing/signing-worker.d.ts +83 -0
  30. package/dist/signing/signing-worker.d.ts.map +1 -0
  31. package/dist/signing/signing-worker.js +206 -0
  32. package/dist/signing/signing-worker.test.d.ts +2 -0
  33. package/dist/signing/signing-worker.test.d.ts.map +1 -0
  34. package/dist/signing/signing-worker.test.js +152 -0
  35. package/dist/user-api/controller.d.ts.map +1 -1
  36. package/dist/user-api/controller.js +8 -1
  37. package/dist/web/frontend/404/index.html +2 -2
  38. package/dist/web/frontend/activities/index.html +3 -3
  39. package/dist/web/frontend/approve/index.html +4 -4
  40. package/dist/web/frontend/assets/404-C4hCL2o8.js +5 -0
  41. package/dist/web/frontend/assets/{activities-DaWkmGqA.js → activities-Bq7yMMQY.js} +2 -2
  42. package/dist/web/frontend/assets/addIdentityProvider-DRJWLhHF.js +28 -0
  43. package/dist/web/frontend/assets/addNetwork-CJUaKP1q.js +38 -0
  44. package/dist/web/frontend/assets/{addParty-B3fFD1eM.js → addParty-2ObTLGIL.js} +1 -1
  45. package/dist/web/frontend/assets/{approve-WbGL4E82.js → approve-BuA5DGC2.js} +2 -2
  46. package/dist/web/frontend/assets/{callback--tihEryB.js → callback-C5vJWFLq.js} +1 -1
  47. package/dist/web/frontend/assets/{identityProviders-Dra4qG6t.js → identityProviders-78FbpzYL.js} +2 -2
  48. package/dist/web/frontend/assets/{index-Bm76pXA_.js → index-BTH_md6b.js} +3 -3
  49. package/dist/web/frontend/assets/index-CFnOucPK.js +4737 -0
  50. package/dist/web/frontend/assets/{login-BC8a-kD_.js → login-Dyn1o6fW.js} +2 -2
  51. package/dist/web/frontend/assets/{networks-B3ztsogL.js → networks-_el10AWP.js} +2 -2
  52. package/dist/web/frontend/assets/{reviewIdentityProvider-Co420Dbk.js → reviewIdentityProvider-DodUZ2mY.js} +3 -3
  53. package/dist/web/frontend/assets/reviewNetwork-BFMtmKEL.js +43 -0
  54. package/dist/web/frontend/assets/settings-BBmkIfYm.js +37 -0
  55. package/dist/web/frontend/assets/{state-Bq65GwdF.js → state-DkxLvywv.js} +1 -1
  56. package/dist/web/frontend/assets/{utils-B2iziW8V.js → utils-DvJKIYxT.js} +1 -1
  57. package/dist/web/frontend/callback/index.html +2 -2
  58. package/dist/web/frontend/identity-providers/add/index.html +3 -3
  59. package/dist/web/frontend/identity-providers/index.html +3 -3
  60. package/dist/web/frontend/identity-providers/review/index.html +3 -3
  61. package/dist/web/frontend/index.html +1 -1
  62. package/dist/web/frontend/login/index.html +3 -3
  63. package/dist/web/frontend/networks/add/index.html +3 -3
  64. package/dist/web/frontend/networks/index.html +3 -3
  65. package/dist/web/frontend/networks/review/index.html +3 -3
  66. package/dist/web/frontend/parties/add/index.html +5 -5
  67. package/dist/web/frontend/parties/index.html +4 -4
  68. package/dist/web/frontend/settings/index.html +3 -3
  69. package/package.json +20 -20
  70. package/dist/web/frontend/assets/404-MVLWUPwN.js +0 -5
  71. package/dist/web/frontend/assets/addIdentityProvider--rBb0ihb.js +0 -28
  72. package/dist/web/frontend/assets/addNetwork-C6UDsRkU.js +0 -38
  73. package/dist/web/frontend/assets/index-D1dC4Cl9.js +0 -4477
  74. package/dist/web/frontend/assets/reviewNetwork-B-Sh8pCH.js +0 -43
  75. package/dist/web/frontend/assets/settings-DvyUjqLl.js +0 -37
@@ -0,0 +1,89 @@
1
+ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ vi.mock('uuid', () => ({ v4: vi.fn(() => 'session-new') }));
5
+ import { pino } from 'pino';
6
+ import { sink } from 'pino-test';
7
+ import { resolveAutomationRunContext } from './service-account-session.js';
8
+ vi.mock('@canton-network/core-wallet-auth', async () => {
9
+ const actual = await vi.importActual('@canton-network/core-wallet-auth');
10
+ return {
11
+ ...actual,
12
+ AuthTokenProvider: {
13
+ fromGatewayConfig: vi.fn().mockReturnValue({
14
+ getAuthContext: vi.fn().mockResolvedValue({
15
+ userId: 'service_account',
16
+ accessToken: 'abc',
17
+ }),
18
+ }),
19
+ },
20
+ };
21
+ });
22
+ const idp = {
23
+ id: 'idp1',
24
+ type: 'oauth',
25
+ issuer: 'https://issuer.example',
26
+ configUrl: 'https://issuer.example/.well-known/openid-configuration',
27
+ };
28
+ const m2mNetwork = {
29
+ id: 'net-m2m',
30
+ name: 'm2m',
31
+ description: '',
32
+ synchronizerId: 'sync::fp',
33
+ identityProviderId: 'idp1',
34
+ ledgerApi: { baseUrl: 'http://ledger' },
35
+ auth: {
36
+ method: 'authorization_code',
37
+ clientId: 'svc',
38
+ audience: 'aud',
39
+ scope: 'scope',
40
+ },
41
+ serviceAccountAuth: {
42
+ method: 'client_credentials',
43
+ clientId: 'svc',
44
+ clientSecret: 'secret',
45
+ audience: 'aud',
46
+ scope: 'scope',
47
+ },
48
+ };
49
+ const logger = pino({ level: 'silent' }, sink());
50
+ describe('resolveAutomationRunContext', () => {
51
+ afterEach(() => vi.clearAllMocks());
52
+ it('reuses a valid client-credentials session', async () => {
53
+ const session = {
54
+ id: 'session-1',
55
+ network: 'net-m2m',
56
+ accessToken: 'abc',
57
+ };
58
+ const scopedStore = { setSession: vi.fn() };
59
+ const store = {
60
+ getIdp: vi.fn().mockResolvedValue(idp),
61
+ getNetwork: vi.fn().mockResolvedValue(m2mNetwork),
62
+ withAuthContext: vi.fn().mockReturnValue(scopedStore),
63
+ };
64
+ const result = await resolveAutomationRunContext(store, 'service_account', 'net-m2m', logger);
65
+ expect(result?.authContext).toEqual({
66
+ userId: 'service_account',
67
+ accessToken: session.accessToken,
68
+ });
69
+ });
70
+ it('mints a token when no usable session exists', async () => {
71
+ const scopedStore = { setSession: vi.fn() };
72
+ const store = {
73
+ getIdp: vi.fn().mockResolvedValue(idp),
74
+ getNetwork: vi.fn().mockResolvedValue(m2mNetwork),
75
+ withAuthContext: vi.fn().mockReturnValue(scopedStore),
76
+ };
77
+ const result = await resolveAutomationRunContext(store, 'service_account', 'net-m2m', logger);
78
+ expect(result?.authContext.userId).toBe('service_account');
79
+ });
80
+ it('mints when the stored session is not a client-credentials token', async () => {
81
+ const scopedStore = { setSession: vi.fn() };
82
+ const store = {
83
+ getIdp: vi.fn().mockResolvedValue(idp),
84
+ getNetwork: vi.fn().mockResolvedValue(m2mNetwork),
85
+ withAuthContext: vi.fn().mockReturnValue(scopedStore),
86
+ };
87
+ await resolveAutomationRunContext(store, 'service_account', 'net-m2m', logger);
88
+ });
89
+ });
@@ -0,0 +1,83 @@
1
+ import { Logger } from 'pino';
2
+ import { AuthAware } from '@canton-network/core-wallet-auth';
3
+ import { Store } from '@canton-network/core-wallet-store';
4
+ import { NotificationService } from '../notification/NotificationService.js';
5
+ import type { SigningDrivers } from './signing-drivers.js';
6
+ export type { AccessTokenProviderFactory } from './service-account-session.js';
7
+ /** Configuration for {@link SigningWorker}. */
8
+ export interface SigningWorkerOptions {
9
+ /** Poll interval in ms (`server.signingWorker.pollInterval`, default 5000). */
10
+ intervalMs: number;
11
+ signingDrivers: SigningDrivers;
12
+ store: Store & AuthAware<Store>;
13
+ notificationService: NotificationService;
14
+ logger: Logger;
15
+ }
16
+ /**
17
+ * Background poller that completes service-account transactions stuck in external
18
+ * custody approval.
19
+ *
20
+ * ## When this runs
21
+ *
22
+ * During M2M automation (`prepareExecute` → `signAndExecute`), external signing
23
+ * providers (Fireblocks, Blockdaemon, Dfns) may return `pending` instead of a
24
+ * signature. The gateway persists the transaction with `status: 'pending'` and
25
+ * an `externalTxId` from the provider. This worker picks up those rows on each
26
+ * tick via {@link Store.listAllPendingTransactions} (rows with an
27
+ * `externalTxId` are processed; others are skipped).
28
+ *
29
+ * Participant and wallet-kernel sign synchronously and never appear here.
30
+ *
31
+ * ## What happens on each tick
32
+ *
33
+ * For every pending row the worker resolves an M2M auth context
34
+ * ({@link resolveAutomationRunContext}), loads the user's primary wallet, and
35
+ * calls {@link TransactionService.signAndExecute}.
36
+ *
37
+ * Although the worker always invokes `signAndExecute`, **retries do not submit a
38
+ * new sign request**. `TransactionService.sign()` checks whether the stored
39
+ * transaction already has an `externalTxId`:
40
+ *
41
+ * - **First pass** (no `externalTxId`): `driver.signTransaction()` submits to
42
+ * custody and stores the returned `externalTxId`.
43
+ * - **Subsequent ticks** (`externalTxId` present): `driver.getTransaction()`
44
+ * polls the existing custody request for an updated status/signature.
45
+ *
46
+ * `signAndExecute` then:
47
+ *
48
+ * - returns early while the provider still reports `pending` (worker logs and
49
+ * waits for the next tick), or
50
+ * - calls `execute()` once signing is `signed` (Dfns is special: it broadcasts
51
+ * at sign-time, so execute only reconciles local state).
52
+ *
53
+ * A transaction is re-read before processing so a concurrent DApp call or an
54
+ * earlier tick that already completed it is skipped.
55
+ *
56
+ * ## Lifecycle
57
+ *
58
+ * Started once at gateway init (`start()`). Ticks are serialized: if a tick is
59
+ * still running, the next interval callback is a no-op. Use `stop()` on shutdown.
60
+ */
61
+ export declare class SigningWorker {
62
+ private readonly options;
63
+ private timer;
64
+ private running;
65
+ constructor(options: SigningWorkerOptions);
66
+ /** Starts the interval timer. Idempotent if already started. */
67
+ start(): void;
68
+ /** Clears the interval timer. */
69
+ stop(): void;
70
+ /**
71
+ * One poll cycle: list all pending transactions and attempt to complete
72
+ * external-custody rows via {@link processPending}. Safe to call directly in
73
+ * tests.
74
+ */
75
+ tick(): Promise<void>;
76
+ /**
77
+ * Drives a single pending transaction through `signAndExecute`. Skips when
78
+ * automation context, submitting wallet, or refreshed `pending` status is missing.
79
+ * Errors are logged per transaction; other rows in the same tick still run.
80
+ */
81
+ private processPending;
82
+ }
83
+ //# sourceMappingURL=signing-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing-worker.d.ts","sourceRoot":"","sources":["../../src/signing/signing-worker.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAA;AAC5D,OAAO,EAAE,KAAK,EAAuB,MAAM,mCAAmC,CAAA;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAE5E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAG1D,YAAY,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AAE9E,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACjC,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,cAAc,CAAA;IAC9B,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAA;IAC/B,mBAAmB,EAAE,mBAAmB,CAAA;IACxC,MAAM,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,qBAAa,aAAa;IAIV,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,OAAO,CAAQ;gBAEM,OAAO,EAAE,oBAAoB;IAE1D,gEAAgE;IAChE,KAAK,IAAI,IAAI;IAcb,iCAAiC;IACjC,IAAI,IAAI,IAAI;IAOZ;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B3B;;;;OAIG;YACW,cAAc;CAqH/B"}
@@ -0,0 +1,206 @@
1
+ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { TransactionService } from '../ledger/transaction-service.js';
4
+ import { resolveAutomationRunContext } from './service-account-session.js';
5
+ /**
6
+ * Background poller that completes service-account transactions stuck in external
7
+ * custody approval.
8
+ *
9
+ * ## When this runs
10
+ *
11
+ * During M2M automation (`prepareExecute` → `signAndExecute`), external signing
12
+ * providers (Fireblocks, Blockdaemon, Dfns) may return `pending` instead of a
13
+ * signature. The gateway persists the transaction with `status: 'pending'` and
14
+ * an `externalTxId` from the provider. This worker picks up those rows on each
15
+ * tick via {@link Store.listAllPendingTransactions} (rows with an
16
+ * `externalTxId` are processed; others are skipped).
17
+ *
18
+ * Participant and wallet-kernel sign synchronously and never appear here.
19
+ *
20
+ * ## What happens on each tick
21
+ *
22
+ * For every pending row the worker resolves an M2M auth context
23
+ * ({@link resolveAutomationRunContext}), loads the user's primary wallet, and
24
+ * calls {@link TransactionService.signAndExecute}.
25
+ *
26
+ * Although the worker always invokes `signAndExecute`, **retries do not submit a
27
+ * new sign request**. `TransactionService.sign()` checks whether the stored
28
+ * transaction already has an `externalTxId`:
29
+ *
30
+ * - **First pass** (no `externalTxId`): `driver.signTransaction()` submits to
31
+ * custody and stores the returned `externalTxId`.
32
+ * - **Subsequent ticks** (`externalTxId` present): `driver.getTransaction()`
33
+ * polls the existing custody request for an updated status/signature.
34
+ *
35
+ * `signAndExecute` then:
36
+ *
37
+ * - returns early while the provider still reports `pending` (worker logs and
38
+ * waits for the next tick), or
39
+ * - calls `execute()` once signing is `signed` (Dfns is special: it broadcasts
40
+ * at sign-time, so execute only reconciles local state).
41
+ *
42
+ * A transaction is re-read before processing so a concurrent DApp call or an
43
+ * earlier tick that already completed it is skipped.
44
+ *
45
+ * ## Lifecycle
46
+ *
47
+ * Started once at gateway init (`start()`). Ticks are serialized: if a tick is
48
+ * still running, the next interval callback is a no-op. Use `stop()` on shutdown.
49
+ */
50
+ export class SigningWorker {
51
+ constructor(options) {
52
+ this.options = options;
53
+ this.running = false;
54
+ }
55
+ /** Starts the interval timer. Idempotent if already started. */
56
+ start() {
57
+ this.options.logger.info('Starting signing worker');
58
+ if (this.timer) {
59
+ return;
60
+ }
61
+ this.timer = setInterval(() => {
62
+ void this.tick();
63
+ }, this.options.intervalMs);
64
+ this.options.logger.info({ intervalMs: this.options.intervalMs }, 'Signing worker started');
65
+ }
66
+ /** Clears the interval timer. */
67
+ stop() {
68
+ if (this.timer) {
69
+ clearInterval(this.timer);
70
+ this.timer = undefined;
71
+ }
72
+ }
73
+ /**
74
+ * One poll cycle: list all pending transactions and attempt to complete
75
+ * external-custody rows via {@link processPending}. Safe to call directly in
76
+ * tests.
77
+ */
78
+ async tick() {
79
+ this.options.logger.trace(`Signing worker: executing a tick`);
80
+ if (this.running) {
81
+ return;
82
+ }
83
+ this.running = true;
84
+ try {
85
+ const pending = await this.options.store.listAllPendingTransactions();
86
+ for (const tx of pending) {
87
+ if (!tx.externalTxId) {
88
+ continue;
89
+ }
90
+ await this.processPending(tx);
91
+ }
92
+ }
93
+ catch (error) {
94
+ this.options.logger.error({ err: error }, 'Signing worker tick failed');
95
+ }
96
+ finally {
97
+ this.running = false;
98
+ }
99
+ }
100
+ /**
101
+ * Drives a single pending transaction through `signAndExecute`. Skips when
102
+ * automation context, submitting wallet, or refreshed `pending` status is missing.
103
+ * Errors are logged per transaction; other rows in the same tick still run.
104
+ */
105
+ async processPending(transaction) {
106
+ const logger = this.options.logger;
107
+ const { userId, networkId, externalTxId } = transaction;
108
+ if (!externalTxId || !userId || !networkId) {
109
+ logger.debug({ transaction }, 'Skipping signing worker tick: missing required fields');
110
+ return;
111
+ }
112
+ const runContext = await resolveAutomationRunContext(this.options.store, userId, networkId, logger);
113
+ if (!runContext) {
114
+ logger.debug({ runContext, transactionId: transaction.id }, 'Skipping signing worker tick: no run context');
115
+ return;
116
+ }
117
+ const store = runContext.scopedStore;
118
+ const wallet = await resolveWalletFromTransaction(transaction, store, logger);
119
+ if (!wallet) {
120
+ logger.warn({
121
+ userId,
122
+ networkId,
123
+ transactionId: transaction.id,
124
+ commandId: transaction.commandId,
125
+ }, 'Skipping signing worker tick: could not deduce submitting wallet from transaction payload');
126
+ return;
127
+ }
128
+ const refreshedTx = await store.getTransaction(transaction.id);
129
+ if (!refreshedTx || refreshedTx.status !== 'pending') {
130
+ logger.debug({ refreshedTx }, 'Skipping signing worker tick: transaction no longer pending');
131
+ return;
132
+ }
133
+ const notifier = this.options.notificationService.getNotifier(userId);
134
+ const transactionLogger = this.options.logger.child({
135
+ component: 'TransactionService',
136
+ });
137
+ const transactionService = new TransactionService(store, transactionLogger, this.options.signingDrivers, notifier);
138
+ try {
139
+ const result = await transactionService.signAndExecute(runContext.authContext, runContext.network, wallet, refreshedTx);
140
+ if ('status' in result && result.status === 'pending') {
141
+ logger.info({
142
+ userId,
143
+ networkId,
144
+ transactionId: transaction.id,
145
+ commandId: transaction.commandId,
146
+ externalTxId: result.externalTxId,
147
+ signingProviderId: wallet.signingProviderId,
148
+ }, 'Signing worker: transaction still awaiting external signing');
149
+ }
150
+ else {
151
+ logger.info({
152
+ userId,
153
+ networkId,
154
+ transactionId: transaction.id,
155
+ commandId: transaction.commandId,
156
+ externalTxId: transaction.externalTxId,
157
+ partyId: wallet.partyId,
158
+ signingProviderId: wallet.signingProviderId,
159
+ result,
160
+ }, 'Signing worker completed service account transaction');
161
+ }
162
+ }
163
+ catch (error) {
164
+ logger.error({
165
+ err: error,
166
+ userId,
167
+ networkId,
168
+ transactionId: transaction.id,
169
+ commandId: transaction.commandId,
170
+ externalTxId: transaction.externalTxId,
171
+ partyId: wallet.partyId,
172
+ signingProviderId: wallet.signingProviderId,
173
+ }, 'Signing worker failed to complete service account transaction');
174
+ }
175
+ }
176
+ }
177
+ /**
178
+ * Given the transaction, attempt to resolve the original wallet that submitted it.
179
+ * It's not guaranteed that the primary wallet was used, so we check the `actAs` field in the payload and find a wallet with a matching party ID.
180
+ *
181
+ * Returns `undefined` if the wallet cannot be resolved.
182
+ */
183
+ async function resolveWalletFromTransaction(tx, store, logger) {
184
+ logger.debug({ tx, store }, 'Resolving wallet from transaction payload');
185
+ // first see if we can determine the wallet from the payload's actAs field.
186
+ // If not, we will default to the primary wallet.
187
+ let wallet = undefined;
188
+ const payload = tx.payload;
189
+ if (payload && typeof payload === 'object') {
190
+ const actAs = payload.actAs;
191
+ if (Array.isArray(actAs) && actAs.length !== 0) {
192
+ if (actAs.length > 1) {
193
+ logger.warn({ actAs }, 'Transaction has multiple acting parties; using the first one to resolve wallet');
194
+ }
195
+ const partyId = actAs[0];
196
+ const wallets = await store.getAllWallets();
197
+ wallet = wallets.find((wallet) => wallet.partyId === partyId);
198
+ }
199
+ }
200
+ if (!wallet) {
201
+ // No actAs was given: lets default to the primary party.
202
+ wallet = await store.getPrimaryWallet();
203
+ logger.debug({ wallet, tx }, 'Transaction has no actAs; using primary wallet');
204
+ }
205
+ return wallet;
206
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=signing-worker.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing-worker.test.d.ts","sourceRoot":"","sources":["../../src/signing/signing-worker.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,152 @@
1
+ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { pino } from 'pino';
5
+ import { sink } from 'pino-test';
6
+ import { SigningWorker } from './signing-worker.js';
7
+ import { SigningProvider } from '@canton-network/core-signing-lib';
8
+ const mocks = vi.hoisted(() => ({
9
+ signAndExecute: vi.fn().mockResolvedValue({ commandId: 'cmd-1' }),
10
+ }));
11
+ vi.mock('@canton-network/core-wallet-auth', async () => {
12
+ const actual = await vi.importActual('@canton-network/core-wallet-auth');
13
+ return {
14
+ ...actual,
15
+ AuthTokenProvider: {
16
+ fromGatewayConfig: vi.fn().mockReturnValue({
17
+ getAuthContext: vi.fn().mockResolvedValue({
18
+ userId: 'service_account',
19
+ accessToken: 'abc',
20
+ }),
21
+ }),
22
+ },
23
+ };
24
+ });
25
+ vi.mock('../ledger/transaction-service.js', () => ({
26
+ TransactionService: vi.fn(function TransactionServiceMock() {
27
+ return { signAndExecute: mocks.signAndExecute };
28
+ }),
29
+ }));
30
+ const idp = {
31
+ id: 'idp1',
32
+ type: 'oauth',
33
+ issuer: 'https://issuer.example',
34
+ configUrl: 'https://issuer.example/.well-known/openid-configuration',
35
+ };
36
+ const m2mNetwork = {
37
+ id: 'net-m2m',
38
+ name: 'm2m',
39
+ description: '',
40
+ synchronizerId: 'sync::fp',
41
+ identityProviderId: 'idp1',
42
+ ledgerApi: { baseUrl: 'http://ledger' },
43
+ auth: {
44
+ method: 'authorization_code',
45
+ clientId: 'user',
46
+ audience: 'aud',
47
+ scope: 'scope',
48
+ },
49
+ serviceAccountAuth: {
50
+ method: 'client_credentials',
51
+ clientId: 'svc',
52
+ clientSecret: 'secret',
53
+ audience: 'aud',
54
+ scope: 'scope',
55
+ },
56
+ };
57
+ const wallet = {
58
+ primary: true,
59
+ partyId: 'party::ns',
60
+ status: 'allocated',
61
+ hint: 'party',
62
+ publicKey: 'pk',
63
+ namespace: 'ns',
64
+ networkId: 'net-m2m',
65
+ signingProviderId: SigningProvider.FIREBLOCKS,
66
+ rights: [],
67
+ };
68
+ const aliceWallet = {
69
+ primary: false,
70
+ partyId: 'alice::ns2',
71
+ status: 'allocated',
72
+ hint: 'alice',
73
+ publicKey: 'pk2',
74
+ namespace: 'ns2',
75
+ networkId: 'net-m2m',
76
+ signingProviderId: SigningProvider.FIREBLOCKS,
77
+ rights: [],
78
+ };
79
+ const pendingTransaction = {
80
+ id: 'tx-1',
81
+ commandId: 'cmd-1',
82
+ status: 'pending',
83
+ preparedTransaction: 'blob',
84
+ preparedTransactionHash: 'hash',
85
+ externalTxId: 'ext-1',
86
+ origin: null,
87
+ userId: 'user-1',
88
+ networkId: 'net-m2m',
89
+ };
90
+ function createWorker(storeOverrides = {}) {
91
+ const scopedStore = {
92
+ setSession: vi.fn().mockResolvedValue(undefined),
93
+ getPrimaryWallet: vi.fn().mockResolvedValue(wallet),
94
+ getAllWallets: vi.fn().mockResolvedValue([wallet, aliceWallet]),
95
+ getTransaction: vi.fn().mockResolvedValue(pendingTransaction),
96
+ };
97
+ const store = {
98
+ listAllPendingTransactions: vi
99
+ .fn()
100
+ .mockResolvedValue([pendingTransaction]),
101
+ getIdp: vi.fn().mockResolvedValue(idp),
102
+ getNetwork: vi.fn().mockResolvedValue(m2mNetwork),
103
+ withAuthContext: vi.fn().mockReturnValue(scopedStore),
104
+ ...storeOverrides,
105
+ };
106
+ const logger = pino({ level: 'silent' }, sink());
107
+ return {
108
+ worker: new SigningWorker({
109
+ intervalMs: 1000,
110
+ signingDrivers: {},
111
+ store: store,
112
+ notificationService: {
113
+ getNotifier: vi.fn(() => ({ emit: vi.fn() })),
114
+ },
115
+ logger,
116
+ }),
117
+ store,
118
+ scopedStore,
119
+ };
120
+ }
121
+ describe('SigningWorker', () => {
122
+ afterEach(() => vi.clearAllMocks());
123
+ it('completes pending external transactions with a primary wallet', async () => {
124
+ const { worker } = createWorker();
125
+ await worker.tick();
126
+ expect(mocks.signAndExecute.mock.calls[0][2].partyId).toBe('party::ns');
127
+ });
128
+ it('uses a non-primary wallet when actAs payload is present', async () => {
129
+ const { worker } = createWorker({
130
+ listAllPendingTransactions: vi.fn().mockResolvedValue([
131
+ {
132
+ ...pendingTransaction,
133
+ payload: { actAs: ['alice::ns2'] },
134
+ },
135
+ ]),
136
+ });
137
+ await worker.tick();
138
+ expect(mocks.signAndExecute).toHaveBeenCalledOnce();
139
+ expect(mocks.signAndExecute.mock.calls[0][2].partyId).toBe('alice::ns2');
140
+ });
141
+ it('skips pending transactions without an externalTxId', async () => {
142
+ const { worker } = createWorker({
143
+ listAllPendingTransactions: vi
144
+ .fn()
145
+ .mockResolvedValue([
146
+ { ...pendingTransaction, externalTxId: undefined },
147
+ ]),
148
+ });
149
+ await worker.tick();
150
+ expect(mocks.signAndExecute).not.toHaveBeenCalled();
151
+ });
152
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/user-api/controller.ts"],"names":[],"mappings":"AA+CA,OAAO,EAAE,KAAK,EAAW,MAAM,mCAAmC,CAAA;AAClE,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAC5E,OAAO,EAEH,WAAW,EAKd,MAAM,kCAAkC,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAEhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAYnE,eAAO,MAAM,cAAc,GACvB,YAAY,UAAU,EACtB,SAAS,MAAM,EACf,OAAO,KAAK,EACZ,qBAAqB,mBAAmB,EACxC,aAAa,WAAW,GAAG,SAAS,EACpC,SAAS,cAAc,EACvB,SAAS,MAAM,EACf,cAAc,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4hCvB,CAAA"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/user-api/controller.ts"],"names":[],"mappings":"AA+CA,OAAO,EAAE,KAAK,EAAW,MAAM,mCAAmC,CAAA;AAClE,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAC5E,OAAO,EAEH,WAAW,EAKd,MAAM,kCAAkC,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAEhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAYnE,eAAO,MAAM,cAAc,GACvB,YAAY,UAAU,EACtB,SAAS,MAAM,EACf,OAAO,KAAK,EACZ,qBAAqB,mBAAmB,EACxC,aAAa,WAAW,GAAG,SAAS,EACpC,SAAS,cAAc,EACvB,SAAS,MAAM,EACf,cAAc,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgiCvB,CAAA"}
@@ -46,6 +46,9 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
46
46
  const adminAuth = network.adminAuth
47
47
  ? authSchema.parse(network.adminAuth)
48
48
  : undefined;
49
+ const serviceAccountAuth = network.serviceAccountAuth
50
+ ? authSchema.parse(network.serviceAccountAuth)
51
+ : undefined;
49
52
  const newNetwork = {
50
53
  name: network.name,
51
54
  id: network.id,
@@ -54,6 +57,7 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
54
57
  identityProviderId: network.identityProviderId,
55
58
  auth,
56
59
  adminAuth,
60
+ serviceAccountAuth,
57
61
  ledgerApi,
58
62
  };
59
63
  // TODO: Add an explicit updateNetwork method to the User API spec and controller
@@ -434,12 +438,12 @@ export const userController = (kernelInfo, userUrl, store, notificationService,
434
438
  try {
435
439
  const newSessionId = v4();
436
440
  logger.info(`Adding session with ID ${newSessionId} for network ${params.networkId}`);
441
+ const network = await store.getNetwork(params.networkId);
437
442
  await store.setSession({
438
443
  id: newSessionId,
439
444
  network: params.networkId,
440
445
  accessToken: authContext?.accessToken || '',
441
446
  });
442
- const network = await store.getCurrentNetwork();
443
447
  const idp = await store.getIdp(network.identityProviderId);
444
448
  // Assumption: `setSession` calls `assertConnected`, so its safe to declare that the authContext is defined.
445
449
  const { userId, accessToken } = authContext;
@@ -779,6 +783,9 @@ function toNetworkDto(network) {
779
783
  ...(network.adminAuth
780
784
  ? { adminAuth: toAuthDto(network.adminAuth) }
781
785
  : {}),
786
+ ...(network.serviceAccountAuth
787
+ ? { serviceAccountAuth: toAuthDto(network.serviceAccountAuth) }
788
+ : {}),
782
789
  };
783
790
  }
784
791
  function toPublicNetwork(network) {
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Wallet Gateway - Not found</title>
7
- <script type="module" crossorigin src="../assets/404-MVLWUPwN.js"></script>
8
- <link rel="modulepreload" crossorigin href="../assets/index-D1dC4Cl9.js">
7
+ <script type="module" crossorigin src="../assets/404-C4hCL2o8.js"></script>
8
+ <link rel="modulepreload" crossorigin href="../assets/index-CFnOucPK.js">
9
9
  </head>
10
10
 
11
11
  <body>
@@ -4,9 +4,9 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Wallet Gateway - Activities</title>
7
- <script type="module" crossorigin src="../assets/activities-DaWkmGqA.js"></script>
8
- <link rel="modulepreload" crossorigin href="../assets/index-D1dC4Cl9.js">
9
- <link rel="modulepreload" crossorigin href="../assets/state-Bq65GwdF.js">
7
+ <script type="module" crossorigin src="../assets/activities-Bq7yMMQY.js"></script>
8
+ <link rel="modulepreload" crossorigin href="../assets/index-CFnOucPK.js">
9
+ <link rel="modulepreload" crossorigin href="../assets/state-DkxLvywv.js">
10
10
  <link rel="modulepreload" crossorigin href="../assets/index-CiMGhYb1.js">
11
11
  </head>
12
12
  <body>
@@ -4,11 +4,11 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Wallet Gateway - Activity detail</title>
7
- <script type="module" crossorigin src="../assets/approve-WbGL4E82.js"></script>
8
- <link rel="modulepreload" crossorigin href="../assets/index-D1dC4Cl9.js">
9
- <link rel="modulepreload" crossorigin href="../assets/state-Bq65GwdF.js">
7
+ <script type="module" crossorigin src="../assets/approve-BuA5DGC2.js"></script>
8
+ <link rel="modulepreload" crossorigin href="../assets/index-CFnOucPK.js">
9
+ <link rel="modulepreload" crossorigin href="../assets/state-DkxLvywv.js">
10
10
  <link rel="modulepreload" crossorigin href="../assets/index-CiMGhYb1.js">
11
- <link rel="modulepreload" crossorigin href="../assets/utils-B2iziW8V.js">
11
+ <link rel="modulepreload" crossorigin href="../assets/utils-DvJKIYxT.js">
12
12
  </head>
13
13
 
14
14
  <body>
@@ -0,0 +1,5 @@
1
+ import{h as m,i as z,A,b as E,t as I}from"./index-CFnOucPK.js";var j=Object.create,s=Object.defineProperty,k=Object.getOwnPropertyDescriptor,y=(r,e)=>(e=Symbol[r])?e:Symbol.for("Symbol."+r),x=r=>{throw TypeError(r)},D=(r,e,o)=>e in r?s(r,e,{enumerable:!0,configurable:!0,writable:!0,value:o}):r[e]=o,F=(r,e)=>s(r,"name",{value:e,configurable:!0}),g=r=>[,,,j(r?.[y("metadata")]??null)],C=["class","method","getter","setter","accessor","field","value","get","set"],S=r=>r!==void 0&&typeof r!="function"?x("Function expected"):r,M=(r,e,o,_,t)=>({kind:C[r],name:e,metadata:_,addInitializer:a=>o._?x("Already initialized"):t.push(S(a||null))}),N=(r,e)=>D(e,y("metadata"),r[3]),T=(r,e,o,_)=>{for(var t=0,a=r[e>>1],n=a&&a.length;t<n;t++)a[t].call(o);return _},$=(r,e,o,_,t,a)=>{var n,p,f,i=e&7,b=!1,h=0,P=r[h]||(r[h]=[]),c=i&&(t=t.prototype,i<5&&(i>3||!b)&&k(t,o));F(t,o);for(var d=_.length-1;d>=0;d--)f=M(i,o,p={},r[3],P),n=(0,_[d])(t,f),p._=1,S(n)&&(t=n);return N(r,t),c&&s(t,o,c),b?i^4?a:c:t},w,v,O;w=[I("user-ui-404")];const u=class u extends(O=m){render(){return E`<not-found href=${A("/")}></not-found>`}};u.styles=[m.styles,z`
2
+ :host {
3
+ display: block;
4
+ }
5
+ `];let l=u;v=g(O);l=$(v,0,"NotFoundUi",w,l);T(v,1,l);