@canton-network/wallet-gateway-remote 1.3.0 → 1.4.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.d.ts +4 -4
- package/dist/dapp-api/controller.d.ts +1 -0
- package/dist/dapp-api/controller.d.ts.map +1 -1
- package/dist/dapp-api/controller.js +33 -2
- package/dist/dapp-api/controller.test.d.ts +2 -0
- package/dist/dapp-api/controller.test.d.ts.map +1 -0
- package/dist/dapp-api/controller.test.js +516 -0
- package/dist/dapp-api/rpc-gen/index.d.ts +3 -0
- package/dist/dapp-api/rpc-gen/index.d.ts.map +1 -1
- package/dist/dapp-api/rpc-gen/index.js +1 -0
- package/dist/dapp-api/rpc-gen/typings.d.ts +99 -14
- 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 +5 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1 -0
- package/dist/middleware/rateLimit.js +2 -2
- package/dist/middleware/rateLimit.test.js +14 -0
- package/dist/user-api/controller.d.ts +6 -0
- package/dist/user-api/controller.d.ts.map +1 -1
- package/dist/user-api/controller.js +223 -5
- package/dist/user-api/rpc-gen/index.d.ts +18 -0
- package/dist/user-api/rpc-gen/index.d.ts.map +1 -1
- package/dist/user-api/rpc-gen/index.js +6 -0
- package/dist/user-api/rpc-gen/typings.d.ts +119 -23
- package/dist/user-api/rpc-gen/typings.d.ts.map +1 -1
- package/dist/user-api/server.test.js +53 -0
- package/dist/utils.d.ts +2 -4
- package/dist/utils.d.ts.map +1 -1
- package/dist/web/frontend/404/index.html +2 -2
- package/dist/web/frontend/activities/index.html +3 -3
- package/dist/web/frontend/approve/index.html +4 -5
- package/dist/web/frontend/assets/404-C7V2yl2z.js +5 -0
- package/dist/web/frontend/assets/activities-NY8oZAkH.js +61 -0
- package/dist/web/frontend/assets/addIdentityProvider-38ghz3RT.js +28 -0
- package/dist/web/frontend/assets/addNetwork-BaDhbrZl.js +38 -0
- package/dist/web/frontend/assets/addParty-j3JWZX5a.js +41 -0
- package/dist/web/frontend/assets/approve-43nVzAAC.js +21 -0
- package/dist/web/frontend/assets/callback-BBMiCjHR.js +1 -0
- package/dist/web/frontend/assets/identityProviders-BZ9h_VOJ.js +71 -0
- package/dist/web/frontend/assets/index-B-HiKugW.js +91 -0
- package/dist/web/frontend/assets/{index-_pMHlJoE.js → index-DoNqknyE.js} +116 -116
- package/dist/web/frontend/assets/login-CqwMEG9J.js +10 -0
- package/dist/web/frontend/assets/networks-BJvRi47M.js +73 -0
- package/dist/web/frontend/assets/reviewIdentityProvider-ySlQnw5R.js +43 -0
- package/dist/web/frontend/assets/reviewNetwork-DrVxeHd4.js +43 -0
- package/dist/web/frontend/assets/{settings-B1ga2TK0.js → settings-BVNkaUyJ.js} +1 -1
- package/dist/web/frontend/assets/state-qAsNw5qf.js +1 -0
- package/dist/web/frontend/assets/{utils-CT9Hzi7v.js → utils-BDkHxi8V.js} +1 -1
- package/dist/web/frontend/callback/index.html +2 -2
- package/dist/web/frontend/identity-providers/add/index.html +3 -3
- package/dist/web/frontend/identity-providers/index.html +3 -3
- package/dist/web/frontend/identity-providers/review/index.html +3 -3
- package/dist/web/frontend/index.html +1 -1
- package/dist/web/frontend/login/index.html +3 -4
- package/dist/web/frontend/networks/add/index.html +3 -3
- package/dist/web/frontend/networks/index.html +3 -3
- package/dist/web/frontend/networks/review/index.html +3 -3
- package/dist/web/frontend/parties/add/index.html +5 -5
- package/dist/web/frontend/parties/index.html +4 -4
- package/dist/web/frontend/settings/index.html +3 -3
- package/package.json +26 -23
- package/dist/web/frontend/assets/404-B-o9ppJB.js +0 -5
- package/dist/web/frontend/assets/activities-CGjCIjUH.js +0 -61
- package/dist/web/frontend/assets/addIdentityProvider-CR4Wm9Tl.js +0 -28
- package/dist/web/frontend/assets/addNetwork-Dx0-SN4j.js +0 -38
- package/dist/web/frontend/assets/addParty-COhk_rFn.js +0 -41
- package/dist/web/frontend/assets/approve-B2w66l0J.js +0 -21
- package/dist/web/frontend/assets/callback-BTVon_yQ.js +0 -1
- package/dist/web/frontend/assets/identityProviders-CK8zSrd3.js +0 -71
- package/dist/web/frontend/assets/index-CF4BKzgl.js +0 -91
- package/dist/web/frontend/assets/index-DWz_3f3y.js +0 -1
- package/dist/web/frontend/assets/login-CVoPNVDw.js +0 -10
- package/dist/web/frontend/assets/networks-D1nPvUzM.js +0 -73
- package/dist/web/frontend/assets/reviewIdentityProvider-CNSf2qQv.js +0 -43
- package/dist/web/frontend/assets/reviewNetwork-DGLK-Ume.js +0 -43
- package/dist/web/frontend/assets/state-B2k3ak7d.js +0 -1
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Dfns provisions and activates Canton wallets through its validator integration,
|
|
|
59
59
|
|
|
60
60
|
## Fireblocks
|
|
61
61
|
|
|
62
|
-
1. Complete steps 1–3 from the instructions at https://github.com/canton-network/wallet
|
|
62
|
+
1. Complete steps 1–3 from the instructions at https://github.com/canton-network/wallet/tree/main/core/signing-fireblocks
|
|
63
63
|
|
|
64
64
|
2. set the environment variable `FIREBLOCKS_API_KEY` (get it from `API User (ID)` column in fireblocks api users table).
|
|
65
65
|
|
package/dist/config/Config.d.ts
CHANGED
|
@@ -61,7 +61,7 @@ export declare const rawConfigSchema: z.ZodObject<{
|
|
|
61
61
|
user: z.ZodString;
|
|
62
62
|
password: z.ZodString;
|
|
63
63
|
database: z.ZodString;
|
|
64
|
-
}, z.core.$
|
|
64
|
+
}, z.core.$loose>], "type">;
|
|
65
65
|
}, z.core.$strip>;
|
|
66
66
|
signingStore: z.ZodObject<{
|
|
67
67
|
connection: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -76,7 +76,7 @@ export declare const rawConfigSchema: z.ZodObject<{
|
|
|
76
76
|
user: z.ZodString;
|
|
77
77
|
password: z.ZodString;
|
|
78
78
|
database: z.ZodString;
|
|
79
|
-
}, z.core.$
|
|
79
|
+
}, z.core.$loose>], "type">;
|
|
80
80
|
}, z.core.$strip>;
|
|
81
81
|
bootstrap: z.ZodObject<{
|
|
82
82
|
idps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -220,7 +220,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
220
220
|
user: z.ZodString;
|
|
221
221
|
password: z.ZodString;
|
|
222
222
|
database: z.ZodString;
|
|
223
|
-
}, z.core.$
|
|
223
|
+
}, z.core.$loose>], "type">;
|
|
224
224
|
}, z.core.$strip>;
|
|
225
225
|
signingStore: z.ZodObject<{
|
|
226
226
|
connection: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -235,7 +235,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
235
235
|
user: z.ZodString;
|
|
236
236
|
password: z.ZodString;
|
|
237
237
|
database: z.ZodString;
|
|
238
|
-
}, z.core.$
|
|
238
|
+
}, z.core.$loose>], "type">;
|
|
239
239
|
}, z.core.$strip>;
|
|
240
240
|
bootstrap: z.ZodObject<{
|
|
241
241
|
idps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -18,5 +18,6 @@ export declare const dappController: (kernelInfo: KernelInfoConfig, dappUrl: str
|
|
|
18
18
|
getPrimaryAccount: import("./rpc-gen/typings.js").GetPrimaryAccount;
|
|
19
19
|
listAccounts: import("./rpc-gen/typings.js").ListAccounts;
|
|
20
20
|
txChanged: import("./rpc-gen/typings.js").TxChanged;
|
|
21
|
+
messageSignature: import("./rpc-gen/typings.js").MessageSignature;
|
|
21
22
|
};
|
|
22
23
|
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/dapp-api/controller.ts"],"names":[],"mappings":"AAGA,OAAO,EAEH,WAAW,EAEd,MAAM,kCAAkC,CAAA;
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/dapp-api/controller.ts"],"names":[],"mappings":"AAGA,OAAO,EAEH,WAAW,EAEd,MAAM,kCAAkC,CAAA;AAczC,OAAO,EAAE,KAAK,EAAe,MAAM,mCAAmC,CAAA;AAQtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAC5E,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAI7B,eAAO,MAAM,cAAc,GACvB,YAAY,gBAAgB,EAC5B,SAAS,MAAM,EACf,SAAS,MAAM,EACf,OAAO,KAAK,EACZ,qBAAqB,mBAAmB,EACxC,SAAS,MAAM,EACf,QAAQ,MAAM,GAAG,IAAI,EACrB,UAAU,WAAW;;;;;;;;;;;;;;;;CAyVxB,CAAA"}
|
|
@@ -246,8 +246,36 @@ export const dappController = (kernelInfo, dappUrl, userUrl, store, notification
|
|
|
246
246
|
: {}),
|
|
247
247
|
};
|
|
248
248
|
},
|
|
249
|
-
signMessage:
|
|
250
|
-
|
|
249
|
+
signMessage: async (params) => {
|
|
250
|
+
if (!params?.message)
|
|
251
|
+
throw new Error('Message is required');
|
|
252
|
+
const wallet = await store.getPrimaryWallet();
|
|
253
|
+
if (context === undefined) {
|
|
254
|
+
throw new Error('Unauthenticated context');
|
|
255
|
+
}
|
|
256
|
+
if (wallet === undefined) {
|
|
257
|
+
throw new Error('No primary wallet found');
|
|
258
|
+
}
|
|
259
|
+
const notifier = notificationService.getNotifier(context.userId);
|
|
260
|
+
const messageId = v4();
|
|
261
|
+
await store.setMessageRaw({
|
|
262
|
+
id: messageId,
|
|
263
|
+
status: 'pending',
|
|
264
|
+
userId: context.userId,
|
|
265
|
+
partyId: wallet.partyId,
|
|
266
|
+
publicKey: wallet.publicKey,
|
|
267
|
+
message: params.message,
|
|
268
|
+
origin: origin || null,
|
|
269
|
+
createdAt: new Date(),
|
|
270
|
+
});
|
|
271
|
+
notifier.emit('messageSignature', {
|
|
272
|
+
status: 'pending',
|
|
273
|
+
messageId,
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
messageId,
|
|
277
|
+
userUrl: `${userUrl}/sign-message/index.html?messageId=${messageId}&closeafteraction`,
|
|
278
|
+
};
|
|
251
279
|
},
|
|
252
280
|
getPrimaryAccount: async function () {
|
|
253
281
|
const wallet = await store.getPrimaryWallet();
|
|
@@ -256,6 +284,9 @@ export const dappController = (kernelInfo, dappUrl, userUrl, store, notification
|
|
|
256
284
|
}
|
|
257
285
|
return wallet;
|
|
258
286
|
},
|
|
287
|
+
messageSignature: function () {
|
|
288
|
+
throw new Error('Only for events.');
|
|
289
|
+
},
|
|
259
290
|
});
|
|
260
291
|
};
|
|
261
292
|
async function prepareSubmission(userId, partyId, synchronizerId, params, ledgerClient) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.test.d.ts","sourceRoot":"","sources":["../../src/dapp-api/controller.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,516 @@
|
|
|
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, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { pino } from 'pino';
|
|
5
|
+
import { sink } from 'pino-test';
|
|
6
|
+
import { PartyLevelRight, } from '@canton-network/core-wallet-store';
|
|
7
|
+
import { StoreInternal } from '@canton-network/core-wallet-store-inmemory';
|
|
8
|
+
import { SigningProvider } from '@canton-network/core-signing-lib';
|
|
9
|
+
import { NotificationService } from '../notification/NotificationService.js';
|
|
10
|
+
import { dappController } from './controller.js';
|
|
11
|
+
const ledgerMocks = vi.hoisted(() => ({
|
|
12
|
+
getWithRetry: vi.fn(),
|
|
13
|
+
postWithRetry: vi.fn(),
|
|
14
|
+
getSynchronizerId: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
const mockNetworkStatus = vi.hoisted(() => vi.fn().mockResolvedValue({
|
|
17
|
+
isConnected: true,
|
|
18
|
+
reason: undefined,
|
|
19
|
+
cantonVersion: '3.4',
|
|
20
|
+
}));
|
|
21
|
+
const mockUuidV4 = vi.hoisted(() => vi.fn());
|
|
22
|
+
vi.mock('@canton-network/core-ledger-client', async (importOriginal) => {
|
|
23
|
+
const actual = await importOriginal();
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
LedgerClient: vi.fn(function LedgerClientMock() {
|
|
27
|
+
return {
|
|
28
|
+
getWithRetry: ledgerMocks.getWithRetry,
|
|
29
|
+
postWithRetry: ledgerMocks.postWithRetry,
|
|
30
|
+
getSynchronizerId: ledgerMocks.getSynchronizerId,
|
|
31
|
+
};
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
vi.mock('../utils.js', async (importOriginal) => {
|
|
36
|
+
const actual = await importOriginal();
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
networkStatus: mockNetworkStatus,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
vi.mock('uuid', () => ({
|
|
43
|
+
v4: mockUuidV4,
|
|
44
|
+
}));
|
|
45
|
+
const kernelInfo = {
|
|
46
|
+
id: 'kernel-test',
|
|
47
|
+
clientType: 'browser',
|
|
48
|
+
};
|
|
49
|
+
const dappUrl = 'https://dapp.api.example';
|
|
50
|
+
const userUrl = 'https://user.api.example';
|
|
51
|
+
const origin = 'https://dapp.example';
|
|
52
|
+
const idp = {
|
|
53
|
+
id: 'idp1',
|
|
54
|
+
type: 'oauth',
|
|
55
|
+
issuer: 'http://auth',
|
|
56
|
+
configUrl: 'http://auth/.well-known/openid-configuration',
|
|
57
|
+
};
|
|
58
|
+
const storeNetwork = {
|
|
59
|
+
id: 'network1',
|
|
60
|
+
name: 'testnet',
|
|
61
|
+
synchronizerId: 'sync1::fingerprint',
|
|
62
|
+
description: 'Test',
|
|
63
|
+
identityProviderId: 'idp1',
|
|
64
|
+
ledgerApi: { baseUrl: 'http://ledger.test' },
|
|
65
|
+
auth: {
|
|
66
|
+
method: 'authorization_code',
|
|
67
|
+
clientId: 'cid',
|
|
68
|
+
scope: 'scope',
|
|
69
|
+
audience: 'aud',
|
|
70
|
+
},
|
|
71
|
+
adminAuth: {
|
|
72
|
+
method: 'client_credentials',
|
|
73
|
+
clientId: 'admin-cid',
|
|
74
|
+
clientSecret: 'admin-secret',
|
|
75
|
+
audience: 'admin-aud',
|
|
76
|
+
scope: 'admin-scope',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const auth = {
|
|
80
|
+
userId: 'user-1',
|
|
81
|
+
accessToken: 'access-token-1',
|
|
82
|
+
};
|
|
83
|
+
const session = {
|
|
84
|
+
id: 'session-1',
|
|
85
|
+
network: 'network1',
|
|
86
|
+
accessToken: 'session-token',
|
|
87
|
+
};
|
|
88
|
+
const primaryWallet = {
|
|
89
|
+
primary: true,
|
|
90
|
+
partyId: 'party::namespace',
|
|
91
|
+
status: 'allocated',
|
|
92
|
+
hint: 'party',
|
|
93
|
+
signingProviderId: SigningProvider.WALLET_KERNEL,
|
|
94
|
+
publicKey: 'wallet-public-key',
|
|
95
|
+
namespace: 'namespace',
|
|
96
|
+
networkId: 'network1',
|
|
97
|
+
rights: [PartyLevelRight.CanActAs],
|
|
98
|
+
};
|
|
99
|
+
async function createStore(logger, context, options = {}) {
|
|
100
|
+
const { withSession = true, withWallet = true } = options;
|
|
101
|
+
const store = new StoreInternal({ idps: [idp], networks: [storeNetwork] }, logger, context);
|
|
102
|
+
if (context && withSession) {
|
|
103
|
+
await store.setSession(session);
|
|
104
|
+
}
|
|
105
|
+
if (context && withWallet) {
|
|
106
|
+
await store.addWallet(primaryWallet);
|
|
107
|
+
}
|
|
108
|
+
return store;
|
|
109
|
+
}
|
|
110
|
+
function createController(store, notificationService, logger, context, requestOrigin = origin) {
|
|
111
|
+
return dappController(kernelInfo, dappUrl, userUrl, store, notificationService, logger, requestOrigin, context);
|
|
112
|
+
}
|
|
113
|
+
describe('dappController', () => {
|
|
114
|
+
let logger;
|
|
115
|
+
let notificationService;
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
logger = pino({ level: 'silent' }, sink());
|
|
118
|
+
notificationService = new NotificationService(logger);
|
|
119
|
+
ledgerMocks.getWithRetry.mockReset();
|
|
120
|
+
ledgerMocks.postWithRetry.mockReset();
|
|
121
|
+
ledgerMocks.getSynchronizerId.mockReset();
|
|
122
|
+
ledgerMocks.getSynchronizerId.mockResolvedValue('sync-from-ledger');
|
|
123
|
+
mockNetworkStatus.mockReset();
|
|
124
|
+
mockNetworkStatus.mockResolvedValue({
|
|
125
|
+
isConnected: true,
|
|
126
|
+
reason: undefined,
|
|
127
|
+
cantonVersion: '3.4',
|
|
128
|
+
});
|
|
129
|
+
mockUuidV4.mockReset();
|
|
130
|
+
});
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
vi.clearAllMocks();
|
|
133
|
+
});
|
|
134
|
+
describe('connect', () => {
|
|
135
|
+
it('returns unauthenticated when there is no auth context', async () => {
|
|
136
|
+
const store = await createStore(logger, auth, {
|
|
137
|
+
withSession: false,
|
|
138
|
+
});
|
|
139
|
+
const controller = createController(store, notificationService, logger, undefined);
|
|
140
|
+
await expect(controller.connect()).resolves.toEqual({
|
|
141
|
+
isConnected: false,
|
|
142
|
+
isNetworkConnected: false,
|
|
143
|
+
networkReason: 'Unauthenticated',
|
|
144
|
+
userUrl: `${userUrl}/login/`,
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
it('returns unauthenticated when there is no session', async () => {
|
|
148
|
+
const store = await createStore(logger, auth, {
|
|
149
|
+
withSession: false,
|
|
150
|
+
});
|
|
151
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
152
|
+
await expect(controller.connect()).resolves.toEqual({
|
|
153
|
+
isConnected: false,
|
|
154
|
+
isNetworkConnected: false,
|
|
155
|
+
networkReason: 'Unauthenticated',
|
|
156
|
+
userUrl: `${userUrl}/login/`,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
it('connects and emits statusChanged and connected', async () => {
|
|
160
|
+
const store = await createStore(logger, auth);
|
|
161
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
162
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
163
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
164
|
+
const result = await controller.connect();
|
|
165
|
+
expect(result).toMatchObject({
|
|
166
|
+
isConnected: true,
|
|
167
|
+
reason: 'OK',
|
|
168
|
+
isNetworkConnected: true,
|
|
169
|
+
networkReason: 'OK',
|
|
170
|
+
userUrl: `${userUrl}/login/`,
|
|
171
|
+
});
|
|
172
|
+
expect(mockNetworkStatus).toHaveBeenCalled();
|
|
173
|
+
expect(emitSpy).toHaveBeenCalledWith('statusChanged', expect.objectContaining({
|
|
174
|
+
connection: expect.objectContaining({
|
|
175
|
+
isConnected: true,
|
|
176
|
+
}),
|
|
177
|
+
session: {
|
|
178
|
+
accessToken: auth.accessToken,
|
|
179
|
+
userId: auth.userId,
|
|
180
|
+
},
|
|
181
|
+
}));
|
|
182
|
+
expect(emitSpy).toHaveBeenCalledWith('connected', expect.objectContaining({
|
|
183
|
+
provider: expect.objectContaining({ id: kernelInfo.id }),
|
|
184
|
+
}));
|
|
185
|
+
});
|
|
186
|
+
it('reflects disconnected ledger in network status', async () => {
|
|
187
|
+
mockNetworkStatus.mockResolvedValue({
|
|
188
|
+
isConnected: false,
|
|
189
|
+
reason: 'Ledger unreachable',
|
|
190
|
+
});
|
|
191
|
+
const store = await createStore(logger, auth);
|
|
192
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
193
|
+
const result = await controller.connect();
|
|
194
|
+
expect(result).toMatchObject({
|
|
195
|
+
isConnected: true,
|
|
196
|
+
isNetworkConnected: false,
|
|
197
|
+
networkReason: 'Ledger unreachable',
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
describe('disconnect', () => {
|
|
202
|
+
it('returns null when there is no auth context', async () => {
|
|
203
|
+
const store = await createStore(logger, auth);
|
|
204
|
+
const removeSessionSpy = vi.spyOn(store, 'removeSession');
|
|
205
|
+
const controller = createController(store, notificationService, logger, undefined);
|
|
206
|
+
await expect(controller.disconnect()).resolves.toBeNull();
|
|
207
|
+
expect(removeSessionSpy).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
it('removes the session and emits statusChanged', async () => {
|
|
210
|
+
const store = await createStore(logger, auth);
|
|
211
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
212
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
213
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
214
|
+
await controller.disconnect();
|
|
215
|
+
await expect(store.getSession()).resolves.toBeUndefined();
|
|
216
|
+
expect(emitSpy).toHaveBeenCalledWith('statusChanged', expect.objectContaining({
|
|
217
|
+
connection: expect.objectContaining({
|
|
218
|
+
isConnected: false,
|
|
219
|
+
reason: 'disconnect',
|
|
220
|
+
}),
|
|
221
|
+
}));
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('isConnected', () => {
|
|
225
|
+
it('returns unauthenticated when there is no session', async () => {
|
|
226
|
+
const store = await createStore(logger, auth, {
|
|
227
|
+
withSession: false,
|
|
228
|
+
});
|
|
229
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
230
|
+
await expect(controller.isConnected()).resolves.toEqual({
|
|
231
|
+
isConnected: false,
|
|
232
|
+
isNetworkConnected: false,
|
|
233
|
+
networkReason: 'Unauthenticated',
|
|
234
|
+
userUrl: `${userUrl}/login/`,
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
it('returns connected status when authenticated', async () => {
|
|
238
|
+
const store = await createStore(logger, auth);
|
|
239
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
240
|
+
await expect(controller.isConnected()).resolves.toEqual({
|
|
241
|
+
isConnected: true,
|
|
242
|
+
reason: 'OK',
|
|
243
|
+
isNetworkConnected: true,
|
|
244
|
+
networkReason: 'OK',
|
|
245
|
+
userUrl: `${userUrl}/login/`,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe('status', () => {
|
|
250
|
+
it('returns unauthenticated status when there is no session', async () => {
|
|
251
|
+
const store = await createStore(logger, auth, {
|
|
252
|
+
withSession: false,
|
|
253
|
+
});
|
|
254
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
255
|
+
const result = await controller.status();
|
|
256
|
+
expect(result.connection).toMatchObject({
|
|
257
|
+
isConnected: false,
|
|
258
|
+
reason: 'Unauthenticated',
|
|
259
|
+
});
|
|
260
|
+
expect(result.network).toBeUndefined();
|
|
261
|
+
});
|
|
262
|
+
it('returns full status when authenticated', async () => {
|
|
263
|
+
const store = await createStore(logger, auth);
|
|
264
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
265
|
+
const result = await controller.status();
|
|
266
|
+
expect(result).toMatchObject({
|
|
267
|
+
provider: expect.objectContaining({ id: kernelInfo.id }),
|
|
268
|
+
connection: {
|
|
269
|
+
isConnected: true,
|
|
270
|
+
reason: 'OK',
|
|
271
|
+
isNetworkConnected: true,
|
|
272
|
+
networkReason: 'OK',
|
|
273
|
+
},
|
|
274
|
+
network: {
|
|
275
|
+
networkId: storeNetwork.id,
|
|
276
|
+
ledgerApi: storeNetwork.ledgerApi.baseUrl,
|
|
277
|
+
accessToken: auth.accessToken,
|
|
278
|
+
},
|
|
279
|
+
session: {
|
|
280
|
+
id: session.id,
|
|
281
|
+
accessToken: auth.accessToken,
|
|
282
|
+
userId: auth.userId,
|
|
283
|
+
},
|
|
284
|
+
userUrl: `${userUrl}/login/`,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
describe('ledgerApi', () => {
|
|
289
|
+
it('performs GET via ledger client', async () => {
|
|
290
|
+
ledgerMocks.getWithRetry.mockResolvedValueOnce({ ok: true });
|
|
291
|
+
const store = await createStore(logger, auth);
|
|
292
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
293
|
+
const result = await controller.ledgerApi({
|
|
294
|
+
requestMethod: 'get',
|
|
295
|
+
resource: '/v2/parties',
|
|
296
|
+
path: { partyId: 'party::x' },
|
|
297
|
+
query: { limit: '10' },
|
|
298
|
+
});
|
|
299
|
+
expect(ledgerMocks.getWithRetry).toHaveBeenCalledWith('/v2/parties', undefined, {
|
|
300
|
+
path: { partyId: 'party::x' },
|
|
301
|
+
query: { limit: '10' },
|
|
302
|
+
});
|
|
303
|
+
expect(result).toEqual({ ok: true });
|
|
304
|
+
});
|
|
305
|
+
it('performs POST via ledger client', async () => {
|
|
306
|
+
ledgerMocks.postWithRetry.mockResolvedValueOnce({ created: true });
|
|
307
|
+
const store = await createStore(logger, auth);
|
|
308
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
309
|
+
const result = await controller.ledgerApi({
|
|
310
|
+
requestMethod: 'post',
|
|
311
|
+
resource: '/v2/commands/submit-and-wait',
|
|
312
|
+
body: { commands: [] },
|
|
313
|
+
query: { wait: 'true' },
|
|
314
|
+
});
|
|
315
|
+
expect(ledgerMocks.postWithRetry).toHaveBeenCalledWith('/v2/commands/submit-and-wait', { commands: [] }, undefined, { query: { wait: 'true' }, path: {} });
|
|
316
|
+
expect(result).toEqual({ created: true });
|
|
317
|
+
});
|
|
318
|
+
it('throws for unsupported request methods', async () => {
|
|
319
|
+
const store = await createStore(logger, auth);
|
|
320
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
321
|
+
await expect(controller.ledgerApi({
|
|
322
|
+
requestMethod: 'patch',
|
|
323
|
+
resource: '/v2/parties',
|
|
324
|
+
})).rejects.toThrow('Unsupported request method: patch');
|
|
325
|
+
});
|
|
326
|
+
it('throws when auth context is missing', async () => {
|
|
327
|
+
const store = await createStore(logger, auth);
|
|
328
|
+
const controller = createController(store, notificationService, logger, undefined);
|
|
329
|
+
await expect(controller.ledgerApi({
|
|
330
|
+
requestMethod: 'get',
|
|
331
|
+
resource: '/v2/parties',
|
|
332
|
+
})).rejects.toThrow();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
describe('prepareExecute', () => {
|
|
336
|
+
const prepareParams = {
|
|
337
|
+
commands: [
|
|
338
|
+
{
|
|
339
|
+
CreateCommand: {
|
|
340
|
+
templateId: 'pkg:Mod:T',
|
|
341
|
+
createArguments: {},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
};
|
|
346
|
+
it('throws when auth context is missing', async () => {
|
|
347
|
+
const store = await createStore(logger, auth);
|
|
348
|
+
const controller = createController(store, notificationService, logger, undefined);
|
|
349
|
+
await expect(controller.prepareExecute(prepareParams)).rejects.toThrow('Unauthenticated context');
|
|
350
|
+
});
|
|
351
|
+
it('throws when there is no primary wallet', async () => {
|
|
352
|
+
const store = await createStore(logger, auth, { withWallet: false });
|
|
353
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
354
|
+
await expect(controller.prepareExecute(prepareParams)).rejects.toThrow('No primary wallet found');
|
|
355
|
+
});
|
|
356
|
+
it('prepares a transaction and returns the approve URL', async () => {
|
|
357
|
+
mockUuidV4.mockReturnValueOnce('generated-command-id');
|
|
358
|
+
mockUuidV4.mockReturnValueOnce('transaction-id');
|
|
359
|
+
ledgerMocks.postWithRetry.mockResolvedValueOnce({
|
|
360
|
+
preparedTransaction: 'prepared-blob',
|
|
361
|
+
preparedTransactionHash: 'hash',
|
|
362
|
+
});
|
|
363
|
+
const store = await createStore(logger, auth);
|
|
364
|
+
const setTransactionSpy = vi.spyOn(store, 'setTransaction');
|
|
365
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
366
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
367
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
368
|
+
const result = await controller.prepareExecute(prepareParams);
|
|
369
|
+
expect(emitSpy).toHaveBeenCalledWith('txChanged', {
|
|
370
|
+
status: 'pending',
|
|
371
|
+
commandId: 'generated-command-id',
|
|
372
|
+
});
|
|
373
|
+
expect(ledgerMocks.postWithRetry).toHaveBeenCalledWith('/v2/interactive-submission/prepare', expect.any(Object));
|
|
374
|
+
expect(setTransactionSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
375
|
+
id: 'transaction-id',
|
|
376
|
+
commandId: 'generated-command-id',
|
|
377
|
+
status: 'pending',
|
|
378
|
+
preparedTransaction: 'prepared-blob',
|
|
379
|
+
preparedTransactionHash: 'hash',
|
|
380
|
+
origin,
|
|
381
|
+
}));
|
|
382
|
+
expect(result.userUrl).toBe(`${userUrl}/approve/index.html?transactionId=transaction-id&commandId=generated-command-id&closeafteraction`);
|
|
383
|
+
});
|
|
384
|
+
it('uses the provided commandId when present', async () => {
|
|
385
|
+
mockUuidV4.mockReturnValueOnce('transaction-id');
|
|
386
|
+
ledgerMocks.postWithRetry.mockResolvedValueOnce({
|
|
387
|
+
preparedTransaction: 'prepared-blob',
|
|
388
|
+
preparedTransactionHash: 'hash',
|
|
389
|
+
});
|
|
390
|
+
const store = await createStore(logger, auth);
|
|
391
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
392
|
+
await controller.prepareExecute({
|
|
393
|
+
...prepareParams,
|
|
394
|
+
commandId: 'existing-command-id',
|
|
395
|
+
});
|
|
396
|
+
expect(mockUuidV4).toHaveBeenCalledOnce();
|
|
397
|
+
expect(ledgerMocks.postWithRetry.mock.calls[0][1]).toMatchObject({
|
|
398
|
+
commandId: 'existing-command-id',
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
it('fetches synchronizerId from ledger when not on network', async () => {
|
|
402
|
+
const networkWithoutSync = {
|
|
403
|
+
...storeNetwork,
|
|
404
|
+
synchronizerId: undefined,
|
|
405
|
+
};
|
|
406
|
+
const store = new StoreInternal({ idps: [idp], networks: [networkWithoutSync] }, logger, auth);
|
|
407
|
+
await store.setSession(session);
|
|
408
|
+
await store.addWallet(primaryWallet);
|
|
409
|
+
mockUuidV4.mockReturnValueOnce('cmd-id');
|
|
410
|
+
mockUuidV4.mockReturnValueOnce('tx-id');
|
|
411
|
+
ledgerMocks.postWithRetry.mockResolvedValueOnce({
|
|
412
|
+
preparedTransaction: 'blob',
|
|
413
|
+
preparedTransactionHash: 'hash',
|
|
414
|
+
});
|
|
415
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
416
|
+
await controller.prepareExecute(prepareParams);
|
|
417
|
+
expect(ledgerMocks.getSynchronizerId).toHaveBeenCalled();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
describe('signMessage', () => {
|
|
421
|
+
it('throws when message is missing', async () => {
|
|
422
|
+
const store = await createStore(logger, auth);
|
|
423
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
424
|
+
await expect(controller.signMessage({ message: '' })).rejects.toThrow('Message is required');
|
|
425
|
+
});
|
|
426
|
+
it('throws when auth context is missing', async () => {
|
|
427
|
+
const store = await createStore(logger, auth);
|
|
428
|
+
const controller = createController(store, notificationService, logger, undefined);
|
|
429
|
+
await expect(controller.signMessage({ message: 'hello' })).rejects.toThrow('Unauthenticated context');
|
|
430
|
+
});
|
|
431
|
+
it('throws when there is no primary wallet', async () => {
|
|
432
|
+
const store = await createStore(logger, auth, { withWallet: false });
|
|
433
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
434
|
+
await expect(controller.signMessage({ message: 'hello' })).rejects.toThrow('No primary wallet found');
|
|
435
|
+
});
|
|
436
|
+
it('stores a pending message and returns the sign URL', async () => {
|
|
437
|
+
mockUuidV4.mockReturnValueOnce('message-id');
|
|
438
|
+
const store = await createStore(logger, auth);
|
|
439
|
+
const setMessageSpy = vi.spyOn(store, 'setMessageRaw');
|
|
440
|
+
const notifier = notificationService.getNotifier(auth.userId);
|
|
441
|
+
const emitSpy = vi.spyOn(notifier, 'emit');
|
|
442
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
443
|
+
const result = await controller.signMessage({ message: 'hello' });
|
|
444
|
+
expect(setMessageSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
445
|
+
id: 'message-id',
|
|
446
|
+
status: 'pending',
|
|
447
|
+
userId: auth.userId,
|
|
448
|
+
partyId: primaryWallet.partyId,
|
|
449
|
+
publicKey: primaryWallet.publicKey,
|
|
450
|
+
message: 'hello',
|
|
451
|
+
origin,
|
|
452
|
+
}));
|
|
453
|
+
expect(emitSpy).toHaveBeenCalledWith('messageSignature', {
|
|
454
|
+
status: 'pending',
|
|
455
|
+
messageId: 'message-id',
|
|
456
|
+
});
|
|
457
|
+
expect(result).toEqual({
|
|
458
|
+
messageId: 'message-id',
|
|
459
|
+
userUrl: `${userUrl}/sign-message/index.html?messageId=message-id&closeafteraction`,
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
describe('accounts', () => {
|
|
464
|
+
it('lists accounts from the store', async () => {
|
|
465
|
+
const store = await createStore(logger, auth);
|
|
466
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
467
|
+
const accounts = await controller.listAccounts();
|
|
468
|
+
expect(accounts).toHaveLength(1);
|
|
469
|
+
expect(accounts[0]?.partyId).toBe(primaryWallet.partyId);
|
|
470
|
+
});
|
|
471
|
+
it('returns the primary account', async () => {
|
|
472
|
+
const store = await createStore(logger, auth);
|
|
473
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
474
|
+
await expect(controller.getPrimaryAccount()).resolves.toEqual(primaryWallet);
|
|
475
|
+
});
|
|
476
|
+
it('throws when there is no primary account', async () => {
|
|
477
|
+
const store = await createStore(logger, auth, { withWallet: false });
|
|
478
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
479
|
+
await expect(controller.getPrimaryAccount()).rejects.toThrow('No primary wallet found');
|
|
480
|
+
});
|
|
481
|
+
it('returns the active network with access token when authenticated', async () => {
|
|
482
|
+
const store = await createStore(logger, auth);
|
|
483
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
484
|
+
await expect(controller.getActiveNetwork()).resolves.toEqual({
|
|
485
|
+
networkId: storeNetwork.id,
|
|
486
|
+
ledgerApi: storeNetwork.ledgerApi.baseUrl,
|
|
487
|
+
accessToken: auth.accessToken,
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
it('returns the active network without access token when unauthenticated', async () => {
|
|
491
|
+
const store = await createStore(logger, auth);
|
|
492
|
+
const controller = createController(store, notificationService, logger, undefined);
|
|
493
|
+
await expect(controller.getActiveNetwork()).resolves.toEqual({
|
|
494
|
+
networkId: storeNetwork.id,
|
|
495
|
+
ledgerApi: storeNetwork.ledgerApi.baseUrl,
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
describe('event-only methods', () => {
|
|
500
|
+
it.each([
|
|
501
|
+
'connected',
|
|
502
|
+
'onStatusChanged',
|
|
503
|
+
'accountsChanged',
|
|
504
|
+
'txChanged',
|
|
505
|
+
])('%s throws Only for events', async (method) => {
|
|
506
|
+
const store = await createStore(logger, auth);
|
|
507
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
508
|
+
await expect(controller[method]()).rejects.toThrow('Only for events.');
|
|
509
|
+
});
|
|
510
|
+
it('messageSignature throws Only for events', async () => {
|
|
511
|
+
const store = await createStore(logger, auth);
|
|
512
|
+
const controller = createController(store, notificationService, logger, auth);
|
|
513
|
+
expect(() => controller.messageSignature()).toThrow('Only for events.');
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
|
@@ -12,6 +12,7 @@ import { AccountsChanged } from './typings.js';
|
|
|
12
12
|
import { GetPrimaryAccount } from './typings.js';
|
|
13
13
|
import { ListAccounts } from './typings.js';
|
|
14
14
|
import { TxChanged } from './typings.js';
|
|
15
|
+
import { MessageSignature } from './typings.js';
|
|
15
16
|
export type Methods = {
|
|
16
17
|
status: Status;
|
|
17
18
|
connect: Connect;
|
|
@@ -27,6 +28,7 @@ export type Methods = {
|
|
|
27
28
|
getPrimaryAccount: GetPrimaryAccount;
|
|
28
29
|
listAccounts: ListAccounts;
|
|
29
30
|
txChanged: TxChanged;
|
|
31
|
+
messageSignature: MessageSignature;
|
|
30
32
|
};
|
|
31
33
|
declare function buildController(methods: Methods): {
|
|
32
34
|
status: Status;
|
|
@@ -43,6 +45,7 @@ declare function buildController(methods: Methods): {
|
|
|
43
45
|
getPrimaryAccount: GetPrimaryAccount;
|
|
44
46
|
listAccounts: ListAccounts;
|
|
45
47
|
txChanged: TxChanged;
|
|
48
|
+
messageSignature: MessageSignature;
|
|
46
49
|
};
|
|
47
50
|
export default buildController;
|
|
48
51
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dapp-api/rpc-gen/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dapp-api/rpc-gen/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAE/C,MAAM,MAAM,OAAO,GAAG;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,UAAU,CAAA;IACtB,WAAW,EAAE,WAAW,CAAA;IACxB,gBAAgB,EAAE,gBAAgB,CAAA;IAClC,cAAc,EAAE,cAAc,CAAA;IAC9B,WAAW,EAAE,WAAW,CAAA;IACxB,SAAS,EAAE,SAAS,CAAA;IACpB,SAAS,EAAE,SAAS,CAAA;IACpB,eAAe,EAAE,eAAe,CAAA;IAChC,eAAe,EAAE,eAAe,CAAA;IAChC,iBAAiB,EAAE,iBAAiB,CAAA;IACpC,YAAY,EAAE,YAAY,CAAA;IAC1B,SAAS,EAAE,SAAS,CAAA;IACpB,gBAAgB,EAAE,gBAAgB,CAAA;CACrC,CAAA;AAED,iBAAS,eAAe,CAAC,OAAO,EAAE,OAAO;;;;;;;;;;;;;;;;EAkBxC;AAED,eAAe,eAAe,CAAA"}
|