@canton-network/core-wallet-store-sql 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
package/dist/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ import type { StoreConfig } from '@canton-network/core-wallet-store';
3
+ export declare function createCLI(config: StoreConfig): Command;
4
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAA;AAKpE,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAuEtD"}
package/dist/cli.js ADDED
@@ -0,0 +1,64 @@
1
+ import { Command } from 'commander';
2
+ import { connection, StoreSql } from './store-sql.js';
3
+ import { migrator } from './migrator.js';
4
+ import { pino } from 'pino';
5
+ const logger = pino({ name: 'main', level: 'debug' });
6
+ export function createCLI(config) {
7
+ console.log('Wallet Store Sql CLI');
8
+ const program = new Command();
9
+ program
10
+ .command('up')
11
+ .description('Run all pending migrations')
12
+ .action(async () => {
13
+ const db = connection(config);
14
+ const umzug = migrator(db);
15
+ await umzug.up();
16
+ await db.destroy();
17
+ });
18
+ program
19
+ .command('down')
20
+ .description('Rollback last migration')
21
+ .action(async () => {
22
+ const db = connection(config);
23
+ const umzug = migrator(db);
24
+ await umzug.down();
25
+ await db.destroy();
26
+ });
27
+ program
28
+ .command('status')
29
+ .description('Show executed and pending migrations')
30
+ .action(async () => {
31
+ const db = connection(config);
32
+ const umzug = migrator(db);
33
+ const executed = await umzug.executed();
34
+ const pending = await umzug.pending();
35
+ console.log('Executed migrations:', executed);
36
+ console.log('Pending migrations:', pending);
37
+ await db.destroy();
38
+ });
39
+ program
40
+ .command('reset')
41
+ .description('Rollback all migrations and reapply them')
42
+ .action(async () => {
43
+ const db = connection(config);
44
+ const umzug = migrator(db);
45
+ const executed = await umzug.executed();
46
+ // Rollback all executed migrations in reverse order
47
+ for (const migration of executed.reverse()) {
48
+ await umzug.down({ to: migration.name });
49
+ }
50
+ // Reapply all migrations
51
+ await umzug.up();
52
+ await db.destroy();
53
+ });
54
+ program
55
+ .command('bootstrap')
56
+ .description('Bootstrap DB from config')
57
+ .action(async () => {
58
+ const db = connection(config);
59
+ const store = new StoreSql(db, logger);
60
+ await Promise.all(config.networks.map((network) => store.addNetwork(network)));
61
+ await db.destroy();
62
+ });
63
+ return program;
64
+ }
@@ -0,0 +1,4 @@
1
+ export * from './store-sql.js';
2
+ export * from './migrator.js';
3
+ export * from './cli.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './store-sql.js';
2
+ export * from './migrator.js';
3
+ export * from './cli.js';
@@ -0,0 +1,5 @@
1
+ import { Umzug } from 'umzug';
2
+ import { Kysely } from 'kysely';
3
+ import { DB } from './schema';
4
+ export declare const migrator: (db: Kysely<DB>) => Umzug<Kysely<DB>>;
5
+ //# sourceMappingURL=migrator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrator.d.ts","sourceRoot":"","sources":["../src/migrator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAA+B,MAAM,OAAO,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,EAAE,EAAE,MAAM,UAAU,CAAA;AAwC7B,eAAO,MAAM,QAAQ,GAAI,IAAI,MAAM,CAAC,EAAE,CAAC,sBA0BtC,CAAA"}
@@ -0,0 +1,64 @@
1
+ import { Umzug } from 'umzug';
2
+ class KyselyStorage {
3
+ db;
4
+ constructor(db) {
5
+ this.db = db;
6
+ }
7
+ async ensureTable() {
8
+ await this.db.schema
9
+ .createTable('migrations')
10
+ .ifNotExists()
11
+ .addColumn('name', 'text', (col) => col.primaryKey())
12
+ .addColumn('executedAt', 'text', (col) => col.notNull())
13
+ .execute();
14
+ }
15
+ async executed() {
16
+ await this.ensureTable();
17
+ const rows = await this.db
18
+ .selectFrom('migrations')
19
+ .select('name')
20
+ .execute();
21
+ return rows.map((r) => r.name);
22
+ }
23
+ async logMigration({ name }) {
24
+ await this.ensureTable();
25
+ await this.db
26
+ .insertInto('migrations')
27
+ .values({ name, executedAt: new Date().toISOString() })
28
+ .execute();
29
+ }
30
+ async unlogMigration({ name }) {
31
+ await this.ensureTable();
32
+ await this.db
33
+ .deleteFrom('migrations')
34
+ .where('name', '=', name)
35
+ .execute();
36
+ }
37
+ }
38
+ export const migrator = (db) => {
39
+ const ext = import.meta.url.endsWith('.ts') ? 'ts' : 'js';
40
+ const glob = new URL(`./migrations/*.${ext}`, import.meta.url).pathname;
41
+ return new Umzug({
42
+ migrations: {
43
+ glob: glob,
44
+ resolve: ({ name, path, context }) => {
45
+ // Dynamic import for ESM
46
+ return {
47
+ name,
48
+ up: async () => {
49
+ console.log(path);
50
+ const { up } = await import(path);
51
+ return up(context);
52
+ },
53
+ down: async () => {
54
+ const { down } = await import(path);
55
+ return down(context);
56
+ },
57
+ };
58
+ },
59
+ },
60
+ context: db,
61
+ storage: new KyselyStorage(db),
62
+ logger: console,
63
+ });
64
+ };
@@ -0,0 +1,69 @@
1
+ import { UserId } from '@canton-network/core-wallet-auth';
2
+ import { Wallet, Transaction, Session, Auth, Network } from '@canton-network/core-wallet-store';
3
+ interface MigrationTable {
4
+ name: string;
5
+ executedAt: string;
6
+ }
7
+ interface IdpTable {
8
+ identityProviderId: string;
9
+ type: string;
10
+ issuer: string;
11
+ configUrl: string;
12
+ audience: string;
13
+ tokenUrl: string;
14
+ grantType: string;
15
+ scope: string;
16
+ clientId: string;
17
+ clientSecret: string;
18
+ adminClientId: string;
19
+ adminClientSecret: string;
20
+ }
21
+ interface NetworkTable {
22
+ name: string;
23
+ chainId: string;
24
+ synchronizerId: string;
25
+ description: string;
26
+ ledgerApiBaseUrl: string;
27
+ ledgerApiAdminGrpcUrl: string;
28
+ userId: UserId | undefined;
29
+ identityProviderId: string;
30
+ }
31
+ interface WalletTable {
32
+ primary: number;
33
+ partyId: string;
34
+ hint: string;
35
+ publicKey: string;
36
+ namespace: string;
37
+ chainId: string;
38
+ signingProviderId: string;
39
+ userId: UserId;
40
+ }
41
+ interface TransactionTable {
42
+ status: string;
43
+ commandId: string;
44
+ preparedTransaction: string;
45
+ preparedTransactionHash: string;
46
+ payload: string | undefined;
47
+ userId: UserId;
48
+ }
49
+ interface SessionTable extends Session {
50
+ userId: UserId;
51
+ }
52
+ export interface DB {
53
+ migrations: MigrationTable;
54
+ idps: IdpTable;
55
+ networks: NetworkTable;
56
+ wallets: WalletTable;
57
+ transactions: TransactionTable;
58
+ sessions: SessionTable;
59
+ }
60
+ export declare const toAuth: (table: IdpTable) => Auth;
61
+ export declare const fromAuth: (auth: Auth) => IdpTable;
62
+ export declare const toNetwork: (table: NetworkTable, authTable?: IdpTable) => Network;
63
+ export declare const fromNetwork: (network: Network, userId?: UserId) => NetworkTable;
64
+ export declare const fromWallet: (wallet: Wallet, userId: UserId) => WalletTable;
65
+ export declare const toWallet: (table: WalletTable) => Wallet;
66
+ export declare const fromTransaction: (transaction: Transaction, userId: UserId) => TransactionTable;
67
+ export declare const toTransaction: (table: TransactionTable) => Transaction;
68
+ export {};
69
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAA;AACzD,OAAO,EACH,MAAM,EACN,WAAW,EACX,OAAO,EACP,IAAI,EACJ,OAAO,EACV,MAAM,mCAAmC,CAAA;AAE1C,UAAU,cAAc;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACrB;AAED,UAAU,QAAQ;IACd,kBAAkB,EAAE,MAAM,CAAA;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB,EAAE,MAAM,CAAA;CAC5B;AAED,UAAU,YAAY;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,EAAE,MAAM,CAAA;IACxB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,kBAAkB,EAAE,MAAM,CAAA;CAC7B;AAED,UAAU,WAAW;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,iBAAiB,EAAE,MAAM,CAAA;IACzB,MAAM,EAAE,MAAM,CAAA;CACjB;AAED,UAAU,gBAAgB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,uBAAuB,EAAE,MAAM,CAAA;IAC/B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;CACjB;AAED,UAAU,YAAa,SAAQ,OAAO;IAClC,MAAM,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,EAAE;IACf,UAAU,EAAE,cAAc,CAAA;IAC1B,IAAI,EAAE,QAAQ,CAAA;IACd,QAAQ,EAAE,YAAY,CAAA;IACtB,OAAO,EAAE,WAAW,CAAA;IACpB,YAAY,EAAE,gBAAgB,CAAA;IAC9B,QAAQ,EAAE,YAAY,CAAA;CACzB;AAED,eAAO,MAAM,MAAM,GAAI,OAAO,QAAQ,KAAG,IAkDxC,CAAA;AAED,eAAO,MAAM,QAAQ,GAAI,MAAM,IAAI,KAAG,QAkDrC,CAAA;AAED,eAAO,MAAM,SAAS,GAClB,OAAO,YAAY,EACnB,YAAY,QAAQ,KACrB,OAeF,CAAA;AAED,eAAO,MAAM,WAAW,GACpB,SAAS,OAAO,EAChB,SAAS,MAAM,KAChB,YAWF,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,EAAE,QAAQ,MAAM,KAAG,WAM3D,CAAA;AAED,eAAO,MAAM,QAAQ,GAAI,OAAO,WAAW,KAAG,MAK7C,CAAA;AAED,eAAO,MAAM,eAAe,GACxB,aAAa,WAAW,EACxB,QAAQ,MAAM,KACf,gBAQF,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,OAAO,gBAAgB,KAAG,WAMvD,CAAA"}
package/dist/schema.js ADDED
@@ -0,0 +1,159 @@
1
+ export const toAuth = (table) => {
2
+ switch (table.type) {
3
+ case 'password':
4
+ return {
5
+ identityProviderId: table.identityProviderId,
6
+ type: table.type,
7
+ issuer: table.issuer,
8
+ configUrl: table.configUrl,
9
+ audience: table.audience,
10
+ tokenUrl: table.tokenUrl || '',
11
+ grantType: table.grantType || '',
12
+ scope: table.scope,
13
+ clientId: table.clientId,
14
+ admin: {
15
+ clientId: table.adminClientId,
16
+ clientSecret: table.adminClientSecret,
17
+ },
18
+ };
19
+ case 'implicit':
20
+ return {
21
+ identityProviderId: table.identityProviderId,
22
+ type: table.type,
23
+ issuer: table.issuer,
24
+ configUrl: table.configUrl,
25
+ audience: table.audience,
26
+ scope: table.scope,
27
+ clientId: table.clientId,
28
+ admin: {
29
+ clientId: table.adminClientId,
30
+ clientSecret: table.adminClientSecret,
31
+ },
32
+ };
33
+ case 'client_credentials':
34
+ return {
35
+ identityProviderId: table.identityProviderId,
36
+ type: table.type,
37
+ issuer: table.issuer,
38
+ configUrl: table.configUrl,
39
+ audience: table.audience,
40
+ scope: table.scope,
41
+ clientId: table.clientId,
42
+ clientSecret: table.clientSecret,
43
+ admin: {
44
+ clientId: table.adminClientId,
45
+ clientSecret: table.adminClientSecret,
46
+ },
47
+ };
48
+ default:
49
+ throw new Error(`Unknown auth type: ${table.type}`);
50
+ }
51
+ };
52
+ export const fromAuth = (auth) => {
53
+ switch (auth.type) {
54
+ case 'password':
55
+ return {
56
+ identityProviderId: auth.identityProviderId,
57
+ type: auth.type,
58
+ issuer: auth.issuer,
59
+ configUrl: auth.configUrl,
60
+ audience: auth.audience,
61
+ tokenUrl: auth.tokenUrl,
62
+ grantType: auth.grantType,
63
+ scope: auth.scope,
64
+ clientId: auth.clientId,
65
+ clientSecret: '',
66
+ adminClientId: auth.admin?.clientId || '',
67
+ adminClientSecret: auth.admin?.clientSecret || '',
68
+ };
69
+ case 'implicit':
70
+ return {
71
+ identityProviderId: auth.identityProviderId,
72
+ type: auth.type,
73
+ issuer: auth.issuer,
74
+ configUrl: auth.configUrl,
75
+ audience: auth.audience,
76
+ tokenUrl: '',
77
+ grantType: '',
78
+ scope: auth.scope,
79
+ clientId: auth.clientId,
80
+ clientSecret: '',
81
+ adminClientId: auth.admin?.clientId || '',
82
+ adminClientSecret: auth.admin?.clientSecret || '',
83
+ };
84
+ case 'client_credentials':
85
+ return {
86
+ identityProviderId: auth.identityProviderId,
87
+ type: auth.type,
88
+ issuer: auth.issuer,
89
+ configUrl: auth.configUrl,
90
+ audience: auth.audience,
91
+ tokenUrl: '',
92
+ grantType: '',
93
+ scope: auth.scope,
94
+ clientId: auth.clientId,
95
+ clientSecret: auth.clientSecret,
96
+ adminClientId: auth.admin?.clientId || '',
97
+ adminClientSecret: auth.admin?.clientSecret || '',
98
+ };
99
+ default:
100
+ throw new Error(`Unknown auth type`);
101
+ }
102
+ };
103
+ export const toNetwork = (table, authTable) => {
104
+ if (!authTable) {
105
+ throw new Error(`Missing auth table for network: ${table.name}`);
106
+ }
107
+ return {
108
+ name: table.name,
109
+ chainId: table.chainId,
110
+ synchronizerId: table.synchronizerId,
111
+ description: table.description,
112
+ ledgerApi: {
113
+ baseUrl: table.ledgerApiBaseUrl,
114
+ adminGrpcUrl: table.ledgerApiAdminGrpcUrl,
115
+ },
116
+ auth: toAuth(authTable),
117
+ };
118
+ };
119
+ export const fromNetwork = (network, userId) => {
120
+ return {
121
+ name: network.name,
122
+ chainId: network.chainId,
123
+ synchronizerId: network.synchronizerId,
124
+ description: network.description,
125
+ ledgerApiBaseUrl: network.ledgerApi.baseUrl,
126
+ ledgerApiAdminGrpcUrl: network.ledgerApi.adminGrpcUrl,
127
+ userId: userId,
128
+ identityProviderId: network.auth.identityProviderId,
129
+ };
130
+ };
131
+ export const fromWallet = (wallet, userId) => {
132
+ return {
133
+ ...wallet,
134
+ primary: wallet.primary ? 1 : 0,
135
+ userId: userId,
136
+ };
137
+ };
138
+ export const toWallet = (table) => {
139
+ return {
140
+ ...table,
141
+ primary: table.primary === 1,
142
+ };
143
+ };
144
+ export const fromTransaction = (transaction, userId) => {
145
+ return {
146
+ ...transaction,
147
+ payload: transaction.payload
148
+ ? JSON.stringify(transaction.payload)
149
+ : undefined,
150
+ userId: userId,
151
+ };
152
+ };
153
+ export const toTransaction = (table) => {
154
+ return {
155
+ ...table,
156
+ status: table.status,
157
+ payload: table.payload ? JSON.parse(table.payload) : undefined,
158
+ };
159
+ };
@@ -0,0 +1,30 @@
1
+ import { Logger } from 'pino';
2
+ import { AuthContext, AuthAware } from '@canton-network/core-wallet-auth';
3
+ import { Store as BaseStore, Wallet, PartyId, Session, WalletFilter, Transaction, Network, StoreConfig } from '@canton-network/core-wallet-store';
4
+ import { Kysely } from 'kysely';
5
+ import { DB } from './schema.js';
6
+ export declare class StoreSql implements BaseStore, AuthAware<StoreSql> {
7
+ private db;
8
+ private logger;
9
+ authContext: AuthContext | undefined;
10
+ constructor(db: Kysely<DB>, logger: Logger, authContext?: AuthContext);
11
+ withAuthContext(context?: AuthContext): StoreSql;
12
+ private assertConnected;
13
+ getWallets(filter?: WalletFilter): Promise<Array<Wallet>>;
14
+ getPrimaryWallet(): Promise<Wallet | undefined>;
15
+ setPrimaryWallet(partyId: PartyId): Promise<void>;
16
+ addWallet(wallet: Wallet): Promise<void>;
17
+ getSession(): Promise<Session | undefined>;
18
+ setSession(session: Session): Promise<void>;
19
+ removeSession(): Promise<void>;
20
+ getNetwork(chainId: string): Promise<Network>;
21
+ getCurrentNetwork(): Promise<Network>;
22
+ listNetworks(): Promise<Array<Network>>;
23
+ updateNetwork(network: Network): Promise<void>;
24
+ addNetwork(network: Network): Promise<void>;
25
+ removeNetwork(chainId: string): Promise<void>;
26
+ setTransaction(transaction: Transaction): Promise<void>;
27
+ getTransaction(commandId: string): Promise<Transaction | undefined>;
28
+ }
29
+ export declare const connection: (config: StoreConfig) => Kysely<DB>;
30
+ //# sourceMappingURL=store-sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-sql.d.ts","sourceRoot":"","sources":["../src/store-sql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EACH,WAAW,EAEX,SAAS,EACZ,MAAM,kCAAkC,CAAA;AACzC,OAAO,EACH,KAAK,IAAI,SAAS,EAClB,MAAM,EACN,OAAO,EACP,OAAO,EACP,YAAY,EACZ,WAAW,EACX,OAAO,EACP,WAAW,EACd,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAmB,MAAM,EAAiB,MAAM,QAAQ,CAAA;AAE/D,OAAO,EACH,EAAE,EAQL,MAAM,aAAa,CAAA;AAEpB,qBAAa,QAAS,YAAW,SAAS,EAAE,SAAS,CAAC,QAAQ,CAAC;IAIvD,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,MAAM;IAJlB,WAAW,EAAE,WAAW,GAAG,SAAS,CAAA;gBAGxB,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,MAAM,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,WAAW;IAQ7B,eAAe,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,QAAQ;IAIhD,OAAO,CAAC,eAAe;IASjB,UAAU,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IA2B7D,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAK/C,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBjD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCxC,UAAU,IAAI,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAQ1C,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAc3C,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B,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;IAuBvC,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB9C,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB3C,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4B7C,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBvD,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;CAc5E;AAED,eAAO,MAAM,UAAU,GAAI,QAAQ,WAAW,eAqB7C,CAAA"}
@@ -0,0 +1,287 @@
1
+ import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely';
2
+ import Database from 'better-sqlite3';
3
+ import { fromAuth, fromNetwork, fromTransaction, fromWallet, toNetwork, toTransaction, toWallet, } from './schema.js';
4
+ export class StoreSql {
5
+ db;
6
+ logger;
7
+ authContext;
8
+ constructor(db, logger, authContext) {
9
+ this.db = db;
10
+ this.logger = logger;
11
+ this.logger = logger.child({ component: 'StoreInternal' });
12
+ this.authContext = authContext;
13
+ // this.syncWallets()
14
+ }
15
+ withAuthContext(context) {
16
+ return new StoreSql(this.db, this.logger, context);
17
+ }
18
+ assertConnected() {
19
+ if (!this.authContext) {
20
+ throw new Error('User is not connected');
21
+ }
22
+ return this.authContext.userId;
23
+ }
24
+ // Wallet methods
25
+ async getWallets(filter = {}) {
26
+ const userId = this.assertConnected();
27
+ const { chainIds, signingProviderIds } = filter;
28
+ const chainIdSet = chainIds ? new Set(chainIds) : null;
29
+ const signingProviderIdSet = signingProviderIds
30
+ ? new Set(signingProviderIds)
31
+ : null;
32
+ const wallets = await this.db
33
+ .selectFrom('wallets')
34
+ .selectAll()
35
+ .where('userId', '=', userId)
36
+ .execute();
37
+ return wallets
38
+ .filter((wallet) => {
39
+ const matchedChainIds = chainIdSet
40
+ ? chainIdSet.has(wallet.chainId)
41
+ : true;
42
+ const matchedStorageProviderIdS = signingProviderIdSet
43
+ ? signingProviderIdSet.has(wallet.signingProviderId)
44
+ : true;
45
+ return matchedChainIds && matchedStorageProviderIdS;
46
+ })
47
+ .map((table) => toWallet(table));
48
+ }
49
+ async getPrimaryWallet() {
50
+ const wallets = await this.getWallets();
51
+ return wallets.find((w) => w.primary === true);
52
+ }
53
+ async setPrimaryWallet(partyId) {
54
+ const wallets = await this.getWallets();
55
+ if (!wallets.some((w) => w.partyId === partyId)) {
56
+ throw new Error(`Wallet with partyId "${partyId}" not found`);
57
+ }
58
+ const primary = wallets.find((w) => w.primary === true);
59
+ await this.db.transaction().execute(async (trx) => {
60
+ if (primary) {
61
+ await trx
62
+ .updateTable('wallets')
63
+ .set({ primary: 0 })
64
+ .where('partyId', '=', primary.partyId)
65
+ .execute();
66
+ }
67
+ await trx
68
+ .updateTable('wallets')
69
+ .set({ primary: 1 })
70
+ .where('partyId', '=', partyId)
71
+ .execute();
72
+ });
73
+ }
74
+ async addWallet(wallet) {
75
+ const userId = this.assertConnected();
76
+ const wallets = await this.getWallets();
77
+ if (wallets.some((w) => w.partyId === wallet.partyId)) {
78
+ throw new Error(`Wallet with partyId "${wallet.partyId}" already exists`);
79
+ }
80
+ if (wallets.length === 0) {
81
+ // If this is the first wallet, set it as primary automatically
82
+ wallet.primary = true;
83
+ }
84
+ await this.db.transaction().execute(async (trx) => {
85
+ if (wallet.primary) {
86
+ // If the new wallet is primary, set all others to non-primary
87
+ await trx
88
+ .updateTable('wallets')
89
+ .set({ primary: 0 })
90
+ .where((eb) => eb.and([
91
+ eb('primary', '=', 1),
92
+ eb('userId', '=', userId),
93
+ ]))
94
+ .execute();
95
+ }
96
+ await trx
97
+ .insertInto('wallets')
98
+ .values(fromWallet(wallet, userId))
99
+ .execute();
100
+ });
101
+ }
102
+ // Session methods
103
+ async getSession() {
104
+ const sessions = await this.db
105
+ .selectFrom('sessions')
106
+ .selectAll()
107
+ .executeTakeFirst();
108
+ return sessions;
109
+ }
110
+ async setSession(session) {
111
+ const userId = this.assertConnected();
112
+ await this.db.transaction().execute(async (trx) => {
113
+ await trx
114
+ .deleteFrom('sessions')
115
+ .where('userId', '=', userId)
116
+ .execute();
117
+ await trx
118
+ .insertInto('sessions')
119
+ .values({ ...session, userId })
120
+ .execute();
121
+ });
122
+ }
123
+ async removeSession() {
124
+ const userId = this.assertConnected();
125
+ await this.db
126
+ .deleteFrom('sessions')
127
+ .where('userId', '=', userId)
128
+ .execute();
129
+ }
130
+ // Network methods
131
+ async getNetwork(chainId) {
132
+ this.assertConnected();
133
+ const networks = await this.listNetworks();
134
+ if (!networks)
135
+ throw new Error('No networks available');
136
+ const network = networks.find((n) => n.chainId === chainId);
137
+ if (!network)
138
+ throw new Error(`Network "${chainId}" not found`);
139
+ return network;
140
+ }
141
+ async getCurrentNetwork() {
142
+ const session = await this.getSession();
143
+ if (!session) {
144
+ throw new Error('No session found');
145
+ }
146
+ const chainId = session.network;
147
+ if (!chainId) {
148
+ throw new Error('No current network set in session');
149
+ }
150
+ const networks = await this.listNetworks();
151
+ const network = networks.find((n) => n.chainId === chainId);
152
+ if (!network) {
153
+ throw new Error(`Network "${chainId}" not found`);
154
+ }
155
+ return network;
156
+ }
157
+ async listNetworks() {
158
+ let query = this.db.selectFrom('networks').selectAll();
159
+ if (this.authContext) {
160
+ const userId = this.assertConnected();
161
+ query = query.where((eb) => eb.or([
162
+ eb('userId', 'is', null), // Global networks
163
+ eb('userId', '=', userId), // User-specific networks
164
+ ]));
165
+ }
166
+ else {
167
+ query = query.where('userId', 'is', null); // Only global networks
168
+ }
169
+ const networks = await query.execute();
170
+ const idps = await this.db.selectFrom('idps').selectAll().execute();
171
+ const idpMap = new Map(idps.map((idp) => [idp.identityProviderId, idp]));
172
+ return networks.map((table) => toNetwork(table, idpMap.get(table.identityProviderId)));
173
+ }
174
+ async updateNetwork(network) {
175
+ const userId = this.assertConnected();
176
+ // todo: check and compare userid of existing network
177
+ await this.db.transaction().execute(async (trx) => {
178
+ await trx
179
+ .updateTable('networks')
180
+ .set(fromNetwork(network, userId))
181
+ .where('chainId', '=', network.chainId)
182
+ .execute();
183
+ await trx
184
+ .updateTable('idps')
185
+ .set(fromAuth(network.auth))
186
+ .where('identityProviderId', '=', network.auth.identityProviderId)
187
+ .execute();
188
+ });
189
+ }
190
+ async addNetwork(network) {
191
+ const userId = this.authContext?.userId;
192
+ await this.db.transaction().execute(async (trx) => {
193
+ const networkAlreadyExists = await trx
194
+ .selectFrom('networks')
195
+ .selectAll()
196
+ .where('chainId', '=', network.chainId)
197
+ .executeTakeFirst();
198
+ if (networkAlreadyExists) {
199
+ throw new Error(`Network ${network.chainId} already exists`);
200
+ }
201
+ else {
202
+ await trx
203
+ .insertInto('idps')
204
+ .values(fromAuth(network.auth))
205
+ .execute();
206
+ await trx
207
+ .insertInto('networks')
208
+ .values(fromNetwork(network, userId))
209
+ .execute();
210
+ }
211
+ });
212
+ }
213
+ async removeNetwork(chainId) {
214
+ const userId = this.assertConnected();
215
+ await this.db.transaction().execute(async (trx) => {
216
+ const network = await trx
217
+ .selectFrom('networks')
218
+ .selectAll()
219
+ .where('chainId', '=', chainId)
220
+ .executeTakeFirst();
221
+ if (!network) {
222
+ throw new Error(`Network ${chainId} does not exists`);
223
+ }
224
+ if (network.userId !== userId) {
225
+ throw new Error(`Network ${chainId} is not owned by user ${userId}`);
226
+ }
227
+ await trx
228
+ .deleteFrom('networks')
229
+ .where('chainId', '=', chainId)
230
+ .execute();
231
+ await trx
232
+ .deleteFrom('idps')
233
+ .where('identityProviderId', '=', network.identityProviderId)
234
+ .execute();
235
+ });
236
+ }
237
+ // Transaction methods
238
+ async setTransaction(transaction) {
239
+ const userId = this.assertConnected();
240
+ const existing = await this.getTransaction(transaction.commandId);
241
+ if (existing) {
242
+ await this.db
243
+ .updateTable('transactions')
244
+ .set(fromTransaction(transaction, userId))
245
+ .where('commandId', '=', transaction.commandId)
246
+ .execute();
247
+ }
248
+ else {
249
+ await this.db
250
+ .insertInto('transactions')
251
+ .values(fromTransaction(transaction, userId))
252
+ .execute();
253
+ }
254
+ }
255
+ async getTransaction(commandId) {
256
+ const userId = this.assertConnected();
257
+ const transaction = await this.db
258
+ .selectFrom('transactions')
259
+ .selectAll()
260
+ .where((eb) => eb.and([
261
+ eb('commandId', '=', commandId),
262
+ eb('userId', '=', userId),
263
+ ]))
264
+ .executeTakeFirst();
265
+ return transaction ? toTransaction(transaction) : undefined;
266
+ }
267
+ }
268
+ export const connection = (config) => {
269
+ switch (config.connection.type) {
270
+ case 'sqlite':
271
+ return new Kysely({
272
+ dialect: new SqliteDialect({
273
+ database: new Database(config.connection.database),
274
+ }),
275
+ plugins: [new CamelCasePlugin()],
276
+ });
277
+ case 'memory':
278
+ return new Kysely({
279
+ dialect: new SqliteDialect({
280
+ database: new Database(':memory:'),
281
+ }),
282
+ plugins: [new CamelCasePlugin()],
283
+ });
284
+ default:
285
+ throw new Error(`Unsupported database type: ${config.connection.type}`);
286
+ }
287
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=store-sql.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-sql.test.d.ts","sourceRoot":"","sources":["../src/store-sql.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,206 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import { pino } from 'pino';
3
+ import { sink } from 'pino-test';
4
+ import { migrator } from './migrator';
5
+ import { connection, StoreSql } from './store-sql';
6
+ const authContextMock = {
7
+ userId: 'test-user-id',
8
+ accessToken: 'test-access-token',
9
+ };
10
+ const storeConfig = {
11
+ connection: {
12
+ type: 'memory',
13
+ },
14
+ networks: [],
15
+ };
16
+ const implementations = [['StoreSql', StoreSql]];
17
+ const ledgerApi = {
18
+ baseUrl: 'http://api',
19
+ adminGrpcUrl: 'http://grpc',
20
+ };
21
+ const auth = {
22
+ identityProviderId: 'idp1',
23
+ type: 'password',
24
+ issuer: 'http://auth',
25
+ configUrl: 'http://auth/.well-known/openid-configuration',
26
+ tokenUrl: 'http://auth',
27
+ grantType: 'password',
28
+ clientId: 'cid',
29
+ scope: 'scope',
30
+ audience: 'aud',
31
+ };
32
+ const network = {
33
+ name: 'testnet',
34
+ chainId: 'network1',
35
+ synchronizerId: 'sync1::fingerprint',
36
+ description: 'Test Network',
37
+ ledgerApi,
38
+ auth,
39
+ };
40
+ implementations.forEach(([name, StoreImpl]) => {
41
+ describe(name, () => {
42
+ let db;
43
+ beforeEach(async () => {
44
+ db = connection(storeConfig);
45
+ const umzug = migrator(db);
46
+ await umzug.up();
47
+ });
48
+ afterEach(async () => {
49
+ await db.destroy();
50
+ });
51
+ test('should add and retrieve wallets', async () => {
52
+ const wallet = {
53
+ primary: false,
54
+ partyId: 'party1',
55
+ hint: 'hint',
56
+ signingProviderId: 'internal',
57
+ publicKey: 'publicKey',
58
+ namespace: 'namespace',
59
+ chainId: 'network1',
60
+ };
61
+ const store = new StoreImpl(db, pino(sink()), authContextMock);
62
+ await store.addNetwork(network);
63
+ await store.addWallet(wallet);
64
+ const wallets = await store.getWallets();
65
+ expect(wallets).toHaveLength(1);
66
+ });
67
+ test('should filter wallets', async () => {
68
+ const auth2 = {
69
+ identityProviderId: 'idp2',
70
+ type: 'password',
71
+ issuer: 'http://auth',
72
+ configUrl: 'http://auth/.well-known/openid-configuration',
73
+ tokenUrl: 'http://auth',
74
+ grantType: 'password',
75
+ clientId: 'cid',
76
+ scope: 'scope',
77
+ audience: 'aud',
78
+ };
79
+ const network2 = {
80
+ name: 'testnet',
81
+ chainId: 'network2',
82
+ synchronizerId: 'sync1::fingerprint',
83
+ description: 'Test Network',
84
+ ledgerApi,
85
+ auth: auth2,
86
+ };
87
+ const wallet1 = {
88
+ primary: false,
89
+ partyId: 'party1',
90
+ hint: 'hint1',
91
+ signingProviderId: 'internal',
92
+ publicKey: 'publicKey',
93
+ namespace: 'namespace',
94
+ chainId: 'network1',
95
+ };
96
+ const wallet2 = {
97
+ primary: false,
98
+ partyId: 'party2',
99
+ hint: 'hint2',
100
+ signingProviderId: 'internal',
101
+ publicKey: 'publicKey',
102
+ namespace: 'namespace',
103
+ chainId: 'network1',
104
+ };
105
+ const wallet3 = {
106
+ primary: false,
107
+ partyId: 'party3',
108
+ hint: 'hint3',
109
+ signingProviderId: 'internal',
110
+ publicKey: 'publicKey',
111
+ namespace: 'namespace',
112
+ chainId: 'network2',
113
+ };
114
+ const store = new StoreImpl(db, pino(sink()), authContextMock);
115
+ await store.addNetwork(network);
116
+ await store.addNetwork(network2);
117
+ await store.addWallet(wallet1);
118
+ await store.addWallet(wallet2);
119
+ await store.addWallet(wallet3);
120
+ const getAllWallets = await store.getWallets();
121
+ const getWalletsByChainId = await store.getWallets({
122
+ chainIds: ['network1'],
123
+ });
124
+ const getWalletsBySigningProviderId = await store.getWallets({
125
+ signingProviderIds: ['internal'],
126
+ });
127
+ const getWalletsByChainIdAndSigningProviderId = await store.getWallets({
128
+ chainIds: ['network1'],
129
+ signingProviderIds: ['internal'],
130
+ });
131
+ expect(getAllWallets).toHaveLength(3);
132
+ expect(getWalletsByChainId).toHaveLength(2);
133
+ expect(getWalletsBySigningProviderId).toHaveLength(3);
134
+ expect(getWalletsByChainIdAndSigningProviderId).toHaveLength(2);
135
+ });
136
+ test('should set and get primary wallet', async () => {
137
+ const wallet1 = {
138
+ primary: false,
139
+ partyId: 'party1',
140
+ hint: 'hint1',
141
+ signingProviderId: 'internal',
142
+ publicKey: 'publicKey',
143
+ namespace: 'namespace',
144
+ chainId: 'network1',
145
+ };
146
+ const wallet2 = {
147
+ primary: false,
148
+ partyId: 'party2',
149
+ hint: 'hint2',
150
+ signingProviderId: 'internal',
151
+ publicKey: 'publicKey',
152
+ namespace: 'namespace',
153
+ chainId: 'network1',
154
+ };
155
+ const store = new StoreImpl(db, pino(sink()), authContextMock);
156
+ await store.addNetwork(network);
157
+ await store.addWallet(wallet1);
158
+ await store.addWallet(wallet2);
159
+ await store.setPrimaryWallet('party2');
160
+ const primary = await store.getPrimaryWallet();
161
+ expect(primary?.partyId).toBe('party2');
162
+ expect(primary?.primary).toBe(true);
163
+ });
164
+ test('should set and get session', async () => {
165
+ const store = new StoreImpl(db, pino(sink()), authContextMock);
166
+ await store.addNetwork(network);
167
+ const session = {
168
+ network: 'network1',
169
+ accessToken: 'token',
170
+ };
171
+ await store.setSession(session);
172
+ const result = await store.getSession();
173
+ expect(result).toEqual({
174
+ ...session,
175
+ userId: authContextMock.userId,
176
+ });
177
+ await store.removeSession();
178
+ const removed = await store.getSession();
179
+ expect(removed).toBeUndefined();
180
+ });
181
+ test('should add, list, get, update, and remove networks', async () => {
182
+ const store = new StoreImpl(db, pino(sink()), authContextMock);
183
+ await store.addNetwork(network);
184
+ const listed = await store.listNetworks();
185
+ expect(listed).toHaveLength(1);
186
+ expect(listed[0].description).toBe('Test Network');
187
+ await store.updateNetwork({
188
+ ...network,
189
+ description: 'Updated Network',
190
+ });
191
+ const fetched = await store.getNetwork('network1');
192
+ expect(fetched.description).toBe('Updated Network');
193
+ await store.removeNetwork('network1');
194
+ const afterRemove = await store.listNetworks();
195
+ expect(afterRemove).toHaveLength(0);
196
+ });
197
+ test('should throw when getting a non-existent network', async () => {
198
+ const store = new StoreImpl(db, pino(sink()), authContextMock);
199
+ await expect(store.getNetwork('doesnotexist')).rejects.toThrow();
200
+ });
201
+ test('should throw when getting current network if none set', async () => {
202
+ const store = new StoreImpl(db, pino(sink()), authContextMock);
203
+ await expect(store.getCurrentNetwork()).rejects.toThrow();
204
+ });
205
+ });
206
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@canton-network/core-wallet-store-sql",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "repository": "https://github.com/hyperledger-labs/splice-wallet-kernel",
6
+ "description": "SQL implementation of the Store API",
7
+ "license": "Apache-2.0",
8
+ "author": "Marc Juchli <marc.juchli@digitalasset.com>",
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
+ "better-sqlite3": "^12.2.0",
23
+ "commander": "^14.0.0",
24
+ "kysely": "^0.28.5",
25
+ "pino": "^9.7.0",
26
+ "umzug": "^3.8.2",
27
+ "zod": "^3.25.64"
28
+ },
29
+ "devDependencies": {
30
+ "@jest/globals": "^29.0.0",
31
+ "@swc/core": "^1.11.31",
32
+ "@swc/jest": "^0.2.38",
33
+ "@types/better-sqlite3": "^7.6.13",
34
+ "@types/jest": "^30.0.0",
35
+ "jest": "^30.0.0",
36
+ "pino-test": "^1.1.0",
37
+ "ts-jest": "^29.4.0",
38
+ "ts-jest-resolver": "^2.0.1",
39
+ "tsx": "^4.20.4",
40
+ "typescript": "^5.8.3"
41
+ },
42
+ "files": [
43
+ "dist/*"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }