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