@canton-network/core-wallet-store-inmemory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # wallet-store
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Store.test.d.ts","sourceRoot":"","sources":["../src/Store.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,158 @@
1
+ import { beforeEach, describe, expect, test } from '@jest/globals';
2
+ import { StoreInternal } from './StoreInternal';
3
+ import { pino } from 'pino';
4
+ import { sink } from 'pino-test';
5
+ const authContextMock = {
6
+ userId: 'test-user-id',
7
+ accessToken: 'test-access-token',
8
+ };
9
+ const storeConfig = {
10
+ networks: [],
11
+ };
12
+ const implementations = [
13
+ ['StoreInternal', StoreInternal],
14
+ ];
15
+ implementations.forEach(([name, StoreImpl]) => {
16
+ describe(name, () => {
17
+ let store;
18
+ beforeEach(() => {
19
+ store = new StoreImpl(storeConfig, pino(sink()), authContextMock);
20
+ });
21
+ test('should add and retrieve wallets', async () => {
22
+ const wallet = {
23
+ primary: false,
24
+ partyId: 'party1',
25
+ hint: 'hint',
26
+ signingProviderId: 'internal',
27
+ publicKey: 'publicKey',
28
+ namespace: 'namespace',
29
+ chainId: 'network1',
30
+ };
31
+ await store.addWallet(wallet);
32
+ const wallets = await store.getWallets();
33
+ expect(wallets).toHaveLength(1);
34
+ });
35
+ test('should filter wallets', async () => {
36
+ const wallet1 = {
37
+ primary: false,
38
+ partyId: 'party1',
39
+ hint: 'hint1',
40
+ signingProviderId: 'internal1',
41
+ publicKey: 'publicKey',
42
+ namespace: 'namespace',
43
+ chainId: 'network1',
44
+ };
45
+ const wallet2 = {
46
+ primary: false,
47
+ partyId: 'party2',
48
+ hint: 'hint2',
49
+ signingProviderId: 'internal2',
50
+ publicKey: 'publicKey',
51
+ namespace: 'namespace',
52
+ chainId: 'network1',
53
+ };
54
+ const wallet3 = {
55
+ primary: false,
56
+ partyId: 'party3',
57
+ hint: 'hint3',
58
+ signingProviderId: 'internal2',
59
+ publicKey: 'publicKey',
60
+ namespace: 'namespace',
61
+ chainId: 'network2',
62
+ };
63
+ await store.addWallet(wallet1);
64
+ await store.addWallet(wallet2);
65
+ await store.addWallet(wallet3);
66
+ const getAllWallets = await store.getWallets();
67
+ const getWalletsByChainId = await store.getWallets({
68
+ chainIds: ['network1'],
69
+ });
70
+ const getWalletsBySigningProviderId = await store.getWallets({
71
+ signingProviderIds: ['internal2'],
72
+ });
73
+ const getWalletsByChainIdAndSigningProviderId = await store.getWallets({
74
+ chainIds: ['network1'],
75
+ signingProviderIds: ['internal2'],
76
+ });
77
+ expect(getAllWallets).toHaveLength(3);
78
+ expect(getWalletsByChainId).toHaveLength(2);
79
+ expect(getWalletsBySigningProviderId).toHaveLength(2);
80
+ expect(getWalletsByChainIdAndSigningProviderId).toHaveLength(1);
81
+ });
82
+ test('should set and get primary wallet', async () => {
83
+ const wallet1 = {
84
+ primary: false,
85
+ partyId: 'party1',
86
+ hint: 'hint1',
87
+ signingProviderId: 'internal',
88
+ publicKey: 'publicKey',
89
+ namespace: 'namespace',
90
+ chainId: 'network1',
91
+ };
92
+ const wallet2 = {
93
+ primary: false,
94
+ partyId: 'party2',
95
+ hint: 'hint2',
96
+ signingProviderId: 'internal',
97
+ publicKey: 'publicKey',
98
+ namespace: 'namespace',
99
+ chainId: 'network1',
100
+ };
101
+ await store.addWallet(wallet1);
102
+ await store.addWallet(wallet2);
103
+ await store.setPrimaryWallet('party2');
104
+ const primary = await store.getPrimaryWallet();
105
+ expect(primary?.partyId).toBe('party2');
106
+ expect(primary?.primary).toBe(true);
107
+ });
108
+ test('should set and get session', async () => {
109
+ const session = { network: 'net', accessToken: 'token' };
110
+ await store.setSession(session);
111
+ const result = await store.getSession();
112
+ expect(result).toEqual(session);
113
+ await store.removeSession();
114
+ const removed = await store.getSession();
115
+ expect(removed).toBeUndefined();
116
+ });
117
+ test('should add, list, get, update, and remove networks', async () => {
118
+ const ledgerApi = {
119
+ baseUrl: 'http://api',
120
+ adminGrpcUrl: 'http://grpc',
121
+ };
122
+ const auth = {
123
+ identityProviderId: 'idp1',
124
+ type: 'password',
125
+ issuer: 'http://auth',
126
+ configUrl: 'http://auth/.well-known/openid-configuration',
127
+ tokenUrl: 'http://auth',
128
+ grantType: 'password',
129
+ clientId: 'cid',
130
+ scope: 'scope',
131
+ audience: 'aud',
132
+ };
133
+ const network = {
134
+ name: 'testnet',
135
+ chainId: 'network1',
136
+ synchronizerId: 'sync1::fingerprint',
137
+ description: 'Test Network',
138
+ ledgerApi,
139
+ auth,
140
+ };
141
+ await store.updateNetwork(network);
142
+ const listed = await store.listNetworks();
143
+ expect(listed).toHaveLength(1);
144
+ expect(listed[0].name).toBe('testnet');
145
+ const fetched = await store.getNetwork('network1');
146
+ expect(fetched.description).toBe('Test Network');
147
+ await store.removeNetwork('network1');
148
+ const afterRemove = await store.listNetworks();
149
+ expect(afterRemove).toHaveLength(0);
150
+ });
151
+ test('should throw when getting a non-existent network', async () => {
152
+ await expect(store.getNetwork('doesnotexist')).rejects.toThrow();
153
+ });
154
+ test('should throw when getting current network if none set', async () => {
155
+ await expect(store.getCurrentNetwork()).rejects.toThrow();
156
+ });
157
+ });
158
+ });
@@ -0,0 +1,42 @@
1
+ import { Logger } from 'pino';
2
+ import { AuthContext, UserId, AuthAware } from '@canton-network/core-wallet-auth';
3
+ import { Store, Wallet, PartyId, Session, WalletFilter, Transaction, Network } from '@canton-network/core-wallet-store';
4
+ interface UserStorage {
5
+ wallets: Array<Wallet>;
6
+ transactions: Map<string, Transaction>;
7
+ session: Session | undefined;
8
+ }
9
+ export interface StoreInternalConfig {
10
+ networks: Array<Network>;
11
+ }
12
+ type Memory = Map<UserId, UserStorage>;
13
+ export declare class StoreInternal implements Store, AuthAware<StoreInternal> {
14
+ private logger;
15
+ private systemStorage;
16
+ private userStorage;
17
+ authContext: AuthContext | undefined;
18
+ constructor(config: StoreInternalConfig, logger: Logger, authContext?: AuthContext, userStorage?: Memory);
19
+ withAuthContext(context?: AuthContext): StoreInternal;
20
+ static createStorage(): UserStorage;
21
+ private assertConnected;
22
+ private getStorage;
23
+ private updateStorage;
24
+ private syncWallets;
25
+ getWallets(filter?: WalletFilter): Promise<Array<Wallet>>;
26
+ getPrimaryWallet(): Promise<Wallet | undefined>;
27
+ setPrimaryWallet(partyId: PartyId): Promise<void>;
28
+ addWallet(wallet: Wallet): Promise<void>;
29
+ getSession(): Promise<Session | undefined>;
30
+ setSession(session: Session): Promise<void>;
31
+ removeSession(): Promise<void>;
32
+ getNetwork(chainId: string): Promise<Network>;
33
+ getCurrentNetwork(): Promise<Network>;
34
+ listNetworks(): Promise<Array<Network>>;
35
+ updateNetwork(network: Network): Promise<void>;
36
+ addNetwork(network: Network): Promise<void>;
37
+ removeNetwork(chainId: string): Promise<void>;
38
+ setTransaction(transaction: Transaction): Promise<void>;
39
+ getTransaction(commandId: string): Promise<Transaction | undefined>;
40
+ }
41
+ export {};
42
+ //# sourceMappingURL=StoreInternal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StoreInternal.d.ts","sourceRoot":"","sources":["../src/StoreInternal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EACH,WAAW,EACX,MAAM,EACN,SAAS,EACZ,MAAM,kCAAkC,CAAA;AACzC,OAAO,EACH,KAAK,EACL,MAAM,EACN,OAAO,EACP,OAAO,EACP,YAAY,EACZ,WAAW,EACX,OAAO,EACV,MAAM,mCAAmC,CAAA;AAG1C,UAAU,WAAW;IACjB,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACtB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACtC,OAAO,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAChC,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;CAC3B;AAED,KAAK,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;AAGtC,qBAAa,aAAc,YAAW,KAAK,EAAE,SAAS,CAAC,aAAa,CAAC;IACjE,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,WAAW,CAAQ;IAE3B,WAAW,EAAE,WAAW,GAAG,SAAS,CAAA;gBAGhC,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,MAAM;IAUxB,eAAe,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa;IASrD,MAAM,CAAC,aAAa,IAAI,WAAW;IAQnC,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,aAAa;YAOP,WAAW;IAqEnB,UAAU,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAkB7D,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAK/C,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBjD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBxC,UAAU,IAAI,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAI1C,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAM3C,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAO9B,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAW7C,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC;IAkBrC,YAAY,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAIvC,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAM9C,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAW3C,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ7C,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAQvD,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;CAM5E"}
@@ -0,0 +1,225 @@
1
+ import { LedgerClient } from '@canton-network/core-ledger-client';
2
+ // TODO: remove AuthAware and instead provide wrapper in clients
3
+ export class StoreInternal {
4
+ logger;
5
+ systemStorage;
6
+ userStorage;
7
+ authContext;
8
+ constructor(config, logger, authContext, userStorage) {
9
+ this.logger = logger.child({ component: 'StoreInternal' });
10
+ this.systemStorage = config;
11
+ this.authContext = authContext;
12
+ this.userStorage = userStorage || new Map();
13
+ this.syncWallets();
14
+ }
15
+ withAuthContext(context) {
16
+ return new StoreInternal(this.systemStorage, this.logger, context, this.userStorage);
17
+ }
18
+ static createStorage() {
19
+ return {
20
+ wallets: [],
21
+ transactions: new Map(),
22
+ session: undefined,
23
+ };
24
+ }
25
+ assertConnected() {
26
+ if (!this.authContext) {
27
+ throw new Error('User is not connected');
28
+ }
29
+ return this.authContext.userId;
30
+ }
31
+ getStorage() {
32
+ const userId = this.assertConnected();
33
+ if (!this.userStorage.has(userId)) {
34
+ this.userStorage.set(userId, StoreInternal.createStorage());
35
+ }
36
+ return this.userStorage.get(userId);
37
+ }
38
+ updateStorage(storage) {
39
+ const userId = this.assertConnected();
40
+ this.userStorage.set(userId, storage);
41
+ }
42
+ // Wallet methods
43
+ async syncWallets() {
44
+ try {
45
+ const network = await this.getCurrentNetwork();
46
+ // Get existing parties from participant
47
+ const ledgerClient = new LedgerClient(network.ledgerApi.baseUrl, this.authContext.accessToken, this.logger);
48
+ const rights = await ledgerClient.get('/v2/users/{user-id}/rights', {
49
+ path: {
50
+ 'user-id': this.authContext.userId,
51
+ },
52
+ });
53
+ const parties = rights.rights
54
+ ?.filter((right) => 'CanActAs' in right.kind)
55
+ .map((right) => {
56
+ if ('CanActAs' in right.kind) {
57
+ return right.kind.CanActAs.value.party;
58
+ }
59
+ throw new Error('Unexpected right kind');
60
+ });
61
+ // Merge Wallets
62
+ const existingWallets = await this.getWallets();
63
+ const existingPartyIds = new Set(existingWallets.map((w) => w.partyId));
64
+ const participantWallets = parties
65
+ ?.filter((party) => !existingPartyIds.has(party)
66
+ // todo: filter on idp id
67
+ )
68
+ .map((party) => {
69
+ const [hint, namespace] = party.split('::');
70
+ return {
71
+ primary: false,
72
+ partyId: party,
73
+ hint: hint,
74
+ publicKey: namespace,
75
+ namespace: namespace,
76
+ chainId: network.chainId,
77
+ signingProviderId: 'participant', // todo: determine based on partyDetails.isLocal
78
+ };
79
+ }) || [];
80
+ const storage = this.getStorage();
81
+ const wallets = [...storage.wallets, ...participantWallets];
82
+ // Set primary wallet if none exists
83
+ const hasPrimary = wallets.some((w) => w.primary);
84
+ if (!hasPrimary && wallets.length > 0) {
85
+ wallets[0].primary = true;
86
+ }
87
+ this.logger.debug(wallets, 'Wallets synchronized');
88
+ // Update storage with new wallets
89
+ storage.wallets = wallets;
90
+ this.updateStorage(storage);
91
+ }
92
+ catch {
93
+ return;
94
+ }
95
+ }
96
+ async getWallets(filter = {}) {
97
+ const { chainIds, signingProviderIds } = filter;
98
+ const chainIdSet = chainIds ? new Set(chainIds) : null;
99
+ const signingProviderIdSet = signingProviderIds
100
+ ? new Set(signingProviderIds)
101
+ : null;
102
+ return this.getStorage().wallets.filter((wallet) => {
103
+ const matchedChainIds = chainIdSet
104
+ ? chainIdSet.has(wallet.chainId)
105
+ : true;
106
+ const matchedStorageProviderIdS = signingProviderIdSet
107
+ ? signingProviderIdSet.has(wallet.signingProviderId)
108
+ : true;
109
+ return matchedChainIds && matchedStorageProviderIdS;
110
+ });
111
+ }
112
+ async getPrimaryWallet() {
113
+ const wallets = await this.getWallets();
114
+ return wallets.find((w) => w.primary === true);
115
+ }
116
+ async setPrimaryWallet(partyId) {
117
+ const storage = this.getStorage();
118
+ if (!storage.wallets.some((w) => w.partyId === partyId)) {
119
+ throw new Error(`Wallet with partyId "${partyId}" not found`);
120
+ }
121
+ const wallets = storage.wallets.map((w) => {
122
+ if (w.partyId === partyId) {
123
+ w.primary = true;
124
+ }
125
+ else {
126
+ w.primary = false;
127
+ }
128
+ return w;
129
+ });
130
+ storage.wallets = wallets;
131
+ this.updateStorage(storage);
132
+ }
133
+ async addWallet(wallet) {
134
+ const storage = this.getStorage();
135
+ if (storage.wallets.some((w) => w.partyId === wallet.partyId)) {
136
+ throw new Error(`Wallet with partyId "${wallet.partyId}" already exists`);
137
+ }
138
+ const wallets = await this.getWallets();
139
+ if (wallets.length === 0) {
140
+ // If this is the first wallet, set it as primary automatically
141
+ wallet.primary = true;
142
+ }
143
+ if (wallet.primary) {
144
+ // If the new wallet is primary, set all others to non-primary
145
+ storage.wallets.map((w) => (w.primary = false));
146
+ }
147
+ wallets.push(wallet);
148
+ storage.wallets = wallets;
149
+ this.updateStorage(storage);
150
+ }
151
+ // Session methods
152
+ async getSession() {
153
+ return this.getStorage().session;
154
+ }
155
+ async setSession(session) {
156
+ const storage = this.getStorage();
157
+ storage.session = session;
158
+ this.updateStorage(storage);
159
+ }
160
+ async removeSession() {
161
+ const storage = this.getStorage();
162
+ storage.session = undefined;
163
+ this.updateStorage(storage);
164
+ }
165
+ // Network methods
166
+ async getNetwork(chainId) {
167
+ this.assertConnected();
168
+ const networks = await this.listNetworks();
169
+ if (!networks)
170
+ throw new Error('No networks available');
171
+ const network = networks.find((n) => n.chainId === chainId);
172
+ if (!network)
173
+ throw new Error(`Network "${chainId}" not found`);
174
+ return network;
175
+ }
176
+ async getCurrentNetwork() {
177
+ const session = this.getStorage().session;
178
+ if (!session) {
179
+ throw new Error('No session found');
180
+ }
181
+ const chainId = session.network;
182
+ if (!chainId) {
183
+ throw new Error('No current network set in session');
184
+ }
185
+ const networks = await this.listNetworks();
186
+ const network = networks.find((n) => n.chainId === chainId);
187
+ if (!network) {
188
+ throw new Error(`Network "${chainId}" not found`);
189
+ }
190
+ return network;
191
+ }
192
+ async listNetworks() {
193
+ return this.systemStorage.networks;
194
+ }
195
+ async updateNetwork(network) {
196
+ this.assertConnected();
197
+ this.removeNetwork(network.chainId); // Ensure no duplicates
198
+ this.systemStorage.networks.push(network);
199
+ }
200
+ async addNetwork(network) {
201
+ const networkAlreadyExists = this.systemStorage.networks.find((n) => n.chainId === network.chainId);
202
+ if (networkAlreadyExists) {
203
+ throw new Error(`Network ${network.chainId} already exists`);
204
+ }
205
+ else {
206
+ this.systemStorage.networks.push(network);
207
+ }
208
+ }
209
+ async removeNetwork(chainId) {
210
+ this.assertConnected();
211
+ this.systemStorage.networks = this.systemStorage.networks.filter((n) => n.chainId !== chainId);
212
+ }
213
+ // Transaction methods
214
+ async setTransaction(transaction) {
215
+ this.assertConnected();
216
+ const storage = this.getStorage();
217
+ storage.transactions.set(transaction.commandId, transaction);
218
+ this.updateStorage(storage);
219
+ }
220
+ async getTransaction(commandId) {
221
+ this.assertConnected();
222
+ const storage = this.getStorage();
223
+ return storage.transactions.get(commandId);
224
+ }
225
+ }
@@ -0,0 +1,2 @@
1
+ export * from './StoreInternal.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './StoreInternal.js';
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@canton-network/core-wallet-store-inmemory",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "In-memory implementation of the Store API",
6
+ "repository": "https://github.com/hyperledger-labs/splice-wallet-kernel",
7
+ "author": "Marc Juchli <marc.juchli@digitalasset.com>",
8
+ "license": "Apache-2.0",
9
+ "packageManager": "yarn@4.9.2",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "scripts": {
13
+ "build": "tsc -b",
14
+ "dev": "tsc -b --watch",
15
+ "clean": "tsc -b --clean; rm -rf dist",
16
+ "test": "yarn node --experimental-vm-modules $(yarn bin jest)"
17
+ },
18
+ "dependencies": {
19
+ "@canton-network/core-ledger-client": "^0.1.0",
20
+ "@canton-network/core-wallet-auth": "^0.1.0",
21
+ "@canton-network/core-wallet-store": "^0.1.0",
22
+ "pino": "^9.7.0",
23
+ "zod": "^3.25.64"
24
+ },
25
+ "devDependencies": {
26
+ "@jest/globals": "^29.0.0",
27
+ "@swc/core": "^1.11.31",
28
+ "@swc/jest": "^0.2.38",
29
+ "@types/jest": "^30.0.0",
30
+ "jest": "^30.0.0",
31
+ "pino-test": "^1.1.0",
32
+ "ts-jest": "^29.4.0",
33
+ "ts-jest-resolver": "^2.0.1",
34
+ "typescript": "^5.8.3"
35
+ },
36
+ "files": [
37
+ "dist/*"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }