From 29c33fefd94c762e2fcf61eb77cd1fe67e397007 Mon Sep 17 00:00:00 2001 From: PixelPlex Dev team <10460630+pixelplex@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:22:55 +0300 Subject: [PATCH 01/16] feat: implement postgresql connection (#869) Signed-off-by: Marc Juchli --- core/signing-internal/src/controller.ts | 6 +- core/signing-store-sql/package.json | 2 + core/signing-store-sql/src/bootstrap.ts | 3 +- core/signing-store-sql/src/cli.ts | 2 +- .../src/migrations/001-init.ts | 8 +- core/signing-store-sql/src/schema.ts | 23 +- core/signing-store-sql/src/store-sql.ts | 26 +- core/wallet-store-sql/package.json | 2 + core/wallet-store-sql/src/schema.ts | 12 +- core/wallet-store-sql/src/store-sql.ts | 20 +- wallet-gateway/remote/README.md | 37 ++ wallet-gateway/remote/package.json | 8 +- wallet-gateway/remote/src/index.ts | 13 +- wallet-gateway/remote/src/init.ts | 54 ++ yarn.lock | 503 +++++++++++++++--- 15 files changed, 632 insertions(+), 87 deletions(-) diff --git a/core/signing-internal/src/controller.ts b/core/signing-internal/src/controller.ts index 2964d73dc..443ce042c 100644 --- a/core/signing-internal/src/controller.ts +++ b/core/signing-internal/src/controller.ts @@ -186,6 +186,7 @@ export class InternalSigningDriver implements SigningDriverInterface { convertInternalTransaction({ ...tx, signature: tx.signature || 'signed', + createdAt: new Date(tx.createdAt), }) ), }) @@ -236,13 +237,14 @@ export class InternalSigningDriver implements SigningDriverInterface { const { publicKey, privateKey } = createKeyPair() const id = randomUUID() + const now = new Date() const internalKey: SigningKey = { id, name: params.name, publicKey, privateKey, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: now, + updatedAt: now, } await this.store.setSigningKey(_userId, internalKey) diff --git a/core/signing-store-sql/package.json b/core/signing-store-sql/package.json index ba15db5f3..84a84904f 100644 --- a/core/signing-store-sql/package.json +++ b/core/signing-store-sql/package.json @@ -32,6 +32,7 @@ "better-sqlite3": "^12.6.2", "commander": "^14.0.2", "kysely": "^0.28.10", + "pg": "^8.16.3", "pino": "^10.2.1", "umzug": "^3.8.2", "zod": "^4.3.5" @@ -39,6 +40,7 @@ "devDependencies": { "@swc/core": "^1.15.10", "@types/better-sqlite3": "^7.6.13", + "@types/pg": "^8", "tsup": "^8.5.1", "tsx": "^4.21.0", "typescript": "^5.9.3" diff --git a/core/signing-store-sql/src/bootstrap.ts b/core/signing-store-sql/src/bootstrap.ts index 9fd4f071c..a4f7b5116 100644 --- a/core/signing-store-sql/src/bootstrap.ts +++ b/core/signing-store-sql/src/bootstrap.ts @@ -3,9 +3,8 @@ import { Kysely } from 'kysely' import { StoreSql } from './store-sql.js' -import { StoreConfig } from '@canton-network/core-wallet-store' import { Logger } from 'pino' -import { DB } from './schema' +import { DB, StoreConfig } from './schema' export async function bootstrap( db: Kysely, diff --git a/core/signing-store-sql/src/cli.ts b/core/signing-store-sql/src/cli.ts index 5345c6cac..e45f3366f 100644 --- a/core/signing-store-sql/src/cli.ts +++ b/core/signing-store-sql/src/cli.ts @@ -4,9 +4,9 @@ import { Command } from 'commander' import { connection } from './store-sql.js' import { migrator } from './migrator.js' -import type { StoreConfig } from '@canton-network/core-wallet-store' import { pino } from 'pino' import { bootstrap } from './bootstrap.js' +import { StoreConfig } from './schema.js' const logger = pino({ name: 'main', level: 'debug' }) diff --git a/core/signing-store-sql/src/migrations/001-init.ts b/core/signing-store-sql/src/migrations/001-init.ts index 1fd755bde..fe8f7cdfe 100644 --- a/core/signing-store-sql/src/migrations/001-init.ts +++ b/core/signing-store-sql/src/migrations/001-init.ts @@ -17,8 +17,8 @@ export async function up(db: Kysely): Promise { .addColumn('public_key', 'text', (col) => col.notNull()) .addColumn('private_key', 'text') // Encrypted for internal driver .addColumn('metadata', 'text') // JSON string for driver-specific data - .addColumn('created_at', 'integer', (col) => col.notNull()) - .addColumn('updated_at', 'integer', (col) => col.notNull()) + .addColumn('created_at', 'text', (col) => col.notNull()) + .addColumn('updated_at', 'text', (col) => col.notNull()) .addUniqueConstraint('signing_keys_user_id_id_unique', [ 'user_id', 'id', @@ -36,8 +36,8 @@ export async function up(db: Kysely): Promise { .addColumn('public_key', 'text', (col) => col.notNull()) .addColumn('status', 'text', (col) => col.notNull()) .addColumn('metadata', 'text') // JSON string for driver-specific data - .addColumn('created_at', 'integer', (col) => col.notNull()) - .addColumn('updated_at', 'integer', (col) => col.notNull()) + .addColumn('created_at', 'text', (col) => col.notNull()) + .addColumn('updated_at', 'text', (col) => col.notNull()) .addUniqueConstraint('signing_transactions_user_id_id_unique', [ 'user_id', 'id', diff --git a/core/signing-store-sql/src/schema.ts b/core/signing-store-sql/src/schema.ts index 7a014f7e8..df898db54 100644 --- a/core/signing-store-sql/src/schema.ts +++ b/core/signing-store-sql/src/schema.ts @@ -92,7 +92,14 @@ export const toSigningKey = ( : {}), createdAt: new Date(table.createdAt), updatedAt: new Date(table.updatedAt), - ...(table.metadata ? { metadata: JSON.parse(table.metadata) } : {}), + ...(table.metadata + ? { + metadata: + typeof table.metadata === 'string' + ? JSON.parse(table.metadata) + : table.metadata, + } + : {}), } } @@ -125,7 +132,14 @@ export const toSigningTransaction = ( ...(table.signature ? { signature: table.signature } : {}), publicKey: table.publicKey, status: table.status as SigningDriverStatus, - ...(table.metadata ? { metadata: JSON.parse(table.metadata) } : {}), + ...(table.metadata + ? { + metadata: + typeof table.metadata === 'string' + ? JSON.parse(table.metadata) + : table.metadata, + } + : {}), createdAt: new Date(table.createdAt), updatedAt: new Date(table.updatedAt), ...(table.signedAt ? { signedAt: new Date(table.signedAt) } : {}), @@ -148,7 +162,10 @@ export const toSigningDriverConfig = ( ): SigningDriverConfig => { return { driverId: table.driverId, - config: JSON.parse(table.config), + config: + typeof table.config === 'string' + ? JSON.parse(table.config) + : table.config, } } diff --git a/core/signing-store-sql/src/store-sql.ts b/core/signing-store-sql/src/store-sql.ts index 326b5cc4f..754e0276d 100644 --- a/core/signing-store-sql/src/store-sql.ts +++ b/core/signing-store-sql/src/store-sql.ts @@ -15,7 +15,14 @@ import { SigningDriverStatus, SigningDriverConfig, } from '@canton-network/core-signing-lib' -import { CamelCasePlugin, Kysely, SqliteDialect, sql } from 'kysely' +import { + CamelCasePlugin, + Kysely, + SqliteDialect, + sql, + PostgresDialect, +} from 'kysely' +import pg from 'pg' import Database from 'better-sqlite3' import { DB, @@ -343,6 +350,19 @@ export const connection = (config: StoreConfig) => { }), plugins: [new CamelCasePlugin()], }) + case 'postgres': + return new Kysely({ + dialect: new PostgresDialect({ + pool: new pg.Pool({ + database: config.connection.database, + user: config.connection.user, + password: config.connection.password, + port: config.connection.port, + host: config.connection.host, + }), + }), + plugins: [new CamelCasePlugin()], + }) case 'memory': return new Kysely({ dialect: new SqliteDialect({ @@ -350,9 +370,5 @@ export const connection = (config: StoreConfig) => { }), plugins: [new CamelCasePlugin()], }) - default: - throw new Error( - `Unsupported database type: ${config.connection.type}` - ) } } diff --git a/core/wallet-store-sql/package.json b/core/wallet-store-sql/package.json index 0ddfcb90d..bc2a53ff0 100644 --- a/core/wallet-store-sql/package.json +++ b/core/wallet-store-sql/package.json @@ -32,6 +32,7 @@ "better-sqlite3": "^12.6.2", "commander": "^14.0.2", "kysely": "^0.28.10", + "pg": "^8.16.3", "pino": "^10.2.1", "umzug": "^3.8.2", "zod": "^4.3.5" @@ -42,6 +43,7 @@ "@swc/jest": "^0.2.39", "@types/better-sqlite3": "^7.6.13", "@types/jest": "^30.0.0", + "@types/pg": "^8", "jest": "^30.2.0", "pino-test": "^1.1.0", "ts-jest": "^29.4.6", diff --git a/core/wallet-store-sql/src/schema.ts b/core/wallet-store-sql/src/schema.ts index da9b31bed..fb534503e 100644 --- a/core/wallet-store-sql/src/schema.ts +++ b/core/wallet-store-sql/src/schema.ts @@ -129,9 +129,15 @@ export const toNetwork = (table: NetworkTable): Network => { ledgerApi: { baseUrl: table.ledgerApiBaseUrl, }, - auth: authSchema.parse(JSON.parse(table.auth)), + auth: authSchema.parse( + typeof table.auth === 'string' ? JSON.parse(table.auth) : table.auth + ), adminAuth: table.adminAuth - ? authSchema.parse(JSON.parse(table.adminAuth)) + ? authSchema.parse( + typeof table.adminAuth === 'string' + ? JSON.parse(table.adminAuth) + : table.adminAuth + ) : undefined, } } @@ -176,7 +182,7 @@ export const toWallet = (table: WalletTable): Wallet => { throw new Error(`Missing wallet disabled reason: ${table.partyId}`) } return { - primary: table.primary === 1, + primary: Boolean(table.primary), status: toWalletStatus(table.status), partyId: table.partyId, hint: table.hint, diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 336513653..0bbf0d70a 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -20,7 +20,7 @@ import { StoreConfig, UpdateWallet, } from '@canton-network/core-wallet-store' -import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely' +import { CamelCasePlugin, Kysely, PostgresDialect, SqliteDialect } from 'kysely' import Database from 'better-sqlite3' import { DB, @@ -33,6 +33,7 @@ import { toTransaction, toWallet, } from './schema.js' +import pg from 'pg' export class StoreSql implements BaseStore, AuthAware { authContext: AuthContext | undefined @@ -470,6 +471,19 @@ export const connection = (config: StoreConfig) => { }), plugins: [new CamelCasePlugin()], }) + case 'postgres': + return new Kysely({ + dialect: new PostgresDialect({ + pool: new pg.Pool({ + database: config.connection.database, + user: config.connection.user, + password: config.connection.password, + port: config.connection.port, + host: config.connection.host, + }), + }), + plugins: [new CamelCasePlugin()], + }) case 'memory': return new Kysely({ dialect: new SqliteDialect({ @@ -477,9 +491,5 @@ export const connection = (config: StoreConfig) => { }), plugins: [new CamelCasePlugin()], }) - default: - throw new Error( - `Unsupported database type: ${config.connection.type}` - ) } } diff --git a/wallet-gateway/remote/README.md b/wallet-gateway/remote/README.md index cf2a1e129..3f05e6cf9 100644 --- a/wallet-gateway/remote/README.md +++ b/wallet-gateway/remote/README.md @@ -51,3 +51,40 @@ The JSON-RPC API specs from `api-specs/` are generated into strongly-typed metho 2. Place the `fireblocks_secret.key` file at the path `/splice-wallet-kernel/wallet-gateway/remote` 3. Create a file named `fireblocks_api.key` at the path `/splice-wallet-kernel/wallet-gateway/remote` and insert your Fireblocks API key into it (get it from `API User (ID)` column in fireblocks api users table). Make sure file doesn't end with new line character. + +## Postgres connection + +To create a Postgres database you need to: + +1. Start Postgres in Docker using: + +```shell +$ docker run --network=host --name some-postgres -e POSTGRES_PASSWORD=postgres -d postgres +``` + +2. In the file `splice-wallet-kernel/wallet-gateway/test/config.json`, specify the connection settings for both databases (store and signingStore). The connection should look like this (it is important that `store.connection.database !== signingStore.connection.database !== 'postgres'`): + +```json +{ + "store": { + "connection": { + "type": "postgres", + "password": "postgres", + "port": 5432, + "user": "postgres", + "host": "0.0.0.0", + "database": "wallet_store" + } + }, + "signingStore": { + "connection": { + "type": "postgres", + "password": "postgres", + "port": 5432, + "user": "postgres", + "host": "0.0.0.0", + "database": "signing_store" + } + } +} +``` diff --git a/wallet-gateway/remote/package.json b/wallet-gateway/remote/package.json index dd79caa39..bd015c5fd 100644 --- a/wallet-gateway/remote/package.json +++ b/wallet-gateway/remote/package.json @@ -20,7 +20,12 @@ "db:migrate:up": "tsx ./src/index -c ../test/config.json db up", "db:migrate:down": "tsx ./src/index -c ../test/config.json db down", "db:migrate:status": "tsx ./src/index -c ../test/config.json db status", - "db:migrate:reset": "tsx ./src/index -c ../test/config.json db reset" + "db:migrate:reset": "tsx ./src/index -c ../test/config.json db reset", + "signing-db:bootstrap": "tsx ./src/index -c ../test/config.json signing-db bootstrap", + "signing-db:migrate:up": "tsx ./src/index -c ../test/config.json signing-db up", + "signing-db:migrate:down": "tsx ./src/index -c ../test/config.json signing-db down", + "signing-db:migrate:status": "tsx ./src/index -c ../test/config.json signing-db status", + "signing-db:migrate:reset": "tsx ./src/index -c ../test/config.json signing-db reset" }, "keywords": [], "author": "", @@ -55,6 +60,7 @@ "express": "^5.2.1", "express-rate-limit": "^8.2.1", "jose": "^6.1.3", + "kysely": "^0.28.5", "lit": "^3.3.2", "pino": "^10.2.1", "pino-pretty": "^13.1.3", diff --git a/wallet-gateway/remote/src/index.ts b/wallet-gateway/remote/src/index.ts index e39670671..36c373df3 100644 --- a/wallet-gateway/remote/src/index.ts +++ b/wallet-gateway/remote/src/index.ts @@ -7,6 +7,7 @@ import { Option, Command } from '@commander-js/extra-typings' import { initialize } from './init.js' import { createCLI } from '@canton-network/core-wallet-store-sql' +import { createCLI as createSigningCLI } from '@canton-network/core-signing-store-sql' import { ConfigUtils } from './config/ConfigUtils.js' import pino from 'pino' @@ -66,14 +67,24 @@ let db = new Command('db') .description('Database management commands') .allowUnknownOption(true) -const hasDb = process.argv.slice(2).includes('db') +let signingDb = new Command('signing-db') + .description('Signing database management commands') + .allowUnknownOption(true) +const hasDb = process.argv.slice(2).includes('db') if (hasDb) { const config = ConfigUtils.loadConfigFile(options.config) db = createCLI(config.store) as Command } +const hasSigningDb = process.argv.slice(2).includes('signing-db') +if (hasSigningDb) { + const config = ConfigUtils.loadConfigFile(options.config) + signingDb = createSigningCLI(config.signingStore) as Command +} + program.addCommand(db.name('db')) +program.addCommand(signingDb.name('signing-db')) // Now parse normally for execution/help program.parseAsync(process.argv) diff --git a/wallet-gateway/remote/src/init.ts b/wallet-gateway/remote/src/init.ts index 176ee9f17..3152c4e20 100644 --- a/wallet-gateway/remote/src/init.ts +++ b/wallet-gateway/remote/src/init.ts @@ -35,6 +35,7 @@ import path from 'path' import { GATEWAY_VERSION } from './version.js' import { sessionHandler } from './middleware/sessionHandler.js' import { NotificationService } from './notification/NotificationService.js' +import { sql } from 'kysely' let isReady = false @@ -49,6 +50,30 @@ async function initializeDatabase( exists = existsSync(config.store.connection.database) } + if (config.store.connection.type === 'postgres') { + const db = connection({ + ...config.store, + connection: { ...config.store.connection, database: 'postgres' }, + }) + const result = await sql + .raw<{ + '?column?': number + }>( + `select 1 from pg_database where datname='${config.store.connection.database}';` + ) + .execute(db) + const databaseExist = result.rows.length > 0 + if (!databaseExist) { + // Ignore error because postgres does not support `create database if nor exists` clause + await sql + .raw(`create database ${config.store.connection.database};`) + .execute(db) + .catch(() => {}) + exists = false + } + await db.destroy() + } + const db = connection(config.store) const umzug = migrator(db) const pending = await umzug.pending() @@ -84,6 +109,35 @@ async function initializeSigningDatabase( exists = existsSync(config.signingStore.connection.database) } + if (config.signingStore.connection.type === 'postgres') { + const db = signingConnection({ + ...config.signingStore, + connection: { + ...config.signingStore.connection, + database: 'postgres', + }, + }) + const result = await sql + .raw<{ + '?column?': number + }>( + `select 1 from pg_database where datname='${config.signingStore.connection.database}';` + ) + .execute(db) + const databaseExist = result.rows.length > 0 + if (!databaseExist) { + // Ignore error because postgres does not support `create database if nor exists` clause + await sql + .raw( + `create database ${config.signingStore.connection.database};` + ) + .execute(db) + .catch(() => {}) + exists = false + } + await db.destroy() + } + const db = signingConnection(config.signingStore) const umzug = signingMigrator(db) const pending = await umzug.pending() diff --git a/yarn.lock b/yarn.lock index d39e7e2c0..16cf773d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1629,9 +1629,11 @@ __metadata: "@canton-network/core-wallet-store": "workspace:^" "@swc/core": "npm:^1.15.10" "@types/better-sqlite3": "npm:^7.6.13" + "@types/pg": "npm:^8" better-sqlite3: "npm:^12.6.2" commander: "npm:^14.0.2" kysely: "npm:^0.28.10" + pg: "npm:^8.16.3" pino: "npm:^10.2.1" tsup: "npm:^8.5.1" tsx: "npm:^4.21.0" @@ -1822,10 +1824,12 @@ __metadata: "@swc/jest": "npm:^0.2.39" "@types/better-sqlite3": "npm:^7.6.13" "@types/jest": "npm:^30.0.0" + "@types/pg": "npm:^8" better-sqlite3: "npm:^12.6.2" commander: "npm:^14.0.2" jest: "npm:^30.2.0" kysely: "npm:^0.28.10" + pg: "npm:^8.16.3" pino: "npm:^10.2.1" pino-test: "npm:^1.1.0" ts-jest: "npm:^29.4.6" @@ -2101,6 +2105,7 @@ __metadata: express-rate-limit: "npm:^8.2.1" jest: "npm:^30.2.0" jose: "npm:^6.1.3" + kysely: "npm:^0.28.5" lit: "npm:^3.3.2" pino: "npm:^10.2.1" pino-pretty: "npm:^13.1.3" @@ -4920,12 +4925,29 @@ __metadata: languageName: node linkType: hard +"@nx/devkit@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/devkit@npm:22.4.1" + dependencies: + "@zkochan/js-yaml": "npm:0.0.7" + ejs: "npm:^3.1.7" + enquirer: "npm:~2.3.6" + minimatch: "npm:10.1.1" + semver: "npm:^7.6.3" + tslib: "npm:^2.3.0" + yargs-parser: "npm:21.1.1" + peerDependencies: + nx: ">= 21 <= 23 || ^22.0.0-0" + checksum: 10c0/7a9a9da827c7a72d8da6995f166ac1f3f4c218ed3e3d4d841f40dfa9718af2513c9afd9b293331b70809246c0fd4dfc42b368aa6bdc694aad4eb4ad18c1fecc6 + languageName: node + linkType: hard + "@nx/eslint-plugin@npm:^22.4.0": - version: 22.4.0 - resolution: "@nx/eslint-plugin@npm:22.4.0" + version: 22.4.1 + resolution: "@nx/eslint-plugin@npm:22.4.1" dependencies: - "@nx/devkit": "npm:22.4.0" - "@nx/js": "npm:22.4.0" + "@nx/devkit": "npm:22.4.1" + "@nx/js": "npm:22.4.1" "@phenomnomnominal/tsquery": "npm:~6.1.4" "@typescript-eslint/type-utils": "npm:^8.0.0" "@typescript-eslint/utils": "npm:^8.0.0" @@ -4941,7 +4963,7 @@ __metadata: peerDependenciesMeta: eslint-config-prettier: optional: true - checksum: 10c0/408c3c0a0aad6ca291842b9b0144533fe533423a4a7a03886b421accf973776e411d1e1c656bf80eb672c412633ab4e82c1b10c033f780a0942228a4e24a970b + checksum: 10c0/5889067da2af2b0214eae2ae2a59a40e7a3688c0f7a7ee3b71b2b9c7667d94090c40f3c6c004955b7956840ea0d69cafad126a3242abec30d2c8dfb790fc5423 languageName: node linkType: hard @@ -5003,6 +5025,45 @@ __metadata: languageName: node linkType: hard +"@nx/js@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/js@npm:22.4.1" + dependencies: + "@babel/core": "npm:^7.23.2" + "@babel/plugin-proposal-decorators": "npm:^7.22.7" + "@babel/plugin-transform-class-properties": "npm:^7.22.5" + "@babel/plugin-transform-runtime": "npm:^7.23.2" + "@babel/preset-env": "npm:^7.23.2" + "@babel/preset-typescript": "npm:^7.22.5" + "@babel/runtime": "npm:^7.22.6" + "@nx/devkit": "npm:22.4.1" + "@nx/workspace": "npm:22.4.1" + "@zkochan/js-yaml": "npm:0.0.7" + babel-plugin-const-enum: "npm:^1.0.1" + babel-plugin-macros: "npm:^3.1.0" + babel-plugin-transform-typescript-metadata: "npm:^0.3.1" + chalk: "npm:^4.1.0" + columnify: "npm:^1.6.0" + detect-port: "npm:^1.5.1" + ignore: "npm:^5.0.4" + js-tokens: "npm:^4.0.0" + jsonc-parser: "npm:3.2.0" + npm-run-path: "npm:^4.0.1" + picocolors: "npm:^1.1.0" + picomatch: "npm:4.0.2" + semver: "npm:^7.6.3" + source-map-support: "npm:0.5.19" + tinyglobby: "npm:^0.2.12" + tslib: "npm:^2.3.0" + peerDependencies: + verdaccio: ^6.0.5 + peerDependenciesMeta: + verdaccio: + optional: true + checksum: 10c0/3ac2a04988010c6f209aa3e6194c59f48755c7775c78999fd67ecdd06065ff488ffcc5392d80babcdfebd966a505fa04089bc12ea04e9a354f9f13d31b976425 + languageName: node + linkType: hard + "@nx/nx-darwin-arm64@npm:22.4.0": version: 22.4.0 resolution: "@nx/nx-darwin-arm64@npm:22.4.0" @@ -5010,6 +5071,13 @@ __metadata: languageName: node linkType: hard +"@nx/nx-darwin-arm64@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/nx-darwin-arm64@npm:22.4.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@nx/nx-darwin-x64@npm:22.4.0": version: 22.4.0 resolution: "@nx/nx-darwin-x64@npm:22.4.0" @@ -5017,6 +5085,13 @@ __metadata: languageName: node linkType: hard +"@nx/nx-darwin-x64@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/nx-darwin-x64@npm:22.4.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@nx/nx-freebsd-x64@npm:22.4.0": version: 22.4.0 resolution: "@nx/nx-freebsd-x64@npm:22.4.0" @@ -5024,6 +5099,13 @@ __metadata: languageName: node linkType: hard +"@nx/nx-freebsd-x64@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/nx-freebsd-x64@npm:22.4.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@nx/nx-linux-arm-gnueabihf@npm:22.4.0": version: 22.4.0 resolution: "@nx/nx-linux-arm-gnueabihf@npm:22.4.0" @@ -5031,6 +5113,13 @@ __metadata: languageName: node linkType: hard +"@nx/nx-linux-arm-gnueabihf@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/nx-linux-arm-gnueabihf@npm:22.4.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@nx/nx-linux-arm64-gnu@npm:22.4.0": version: 22.4.0 resolution: "@nx/nx-linux-arm64-gnu@npm:22.4.0" @@ -5038,6 +5127,13 @@ __metadata: languageName: node linkType: hard +"@nx/nx-linux-arm64-gnu@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/nx-linux-arm64-gnu@npm:22.4.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@nx/nx-linux-arm64-musl@npm:22.4.0": version: 22.4.0 resolution: "@nx/nx-linux-arm64-musl@npm:22.4.0" @@ -5045,6 +5141,13 @@ __metadata: languageName: node linkType: hard +"@nx/nx-linux-arm64-musl@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/nx-linux-arm64-musl@npm:22.4.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@nx/nx-linux-x64-gnu@npm:22.4.0": version: 22.4.0 resolution: "@nx/nx-linux-x64-gnu@npm:22.4.0" @@ -5052,6 +5155,13 @@ __metadata: languageName: node linkType: hard +"@nx/nx-linux-x64-gnu@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/nx-linux-x64-gnu@npm:22.4.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@nx/nx-linux-x64-musl@npm:22.4.0": version: 22.4.0 resolution: "@nx/nx-linux-x64-musl@npm:22.4.0" @@ -5059,6 +5169,13 @@ __metadata: languageName: node linkType: hard +"@nx/nx-linux-x64-musl@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/nx-linux-x64-musl@npm:22.4.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@nx/nx-win32-arm64-msvc@npm:22.4.0": version: 22.4.0 resolution: "@nx/nx-win32-arm64-msvc@npm:22.4.0" @@ -5066,6 +5183,13 @@ __metadata: languageName: node linkType: hard +"@nx/nx-win32-arm64-msvc@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/nx-win32-arm64-msvc@npm:22.4.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@nx/nx-win32-x64-msvc@npm:22.4.0": version: 22.4.0 resolution: "@nx/nx-win32-x64-msvc@npm:22.4.0" @@ -5073,6 +5197,13 @@ __metadata: languageName: node linkType: hard +"@nx/nx-win32-x64-msvc@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/nx-win32-x64-msvc@npm:22.4.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@nx/playwright@npm:22.4.0": version: 22.4.0 resolution: "@nx/playwright@npm:22.4.0" @@ -5139,6 +5270,23 @@ __metadata: languageName: node linkType: hard +"@nx/workspace@npm:22.4.1": + version: 22.4.1 + resolution: "@nx/workspace@npm:22.4.1" + dependencies: + "@nx/devkit": "npm:22.4.1" + "@zkochan/js-yaml": "npm:0.0.7" + chalk: "npm:^4.1.0" + enquirer: "npm:~2.3.6" + nx: "npm:22.4.1" + picomatch: "npm:4.0.2" + semver: "npm:^7.6.3" + tslib: "npm:^2.3.0" + yargs-parser: "npm:21.1.1" + checksum: 10c0/dae3736c043971657191848f5b4c16caf25227693e5e498d74775eb6035d1a28b38df1b1e515389fba660a7d2ce308228935f0553d481c93e9d182fab9f43346 + languageName: node + linkType: hard + "@open-rpc/examples@npm:^1.7.2": version: 1.7.2 resolution: "@open-rpc/examples@npm:1.7.2" @@ -6194,9 +6342,9 @@ __metadata: linkType: hard "@sinclair/typebox@npm:^0.34.0": - version: 0.34.47 - resolution: "@sinclair/typebox@npm:0.34.47" - checksum: 10c0/ebe923fe2c26900982634e5639a00471da0b182eee61a5a0436cd1df174f90c5b0fcd7507cc21ad2fca3c326aee387487040badc723bc2599a09bc3e9be09b38 + version: 0.34.48 + resolution: "@sinclair/typebox@npm:0.34.48" + checksum: 10c0/e09f26d8ad471a07ee64004eea7c4ec185349a1f61c03e87e71ea33cbe98e97959940076c2e52968a955ffd4c215bf5ba7035e77079511aac7935f25e989e29d languageName: node linkType: hard @@ -6736,36 +6884,36 @@ __metadata: linkType: hard "@tanstack/react-router-devtools@npm:^1.154.7": - version: 1.154.7 - resolution: "@tanstack/react-router-devtools@npm:1.154.7" + version: 1.154.13 + resolution: "@tanstack/react-router-devtools@npm:1.154.13" dependencies: - "@tanstack/router-devtools-core": "npm:1.154.7" + "@tanstack/router-devtools-core": "npm:1.154.13" peerDependencies: - "@tanstack/react-router": ^1.154.7 - "@tanstack/router-core": ^1.154.7 + "@tanstack/react-router": ^1.154.13 + "@tanstack/router-core": ^1.154.13 react: ">=18.0.0 || >=19.0.0" react-dom: ">=18.0.0 || >=19.0.0" peerDependenciesMeta: "@tanstack/router-core": optional: true - checksum: 10c0/c663d720f6cd6d7c44b45887dbc841c60a993a23b568ec7f0d83e20712b386758f97e1634c079c3c891aad941ddfccae0b32ad572d4fec23e284a3623b96c14c + checksum: 10c0/3be459eaf5bad7ca12130c9818d02cef3f57e71ad6893626eb83d3ed16ff794b70159123b9a7035c500a7fb89aa8f2a5a4c70b6f9da29d9e0a6f859c2de918f1 languageName: node linkType: hard "@tanstack/react-router@npm:^1.154.7": - version: 1.154.7 - resolution: "@tanstack/react-router@npm:1.154.7" + version: 1.154.13 + resolution: "@tanstack/react-router@npm:1.154.13" dependencies: "@tanstack/history": "npm:1.154.7" "@tanstack/react-store": "npm:^0.8.0" - "@tanstack/router-core": "npm:1.154.7" + "@tanstack/router-core": "npm:1.154.13" isbot: "npm:^5.1.22" tiny-invariant: "npm:^1.3.3" tiny-warning: "npm:^1.0.3" peerDependencies: react: ">=18.0.0 || >=19.0.0" react-dom: ">=18.0.0 || >=19.0.0" - checksum: 10c0/16e334b29ac41d96ebc5e13dc3a3365d4ffc57fe627a528411d51d368618dd2e640c71c6b66c90bd91090a805d2913ab3313aa17d85354ec346f204a59a55438 + checksum: 10c0/1e6a2e75afbbd8c6e07ca7eb32197aa10851008a9c3c237f48413c79f86a191fe69cbd213b77de7c875e8f2392de6af3eec46094985b9250d0b9bd31de2125d9 languageName: node linkType: hard @@ -6782,9 +6930,9 @@ __metadata: languageName: node linkType: hard -"@tanstack/router-core@npm:1.154.7, @tanstack/router-core@npm:^1.154.7": - version: 1.154.7 - resolution: "@tanstack/router-core@npm:1.154.7" +"@tanstack/router-core@npm:1.154.13, @tanstack/router-core@npm:^1.154.7": + version: 1.154.13 + resolution: "@tanstack/router-core@npm:1.154.13" dependencies: "@tanstack/history": "npm:1.154.7" "@tanstack/store": "npm:^0.8.0" @@ -6793,32 +6941,32 @@ __metadata: seroval-plugins: "npm:^1.4.2" tiny-invariant: "npm:^1.3.3" tiny-warning: "npm:^1.0.3" - checksum: 10c0/c3cbc438b3aa93f1fcb431fd941d9cd5d84612c1a6daa48c13f1b891d3b78104d87af2cbbb7b31997ac2215c13b0ef07b95b014be18353070f1217e75fc62d04 + checksum: 10c0/5c0cd127e3132bf08e16262bdf3e4aa843451b6759e87a52c9b04dc15e9fcdc7afbc466cbb41f0426ecec2cf4e046649176403a92ee1bf4e91e13b4ce8dd2792 languageName: node linkType: hard -"@tanstack/router-devtools-core@npm:1.154.7": - version: 1.154.7 - resolution: "@tanstack/router-devtools-core@npm:1.154.7" +"@tanstack/router-devtools-core@npm:1.154.13": + version: 1.154.13 + resolution: "@tanstack/router-devtools-core@npm:1.154.13" dependencies: clsx: "npm:^2.1.1" goober: "npm:^2.1.16" tiny-invariant: "npm:^1.3.3" peerDependencies: - "@tanstack/router-core": ^1.154.7 + "@tanstack/router-core": ^1.154.13 csstype: ^3.0.10 peerDependenciesMeta: csstype: optional: true - checksum: 10c0/5ef26952c187155bda7a49a4e39e4867169f602b7c8730a57d7d8cc1bb45d70a5e3f9702f0ed0404c8c0ead9c09e471bec15942495f3d4abe988db3408b5e573 + checksum: 10c0/a242286e533eeca6939235642e138136dc0c996803da41750650de036eb411a5e34cd45dd5a47208b24ea17faf6292f077cfde0de5c8c34635be8051e61619e0 languageName: node linkType: hard -"@tanstack/router-generator@npm:1.154.7": - version: 1.154.7 - resolution: "@tanstack/router-generator@npm:1.154.7" +"@tanstack/router-generator@npm:1.154.13": + version: 1.154.13 + resolution: "@tanstack/router-generator@npm:1.154.13" dependencies: - "@tanstack/router-core": "npm:1.154.7" + "@tanstack/router-core": "npm:1.154.13" "@tanstack/router-utils": "npm:1.154.7" "@tanstack/virtual-file-routes": "npm:1.154.7" prettier: "npm:^3.5.0" @@ -6826,13 +6974,13 @@ __metadata: source-map: "npm:^0.7.4" tsx: "npm:^4.19.2" zod: "npm:^3.24.2" - checksum: 10c0/bc306f45e73a9c3e65ef434c1c1d7e3777262f483e8abb1bd1aece63575bd622f0e030abf5be81676cd25ae1d31fa25e7eb564fdb0a30a671182e53ad5a7f983 + checksum: 10c0/d405a5a4274699d89ebb667ab8e06ccce218e49b89dab4d4b3bd41ba34d8fcdc523abbdad8452c46fbd7500ddf194a9daee6916706646ee546e526c3819aa8f3 languageName: node linkType: hard "@tanstack/router-plugin@npm:^1.154.7": - version: 1.154.7 - resolution: "@tanstack/router-plugin@npm:1.154.7" + version: 1.154.13 + resolution: "@tanstack/router-plugin@npm:1.154.13" dependencies: "@babel/core": "npm:^7.28.5" "@babel/plugin-syntax-jsx": "npm:^7.27.1" @@ -6840,8 +6988,8 @@ __metadata: "@babel/template": "npm:^7.27.2" "@babel/traverse": "npm:^7.28.5" "@babel/types": "npm:^7.28.5" - "@tanstack/router-core": "npm:1.154.7" - "@tanstack/router-generator": "npm:1.154.7" + "@tanstack/router-core": "npm:1.154.13" + "@tanstack/router-generator": "npm:1.154.13" "@tanstack/router-utils": "npm:1.154.7" "@tanstack/virtual-file-routes": "npm:1.154.7" babel-dead-code-elimination: "npm:^1.0.11" @@ -6850,7 +6998,7 @@ __metadata: zod: "npm:^3.24.2" peerDependencies: "@rsbuild/core": ">=1.0.2" - "@tanstack/react-router": ^1.154.7 + "@tanstack/react-router": ^1.154.13 vite: ">=5.0.0 || >=6.0.0 || >=7.0.0" vite-plugin-solid: ^2.11.10 webpack: ">=5.92.0" @@ -6865,7 +7013,7 @@ __metadata: optional: true webpack: optional: true - checksum: 10c0/85d859dbbe773e73d3fa2d4ea36c6610a6bd65061a82a47d0c4b42760cd274465bef5ffd374a44699a351914c8786af6ad1613aea36a90f31a13fb1e7a217399 + checksum: 10c0/c643084fcc853c5b9bf29cdf3abe0f11a99a96470c401e6a67ffb3a376bf9753d037ad27ee037d29cb90110e904343a0b0eae6ab5076c1b57cb2899e12dfbe14 languageName: node linkType: hard @@ -7327,6 +7475,17 @@ __metadata: languageName: node linkType: hard +"@types/pg@npm:^8": + version: 8.16.0 + resolution: "@types/pg@npm:8.16.0" + dependencies: + "@types/node": "npm:*" + pg-protocol: "npm:*" + pg-types: "npm:^2.2.0" + checksum: 10c0/421fe7c07d5c0226835d362414a63653f86251ee966150d807ed60174c13921d1b8a3e2f1c2bfba9659ec0282ca50974030c4c1efcd575003eb922ea12ca7d05 + languageName: node + linkType: hard + "@types/prop-types@npm:^15.7.15": version: 15.7.15 resolution: "@types/prop-types@npm:15.7.15" @@ -9065,9 +9224,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001759": - version: 1.0.30001765 - resolution: "caniuse-lite@npm:1.0.30001765" - checksum: 10c0/2bab28b322ec040dde2b6f56019ffd1e0bbd719111e45f58cb0fb06a783812d8ba8df65755320fd253aa1926dffc7bf0864adc11f6b231ac2b3a5b8221199c29 + version: 1.0.30001766 + resolution: "caniuse-lite@npm:1.0.30001766" + checksum: 10c0/cecc8f9a3758c486fc68434a3cca5f4ca7077db5ac9cdb1689786abf63c4aa9891bf70f2df2c3e549d5e284e8da36a218d0e131ebb26dd59280bc99db49640f6 languageName: node linkType: hard @@ -9717,12 +9876,12 @@ __metadata: linkType: hard "cors@npm:^2.8.5, cors@npm:~2.8.5": - version: 2.8.5 - resolution: "cors@npm:2.8.5" + version: 2.8.6 + resolution: "cors@npm:2.8.6" dependencies: object-assign: "npm:^4" vary: "npm:^1" - checksum: 10c0/373702b7999409922da80de4a61938aabba6929aea5b6fd9096fefb9e8342f626c0ebd7507b0e8b0b311380744cc985f27edebc0a26e0ddb784b54e1085de761 + checksum: 10c0/ab2bc57b8af8ef8476682a59647f7c55c1a7d406b559ac06119aa1c5f70b96d35036864d197b24cf86e228e4547231088f1f94ca05061dbb14d89cc0bc9d4cab languageName: node linkType: hard @@ -11841,9 +12000,9 @@ __metadata: linkType: hard "globals@npm:^17.0.0": - version: 17.0.0 - resolution: "globals@npm:17.0.0" - checksum: 10c0/e3c169fdcb0fc6755707b697afb367bea483eb29992cfc0de1637382eb893146e17f8f96db6d7453e3696b478a7863ae2000e6c71cd2f4061410285106d3847a + version: 17.1.0 + resolution: "globals@npm:17.1.0" + checksum: 10c0/4a4a17847676a09f164b8bdce7df105a4f484d6d44586e374087ba9ec7089cbf4c578b8648838dee9917074199c542ce157ea3c07b266f708dfb1652010900c8 languageName: node linkType: hard @@ -13562,7 +13721,7 @@ __metadata: languageName: node linkType: hard -"kysely@npm:^0.28.10": +"kysely@npm:^0.28.10, kysely@npm:^0.28.5": version: 0.28.10 resolution: "kysely@npm:0.28.10" checksum: 10c0/46a42f7092035ccc2adbe5f447ff5377431e9aabd1c55ca2c9f79a11595b97cd446a0ffdcc3e14a993a23fbd7e2a7ab7e874f2b7388f42ddd7d76c6aee9a1241 @@ -14710,7 +14869,7 @@ __metadata: languageName: node linkType: hard -"nx@npm:22.4.0, nx@npm:^22.4.0": +"nx@npm:22.4.0": version: 22.4.0 resolution: "nx@npm:22.4.0" dependencies: @@ -14795,6 +14954,91 @@ __metadata: languageName: node linkType: hard +"nx@npm:22.4.1, nx@npm:^22.4.0": + version: 22.4.1 + resolution: "nx@npm:22.4.1" + dependencies: + "@napi-rs/wasm-runtime": "npm:0.2.4" + "@nx/nx-darwin-arm64": "npm:22.4.1" + "@nx/nx-darwin-x64": "npm:22.4.1" + "@nx/nx-freebsd-x64": "npm:22.4.1" + "@nx/nx-linux-arm-gnueabihf": "npm:22.4.1" + "@nx/nx-linux-arm64-gnu": "npm:22.4.1" + "@nx/nx-linux-arm64-musl": "npm:22.4.1" + "@nx/nx-linux-x64-gnu": "npm:22.4.1" + "@nx/nx-linux-x64-musl": "npm:22.4.1" + "@nx/nx-win32-arm64-msvc": "npm:22.4.1" + "@nx/nx-win32-x64-msvc": "npm:22.4.1" + "@yarnpkg/lockfile": "npm:^1.1.0" + "@yarnpkg/parsers": "npm:3.0.2" + "@zkochan/js-yaml": "npm:0.0.7" + axios: "npm:^1.12.0" + chalk: "npm:^4.1.0" + cli-cursor: "npm:3.1.0" + cli-spinners: "npm:2.6.1" + cliui: "npm:^8.0.1" + dotenv: "npm:~16.4.5" + dotenv-expand: "npm:~11.0.6" + enquirer: "npm:~2.3.6" + figures: "npm:3.2.0" + flat: "npm:^5.0.2" + front-matter: "npm:^4.0.2" + ignore: "npm:^7.0.5" + jest-diff: "npm:^30.0.2" + jsonc-parser: "npm:3.2.0" + lines-and-columns: "npm:2.0.3" + minimatch: "npm:10.1.1" + node-machine-id: "npm:1.1.12" + npm-run-path: "npm:^4.0.1" + open: "npm:^8.4.0" + ora: "npm:5.3.0" + resolve.exports: "npm:2.0.3" + semver: "npm:^7.6.3" + string-width: "npm:^4.2.3" + tar-stream: "npm:~2.2.0" + tmp: "npm:~0.2.1" + tree-kill: "npm:^1.2.2" + tsconfig-paths: "npm:^4.1.2" + tslib: "npm:^2.3.0" + yaml: "npm:^2.6.0" + yargs: "npm:^17.6.2" + yargs-parser: "npm:21.1.1" + peerDependencies: + "@swc-node/register": ^1.8.0 + "@swc/core": ^1.3.85 + dependenciesMeta: + "@nx/nx-darwin-arm64": + optional: true + "@nx/nx-darwin-x64": + optional: true + "@nx/nx-freebsd-x64": + optional: true + "@nx/nx-linux-arm-gnueabihf": + optional: true + "@nx/nx-linux-arm64-gnu": + optional: true + "@nx/nx-linux-arm64-musl": + optional: true + "@nx/nx-linux-x64-gnu": + optional: true + "@nx/nx-linux-x64-musl": + optional: true + "@nx/nx-win32-arm64-msvc": + optional: true + "@nx/nx-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc-node/register": + optional: true + "@swc/core": + optional: true + bin: + nx: bin/nx.js + nx-cloud: bin/nx-cloud.js + checksum: 10c0/2b2b6ab3c240731c2a542a706dceb54ad1900e84e966b41bf7113f1da516eccdc40c4cf674a53ff98e78565bb46f4bdd9f6967e9d0d8f1419e69002398689d75 + languageName: node + linkType: hard + "oauth2-mock-server@npm:^8.2.0": version: 8.2.0 resolution: "oauth2-mock-server@npm:8.2.0" @@ -15345,6 +15589,87 @@ __metadata: languageName: node linkType: hard +"pg-cloudflare@npm:^1.3.0": + version: 1.3.0 + resolution: "pg-cloudflare@npm:1.3.0" + checksum: 10c0/b0866c88af8e54c7b3ed510719d92df37714b3af5e3a3a10d9f761fcec99483e222f5b78a1f2de590368127648087c45c01aaf66fadbe46edb25673eedc4f8fc + languageName: node + linkType: hard + +"pg-connection-string@npm:^2.10.1": + version: 2.10.1 + resolution: "pg-connection-string@npm:2.10.1" + checksum: 10c0/f218a72b59c661022caca9a7f2116655632b1d7e7d6dc9a8ee9f238744e0927e0d6f44e12f50d9767c6d9cd47d9b3766aa054b77504b15c6bf503400530e053e + languageName: node + linkType: hard + +"pg-int8@npm:1.0.1": + version: 1.0.1 + resolution: "pg-int8@npm:1.0.1" + checksum: 10c0/be6a02d851fc2a4ae3e9de81710d861de3ba35ac927268973eb3cb618873a05b9424656df464dd43bd7dc3fc5295c3f5b3c8349494f87c7af50ec59ef14e0b98 + languageName: node + linkType: hard + +"pg-pool@npm:^3.11.0": + version: 3.11.0 + resolution: "pg-pool@npm:3.11.0" + peerDependencies: + pg: ">=8.0" + checksum: 10c0/4b104b48a47257a0edad0c62e5ea1908b72cb79386270264b452e69895e9e4c589d00cdbf6e46d4e9c05bc7e7d191656b66814b5282d65f33b12648a21df3c7f + languageName: node + linkType: hard + +"pg-protocol@npm:*, pg-protocol@npm:^1.11.0": + version: 1.11.0 + resolution: "pg-protocol@npm:1.11.0" + checksum: 10c0/93e83581781418c9173eba4e4545f73392cfe66b78dd1d3624d7339fbd37e7f4abebaf2615e68e0701a9bf0edf5b81a4ad533836f388f775fe25fa24a691c464 + languageName: node + linkType: hard + +"pg-types@npm:2.2.0, pg-types@npm:^2.2.0": + version: 2.2.0 + resolution: "pg-types@npm:2.2.0" + dependencies: + pg-int8: "npm:1.0.1" + postgres-array: "npm:~2.0.0" + postgres-bytea: "npm:~1.0.0" + postgres-date: "npm:~1.0.4" + postgres-interval: "npm:^1.1.0" + checksum: 10c0/ab3f8069a323f601cd2d2279ca8c425447dab3f9b61d933b0601d7ffc00d6200df25e26a4290b2b0783b59278198f7dd2ed03e94c4875797919605116a577c65 + languageName: node + linkType: hard + +"pg@npm:^8.16.3": + version: 8.17.2 + resolution: "pg@npm:8.17.2" + dependencies: + pg-cloudflare: "npm:^1.3.0" + pg-connection-string: "npm:^2.10.1" + pg-pool: "npm:^3.11.0" + pg-protocol: "npm:^1.11.0" + pg-types: "npm:2.2.0" + pgpass: "npm:1.0.5" + peerDependencies: + pg-native: ">=3.0.1" + dependenciesMeta: + pg-cloudflare: + optional: true + peerDependenciesMeta: + pg-native: + optional: true + checksum: 10c0/74b022587f92953f498dba747ccf9c7c90767af70326595d30c7ab0e2f00b2b468226c8abae54caef63ab444a8ac6f1597d859174386c7ba7c318c225d711c5f + languageName: node + linkType: hard + +"pgpass@npm:1.0.5": + version: 1.0.5 + resolution: "pgpass@npm:1.0.5" + dependencies: + split2: "npm:^4.1.0" + checksum: 10c0/5ea6c9b2de04c33abb08d33a2dded303c4a3c7162a9264519cbe85c0a9857d712463140ba42fad0c7cd4b21f644dd870b45bb2e02fcbe505b4de0744fd802c1d + languageName: node + linkType: hard + "picocolors@npm:^1.0.1, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -15478,7 +15803,7 @@ __metadata: languageName: node linkType: hard -"pino@npm:10.2.1, pino@npm:^10.2.1": +"pino@npm:10.2.1": version: 10.2.1 resolution: "pino@npm:10.2.1" dependencies: @@ -15499,6 +15824,27 @@ __metadata: languageName: node linkType: hard +"pino@npm:^10.2.1": + version: 10.3.0 + resolution: "pino@npm:10.3.0" + dependencies: + "@pinojs/redact": "npm:^0.4.0" + atomic-sleep: "npm:^1.0.0" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^3.0.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^5.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^4.0.0" + bin: + pino: bin.js + checksum: 10c0/cfbbc7dfaa2df2aa2dce728d751aa4b5b7ab973b2cd4bfff57868567563ef0c1c021f22932769141d535c72662390e09a0190e44f4413496dbe5e3c672816308 + languageName: node + linkType: hard + "pirates@npm:^4.0.1, pirates@npm:^4.0.7": version: 4.0.7 resolution: "pirates@npm:4.0.7" @@ -15725,6 +16071,36 @@ __metadata: languageName: node linkType: hard +"postgres-array@npm:~2.0.0": + version: 2.0.0 + resolution: "postgres-array@npm:2.0.0" + checksum: 10c0/cbd56207e4141d7fbf08c86f2aebf21fa7064943d3f808ec85f442ff94b48d891e7a144cc02665fb2de5dbcb9b8e3183a2ac749959e794b4a4cfd379d7a21d08 + languageName: node + linkType: hard + +"postgres-bytea@npm:~1.0.0": + version: 1.0.1 + resolution: "postgres-bytea@npm:1.0.1" + checksum: 10c0/10b28a27c9d703d5befd97c443e62b551096d1014bc59ab574c65bf0688de7f3f068003b2aea8dcff83cf0f6f9a35f9f74457c38856cf8eb81b00cf3fb44f164 + languageName: node + linkType: hard + +"postgres-date@npm:~1.0.4": + version: 1.0.7 + resolution: "postgres-date@npm:1.0.7" + checksum: 10c0/0ff91fccc64003e10b767fcfeefb5eaffbc522c93aa65d5051c49b3c4ce6cb93ab091a7d22877a90ad60b8874202c6f1d0f935f38a7235ed3b258efd54b97ca9 + languageName: node + linkType: hard + +"postgres-interval@npm:^1.1.0": + version: 1.2.0 + resolution: "postgres-interval@npm:1.2.0" + dependencies: + xtend: "npm:^4.0.0" + checksum: 10c0/c1734c3cb79e7f22579af0b268a463b1fa1d084e742a02a7a290c4f041e349456f3bee3b4ee0bb3f226828597f7b76deb615c1b857db9a742c45520100456272 + languageName: node + linkType: hard + "powershell-utils@npm:^0.1.0": version: 0.1.0 resolution: "powershell-utils@npm:0.1.0" @@ -16678,11 +17054,11 @@ __metadata: linkType: hard "seroval-plugins@npm:^1.4.2": - version: 1.4.2 - resolution: "seroval-plugins@npm:1.4.2" + version: 1.5.0 + resolution: "seroval-plugins@npm:1.5.0" peerDependencies: seroval: ^1.0 - checksum: 10c0/081a660c6be5aac2f28e736a674c1f5b3475888cf4ba634900e153f9ef0fd93771a519b24453cd85f3877cb274d497ca9d32ddb6de2c1770c5876df6e82e6b5f + checksum: 10c0/a70636d35e0644e37efad37963e6d41ae9e4a02fbf1b57c89dbe4d62122908039e8a0fda1720b8a56aea93741735b2028ada6d3d50c1d40bbb67661f0de92042 languageName: node linkType: hard @@ -16696,9 +17072,9 @@ __metadata: linkType: hard "seroval@npm:^1.4.2": - version: 1.4.2 - resolution: "seroval@npm:1.4.2" - checksum: 10c0/aac544dcdaffebe562ed0793bab684a456503a4a74039df9d8297b0c0e28663924f0401a47282a91ad2f4e5b83db2e07a42da1834b8b537c008e3529c0db38ba + version: 1.5.0 + resolution: "seroval@npm:1.5.0" + checksum: 10c0/aff16b14a7145388555cefd4ebd41759024ee1c2c064080fd8d4fabea4b7c89d103155cd98f5109523b8878e577da73cc6cd8abf98965f2d1f0ba19dc38317ab languageName: node linkType: hard @@ -17109,7 +17485,7 @@ __metadata: languageName: unknown linkType: soft -"split2@npm:^4.0.0, split2@npm:^4.2.0": +"split2@npm:^4.0.0, split2@npm:^4.1.0, split2@npm:^4.2.0": version: 4.2.0 resolution: "split2@npm:4.2.0" checksum: 10c0/b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534 @@ -18940,6 +19316,13 @@ __metadata: languageName: node linkType: hard +"xtend@npm:^4.0.0": + version: 4.0.2 + resolution: "xtend@npm:4.0.2" + checksum: 10c0/366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e + languageName: node + linkType: hard + "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" @@ -19085,8 +19468,8 @@ __metadata: linkType: hard "zod@npm:^3.25.0 || ^4.0.0, zod@npm:^4.3.5": - version: 4.3.5 - resolution: "zod@npm:4.3.5" - checksum: 10c0/5a2db7e59177a3d7e202543f5136cb87b97b047b77c8a3d824098d3fa8b80d3aa40a0a5f296965c3b82dfdccdd05dbbfacce91347f16a39c675680fd7b1ab109 + version: 4.3.6 + resolution: "zod@npm:4.3.6" + checksum: 10c0/860d25a81ab41d33aa25f8d0d07b091a04acb426e605f396227a796e9e800c44723ed96d0f53a512b57be3d1520f45bf69c0cb3b378a232a00787a2609625307 languageName: node linkType: hard From e833cd1c8928c7d7b87d03f5608399590e548e89 Mon Sep 17 00:00:00 2001 From: Marc Juchli <120378272+mjuchli-da@users.noreply.github.com> Date: Fri, 23 Jan 2026 20:57:08 +0100 Subject: [PATCH 02/16] chore: dapp api renamings (#1115) Signed-off-by: Marc Juchli --- api-specs/openrpc-dapp-api.json | 164 ++++++++++-------- api-specs/openrpc-dapp-remote-api.json | 71 ++++---- .../splice-provider/src/SpliceProviderHttp.ts | 6 +- .../package.json | 8 +- .../src/index.ts | 132 +++++++------- .../src/openrpc.json | 71 ++++---- core/wallet-dapp-rpc-client/package.json | 8 +- core/wallet-dapp-rpc-client/src/index.ts | 138 +++++++-------- core/wallet-dapp-rpc-client/src/openrpc.json | 164 ++++++++++-------- docs/wallet-gateway/src/dapp-sdk/usage.rst | 8 +- .../signing-transactions-from-dapps/index.rst | 4 +- examples/ping/src/hooks/useAccounts.ts | 6 +- .../src/contexts/ConnectionProvider.tsx | 4 +- sdk/dapp-sdk/src/dapp-api/rpc-gen/index.ts | 33 ++-- sdk/dapp-sdk/src/dapp-api/rpc-gen/typings.ts | 87 ++++------ sdk/dapp-sdk/src/provider.ts | 51 +++--- sdk/dapp-sdk/src/provider/request.ts | 12 +- .../extension/src/dapp-api/controller.ts | 25 ++- .../extension/src/dapp-api/rpc-gen/index.ts | 33 ++-- .../extension/src/dapp-api/rpc-gen/typings.ts | 87 ++++------ .../remote/src/dapp-api/controller.ts | 55 +++--- .../remote/src/dapp-api/rpc-gen/index.ts | 39 +++-- .../remote/src/dapp-api/rpc-gen/typings.ts | 81 ++++----- wallet-gateway/remote/src/dapp-api/server.ts | 6 +- .../remote/src/user-api/controller.ts | 2 +- yarn.lock | 22 +-- 26 files changed, 636 insertions(+), 681 deletions(-) diff --git a/api-specs/openrpc-dapp-api.json b/api-specs/openrpc-dapp-api.json index 6073f6a32..8a59bb8b8 100644 --- a/api-specs/openrpc-dapp-api.json +++ b/api-specs/openrpc-dapp-api.json @@ -40,52 +40,15 @@ "description": "Invoke a disconnect of the wallet gateway session." }, { - "name": "darsAvailable", + "name": "getActiveNetwork", "params": [], "result": { "name": "result", "schema": { - "title": "darsAvailableResult", - "type": "object", - "properties": { - "dars": { - "title": "dars", - "type": "array", - "items": { - "title": "dar", - "type": "string" - } - } - }, - "required": ["dars"] + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/Network" } }, - "description": "Lists DARs currently available to the connected Validator node, such that dApp can verify whether the app is installed." - }, - { - "name": "prepareReturn", - "params": [ - { - "name": "params", - "schema": { - "title": "prepareReturnParams", - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/JsPrepareSubmissionRequest" - } - } - ], - "result": { - "name": "result", - "schema": { - "title": "prepareReturnResult", - "properties": { - "response": { - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/JsPrepareSubmissionResponse" - } - }, - "required": ["response"] - } - }, - "description": "Processes the prepare step and returns the data to sign." + "description": "Returns the active network." }, { "name": "prepareExecute", @@ -132,6 +95,25 @@ }, "description": "Like prepareExecute, but waits for the transaction to be executed on the ledger." }, + { + "name": "signMessage", + "params": [ + { + "name": "params", + "schema": { + "title": "signMessageParams", + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/SignMessageRequest" + } + } + ], + "result": { + "name": "result", + "schema": { + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/SignMessageResult" + } + }, + "description": "Signs a message." + }, { "name": "ledgerApi", "params": [ @@ -152,7 +134,7 @@ "description": "Proxy for the JSON-API endpoints. Injects authorization headers automatically." }, { - "name": "onAccountsChanged", + "name": "accountsChanged", "params": [], "result": { "name": "result", @@ -162,18 +144,29 @@ } }, { - "name": "requestAccounts", + "name": "getPrimaryAccount", + "params": [], + "result": { + "name": "result", + "schema": { + "$ref": "api-specs/openrpc-user-api.json#/components/schemas/Wallet" + } + }, + "description": "Returns the primary account." + }, + { + "name": "listAccounts", "params": [], "result": { "name": "result", "schema": { - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/RequestAccountsResult" + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/ListAccountsResult" } }, "description": "Lists the addresses (wallets) with their properties; including which network they are associated to and with signing provider is used." }, { - "name": "onTxChanged", + "name": "txChanged", "params": [], "result": { "name": "result", @@ -368,8 +361,8 @@ "$ref": "api-specs/openrpc-user-api.json#/components/schemas/Wallet" } }, - "RequestAccountsResult": { - "title": "RequestAccountsResult", + "ListAccountsResult": { + "title": "ListAccountsResult", "type": "array", "description": "An array of accounts that the user has authorized the dapp to access..", "items": { @@ -542,31 +535,7 @@ "description": "If not connected to a network, the reason why." }, "network": { - "title": "network", - "type": "object", - "description": "Network information, if connected to a network.", - "properties": { - "networkId": { - "title": "networkId", - "type": "string", - "description": "A CAIP-2 compliant chain ID, e.g. 'canton:da-mainnet'." - }, - "ledgerApi": { - "title": "LedgerApiConfig", - "type": "object", - "description": "Ledger API configuration.", - "properties": { - "baseUrl": { - "title": "baseUrl", - "type": "string", - "format": "uri", - "description": "The base URL of the ledger API." - } - }, - "required": ["baseUrl"] - } - }, - "required": ["networkId"] + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/Network" }, "session": { "title": "session", @@ -588,6 +557,59 @@ } }, "required": ["kernel", "isConnected", "isNetworkConnected"] + }, + "SignMessageRequest": { + "title": "SignMessageRequest", + "type": "object", + "description": "Request to sign a message.", + "properties": { + "message": { + "title": "message", + "type": "string", + "description": "The message to sign." + } + }, + "required": ["message"] + }, + "SignMessageResult": { + "title": "SignMessageResult", + "type": "object", + "description": "Result of signing a message.", + "properties": { + "signature": { + "title": "signature", + "type": "string", + "description": "The signature of the message." + } + }, + "required": ["signature"] + }, + "Network": { + "title": "network", + "type": "object", + "description": "Network information, if connected to a network.", + "properties": { + "networkId": { + "title": "networkId", + "type": "string", + "description": "A CAIP-2 compliant chain ID, e.g. 'canton:da-mainnet'." + }, + "ledgerApi": { + "title": "LedgerApiConfig", + "type": "object", + "description": "Ledger API configuration.", + "properties": { + "baseUrl": { + "title": "baseUrl", + "type": "string", + "format": "uri", + "description": "The base URL of the ledger API." + } + }, + "required": ["baseUrl"] + } + }, + "required": ["networkId"] } } } diff --git a/api-specs/openrpc-dapp-remote-api.json b/api-specs/openrpc-dapp-remote-api.json index c7954431e..1b88d8929 100644 --- a/api-specs/openrpc-dapp-remote-api.json +++ b/api-specs/openrpc-dapp-remote-api.json @@ -55,35 +55,23 @@ "description": "Invoke a disconnect of the wallet gateway session." }, { - "name": "darsAvailable", + "name": "getActiveNetwork", "params": [], "result": { "name": "result", "schema": { - "title": "darsAvailableResult", - "type": "object", - "properties": { - "dars": { - "title": "dars", - "type": "array", - "items": { - "title": "dar", - "type": "string" - } - } - }, - "required": ["dars"] + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/Network" } }, - "description": "Lists DARs currently available on the connected Validator node." + "description": "Returns the active network." }, { - "name": "prepareReturn", + "name": "prepareExecute", "params": [ { "name": "params", "schema": { - "title": "prepareReturnParams", + "title": "prepareExecuteParams", "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/JsPrepareSubmissionRequest" } } @@ -91,42 +79,36 @@ "result": { "name": "result", "schema": { - "title": "prepareReturnResult", + "title": "prepareExecuteResult", + "type": "object", "properties": { - "response": { - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/JsPrepareSubmissionResponse" + "userUrl": { + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/UserUrl" } }, - "required": ["response"] + "required": ["userUrl"] } }, - "description": "Processes the prepare step and returns the data to sign." + "description": "Prepares, signs, and executes a transaction." }, { - "name": "prepareExecute", + "name": "signMessage", "params": [ { "name": "params", "schema": { - "title": "prepareExecuteParams", - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/JsPrepareSubmissionRequest" + "title": "signMessageParams", + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/SignMessageRequest" } } ], "result": { "name": "result", "schema": { - "title": "prepareExecuteResult", - "type": "object", - "properties": { - "userUrl": { - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/UserUrl" - } - }, - "required": ["userUrl"] + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/SignMessageResult" } }, - "description": "Prepares, signs, and executes a transaction." + "description": "Signs a message." }, { "name": "ledgerApi", @@ -148,7 +130,7 @@ "description": "Proxy for the JSON-API endpoints. Injects authorization headers automatically." }, { - "name": "onConnected", + "name": "connected", "params": [], "result": { "name": "result", @@ -169,7 +151,7 @@ } }, { - "name": "onAccountsChanged", + "name": "accountsChanged", "params": [], "result": { "name": "result", @@ -179,17 +161,28 @@ } }, { - "name": "requestAccounts", + "name": "getPrimaryAccount", + "params": [], + "result": { + "name": "result", + "schema": { + "$ref": "api-specs/openrpc-user-api.json#/components/schemas/Wallet" + } + }, + "description": "Returns the primary account." + }, + { + "name": "listAccounts", "params": [], "result": { "name": "result", "schema": { - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/RequestAccountsResult" + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/ListAccountsResult" } } }, { - "name": "onTxChanged", + "name": "txChanged", "params": [], "result": { "name": "result", diff --git a/core/splice-provider/src/SpliceProviderHttp.ts b/core/splice-provider/src/SpliceProviderHttp.ts index cf9216662..bf25b9c83 100644 --- a/core/splice-provider/src/SpliceProviderHttp.ts +++ b/core/splice-provider/src/SpliceProviderHttp.ts @@ -85,11 +85,11 @@ export class SpliceProviderHttp extends SpliceProviderBase { this.openSocket(this.url, event.data.token) // We requery the status explicitly here, as it's not guaranteed that the socket will be open & authenticated - // before the `onConnected` event is fired from the `addSession` RPC call. The dappApi.StatusResult and - // dappApi.OnConnectedEvent are mapped manually to avoid dependency. + // before the `connected` event is fired from the `addSession` RPC call. The dappApi.StatusResult and + // dappApi.ConnectedEvent are mapped manually to avoid dependency. this.request({ method: 'status' }) .then((status) => { - this.emit('onConnected', status) + this.emit('connected', status) }) .catch((err) => { console.error( diff --git a/core/wallet-dapp-remote-rpc-client/package.json b/core/wallet-dapp-remote-rpc-client/package.json index a87a29b47..0d25afbd6 100644 --- a/core/wallet-dapp-remote-rpc-client/package.json +++ b/core/wallet-dapp-remote-rpc-client/package.json @@ -31,18 +31,18 @@ "dependencies": { "@canton-network/core-rpc-transport": "workspace:^", "@canton-network/core-types": "workspace:^", - "lodash": "^4.17.23" + "lodash": "^4.17.21" }, "devDependencies": { "@types/isomorphic-fetch": "^0.0.39", "@types/jest": "^30.0.0", "@types/json-schema": "7.0.15", - "@types/lodash": "^4.17.23", + "@types/lodash": "^4.17.21", "@types/ws": "^8.18.1", "globals": "^16.5.0", - "prettier": "^3.8.1", + "prettier": "^3.7.1", "tsup": "^8.5.1", - "typedoc": "^0.28.16", + "typedoc": "^0.28.14", "typescript": "^5.9.3" }, "repository": { diff --git a/core/wallet-dapp-remote-rpc-client/src/index.ts b/core/wallet-dapp-remote-rpc-client/src/index.ts index 1653b0ea2..9c71f8e1f 100644 --- a/core/wallet-dapp-remote-rpc-client/src/index.ts +++ b/core/wallet-dapp-remote-rpc-client/src/index.ts @@ -85,6 +85,12 @@ export type PackageId = string * */ export type PackageIdSelectionPreference = PackageId[] +/** + * + * The message to sign. + * + */ +export type Message = string export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' export type Resource = string export type Body = string @@ -208,30 +214,12 @@ export interface ConnectResult { userUrl: UserUrl [k: string]: any } -export type Dar = string -export type Dars = Dar[] -/** - * - * The prepared transaction data. - * - */ -export type PreparedTransaction = string /** * - * The hash of the prepared transaction. - * - */ -export type PreparedTransactionHash = string -/** - * - * Structure representing the result of a prepareReturn call + * The signature of the transaction. * */ -export interface JsPrepareSubmissionResponse { - preparedTransaction?: PreparedTransaction - preparedTransactionHash?: PreparedTransactionHash - [k: string]: any -} +export type Signature = string export type Response = string /** * @@ -340,12 +328,6 @@ export interface TxChangedPendingEvent { * */ export type StatusSigned = 'signed' -/** - * - * The signature of the transaction. - * - */ -export type Signature = string /** * * The identifier of the provider that signed the transaction. @@ -424,7 +406,7 @@ export interface TxChangedFailedEvent { * Structure representing the request for prepare and execute calls * */ -export interface PrepareReturnParams { +export interface PrepareExecuteParams { commandId?: CommandId commands: JsCommands actAs?: ActAs @@ -436,17 +418,11 @@ export interface PrepareReturnParams { } /** * - * Structure representing the request for prepare and execute calls + * Request to sign a message. * */ -export interface PrepareExecuteParams { - commandId?: CommandId - commands: JsCommands - actAs?: ActAs - readAs?: ReadAs - disclosedContracts?: DisclosedContracts - synchronizerId?: SynchronizerId - packageIdSelectionPreference?: PackageIdSelectionPreference +export interface SignMessageParams { + message: Message [k: string]: any } /** @@ -467,15 +443,19 @@ export type StatusEventAsync = StatusEvent & ConnectResult * */ export type Null = null -export interface DarsAvailableResult { - dars: Dars - [k: string]: any -} -export type PrepareReturnResult = any export interface PrepareExecuteResult { userUrl: UserUrl [k: string]: any } +/** + * + * Result of signing a message. + * + */ +export interface SignMessageResult { + signature: Signature + [k: string]: any +} /** * * Ledger Api configuration options @@ -496,7 +476,7 @@ export type AccountsChangedEvent = Wallet[] * An array of accounts that the user has authorized the dapp to access.. * */ -export type RequestAccountsResult = Wallet[] +export type ListAccountsResult = Wallet[] /** * * Event emitted when a transaction changes. @@ -516,19 +496,20 @@ export type TxChangedEvent = export type Status = () => Promise export type Connect = () => Promise export type Disconnect = () => Promise -export type DarsAvailable = () => Promise -export type PrepareReturn = ( - params: PrepareReturnParams -) => Promise +export type GetActiveNetwork = () => Promise export type PrepareExecute = ( params: PrepareExecuteParams ) => Promise +export type SignMessage = ( + params: SignMessageParams +) => Promise export type LedgerApi = (params: LedgerApiParams) => Promise -export type OnConnected = () => Promise +export type Connected = () => Promise export type OnStatusChanged = () => Promise -export type OnAccountsChanged = () => Promise -export type RequestAccounts = () => Promise -export type OnTxChanged = () => Promise +export type AccountsChanged = () => Promise +export type GetPrimaryAccount = () => Promise +export type ListAccounts = () => Promise +export type TxChanged = () => Promise export class SpliceWalletJSONRPCRemoteDAppAPI { public transport: RpcTransport @@ -569,27 +550,27 @@ export class SpliceWalletJSONRPCRemoteDAppAPI { */ // tslint:disable-next-line:max-line-length public async request( - method: 'darsAvailable', - ...params: Parameters - ): ReturnType + method: 'getActiveNetwork', + ...params: Parameters + ): ReturnType /** * */ // tslint:disable-next-line:max-line-length public async request( - method: 'prepareReturn', - ...params: Parameters - ): ReturnType + method: 'prepareExecute', + ...params: Parameters + ): ReturnType /** * */ // tslint:disable-next-line:max-line-length public async request( - method: 'prepareExecute', - ...params: Parameters - ): ReturnType + method: 'signMessage', + ...params: Parameters + ): ReturnType /** * @@ -605,9 +586,9 @@ export class SpliceWalletJSONRPCRemoteDAppAPI { */ // tslint:disable-next-line:max-line-length public async request( - method: 'onConnected', - ...params: Parameters - ): ReturnType + method: 'connected', + ...params: Parameters + ): ReturnType /** * @@ -623,27 +604,36 @@ export class SpliceWalletJSONRPCRemoteDAppAPI { */ // tslint:disable-next-line:max-line-length public async request( - method: 'onAccountsChanged', - ...params: Parameters - ): ReturnType + method: 'accountsChanged', + ...params: Parameters + ): ReturnType + + /** + * + */ + // tslint:disable-next-line:max-line-length + public async request( + method: 'getPrimaryAccount', + ...params: Parameters + ): ReturnType /** * */ // tslint:disable-next-line:max-line-length public async request( - method: 'requestAccounts', - ...params: Parameters - ): ReturnType + method: 'listAccounts', + ...params: Parameters + ): ReturnType /** * */ // tslint:disable-next-line:max-line-length public async request( - method: 'onTxChanged', - ...params: Parameters - ): ReturnType + method: 'txChanged', + ...params: Parameters + ): ReturnType public async request( method: string, diff --git a/core/wallet-dapp-remote-rpc-client/src/openrpc.json b/core/wallet-dapp-remote-rpc-client/src/openrpc.json index c7954431e..1b88d8929 100644 --- a/core/wallet-dapp-remote-rpc-client/src/openrpc.json +++ b/core/wallet-dapp-remote-rpc-client/src/openrpc.json @@ -55,35 +55,23 @@ "description": "Invoke a disconnect of the wallet gateway session." }, { - "name": "darsAvailable", + "name": "getActiveNetwork", "params": [], "result": { "name": "result", "schema": { - "title": "darsAvailableResult", - "type": "object", - "properties": { - "dars": { - "title": "dars", - "type": "array", - "items": { - "title": "dar", - "type": "string" - } - } - }, - "required": ["dars"] + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/Network" } }, - "description": "Lists DARs currently available on the connected Validator node." + "description": "Returns the active network." }, { - "name": "prepareReturn", + "name": "prepareExecute", "params": [ { "name": "params", "schema": { - "title": "prepareReturnParams", + "title": "prepareExecuteParams", "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/JsPrepareSubmissionRequest" } } @@ -91,42 +79,36 @@ "result": { "name": "result", "schema": { - "title": "prepareReturnResult", + "title": "prepareExecuteResult", + "type": "object", "properties": { - "response": { - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/JsPrepareSubmissionResponse" + "userUrl": { + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/UserUrl" } }, - "required": ["response"] + "required": ["userUrl"] } }, - "description": "Processes the prepare step and returns the data to sign." + "description": "Prepares, signs, and executes a transaction." }, { - "name": "prepareExecute", + "name": "signMessage", "params": [ { "name": "params", "schema": { - "title": "prepareExecuteParams", - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/JsPrepareSubmissionRequest" + "title": "signMessageParams", + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/SignMessageRequest" } } ], "result": { "name": "result", "schema": { - "title": "prepareExecuteResult", - "type": "object", - "properties": { - "userUrl": { - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/UserUrl" - } - }, - "required": ["userUrl"] + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/SignMessageResult" } }, - "description": "Prepares, signs, and executes a transaction." + "description": "Signs a message." }, { "name": "ledgerApi", @@ -148,7 +130,7 @@ "description": "Proxy for the JSON-API endpoints. Injects authorization headers automatically." }, { - "name": "onConnected", + "name": "connected", "params": [], "result": { "name": "result", @@ -169,7 +151,7 @@ } }, { - "name": "onAccountsChanged", + "name": "accountsChanged", "params": [], "result": { "name": "result", @@ -179,17 +161,28 @@ } }, { - "name": "requestAccounts", + "name": "getPrimaryAccount", + "params": [], + "result": { + "name": "result", + "schema": { + "$ref": "api-specs/openrpc-user-api.json#/components/schemas/Wallet" + } + }, + "description": "Returns the primary account." + }, + { + "name": "listAccounts", "params": [], "result": { "name": "result", "schema": { - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/RequestAccountsResult" + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/ListAccountsResult" } } }, { - "name": "onTxChanged", + "name": "txChanged", "params": [], "result": { "name": "result", diff --git a/core/wallet-dapp-rpc-client/package.json b/core/wallet-dapp-rpc-client/package.json index c1d048465..c02defe8f 100644 --- a/core/wallet-dapp-rpc-client/package.json +++ b/core/wallet-dapp-rpc-client/package.json @@ -31,18 +31,18 @@ "dependencies": { "@canton-network/core-rpc-transport": "workspace:^", "@canton-network/core-types": "workspace:^", - "lodash": "^4.17.23" + "lodash": "^4.17.21" }, "devDependencies": { "@types/isomorphic-fetch": "^0.0.39", "@types/jest": "^30.0.0", "@types/json-schema": "7.0.15", - "@types/lodash": "^4.17.23", + "@types/lodash": "^4.17.21", "@types/ws": "^8.18.1", "globals": "^16.5.0", - "prettier": "^3.8.1", + "prettier": "^3.7.1", "tsup": "^8.5.1", - "typedoc": "^0.28.16", + "typedoc": "^0.28.14", "typescript": "^5.9.3" }, "repository": { diff --git a/core/wallet-dapp-rpc-client/src/index.ts b/core/wallet-dapp-rpc-client/src/index.ts index 4c29ca8f0..97f3d8c42 100644 --- a/core/wallet-dapp-rpc-client/src/index.ts +++ b/core/wallet-dapp-rpc-client/src/index.ts @@ -85,6 +85,12 @@ export type PackageId = string * */ export type PackageIdSelectionPreference = PackageId[] +/** + * + * The message to sign. + * + */ +export type Message = string export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' export type Resource = string export type Body = string @@ -195,30 +201,6 @@ export interface Session { userId: UserId [k: string]: any } -export type Dar = string -export type Dars = Dar[] -/** - * - * The prepared transaction data. - * - */ -export type PreparedTransaction = string -/** - * - * The hash of the prepared transaction. - * - */ -export type PreparedTransactionHash = string -/** - * - * Structure representing the result of a prepareReturn call - * - */ -export interface JsPrepareSubmissionResponse { - preparedTransaction?: PreparedTransaction - preparedTransactionHash?: PreparedTransactionHash - [k: string]: any -} /** * * The status of the transaction. @@ -251,6 +233,12 @@ export interface TxChangedExecutedEvent { commandId: CommandId payload: TxChangedExecutedPayload } +/** + * + * The signature of the transaction. + * + */ +export type Signature = string export type Response = string /** * @@ -359,12 +347,6 @@ export interface TxChangedPendingEvent { * */ export type StatusSigned = 'signed' -/** - * - * The signature of the transaction. - * - */ -export type Signature = string /** * * The identifier of the provider that signed the transaction. @@ -411,7 +393,7 @@ export interface TxChangedFailedEvent { * Structure representing the request for prepare and execute calls * */ -export interface PrepareReturnParams { +export interface PrepareExecuteParams { commandId?: CommandId commands: JsCommands actAs?: ActAs @@ -423,17 +405,11 @@ export interface PrepareReturnParams { } /** * - * Structure representing the request for prepare and execute calls + * Request to sign a message. * */ -export interface PrepareExecuteParams { - commandId?: CommandId - commands: JsCommands - actAs?: ActAs - readAs?: ReadAs - disclosedContracts?: DisclosedContracts - synchronizerId?: SynchronizerId - packageIdSelectionPreference?: PackageIdSelectionPreference +export interface SignMessageParams { + message: Message [k: string]: any } /** @@ -462,15 +438,19 @@ export interface StatusEvent { * */ export type Null = null -export interface DarsAvailableResult { - dars: Dars - [k: string]: any -} -export type PrepareReturnResult = any export interface PrepareExecuteAndWaitResult { tx: TxChangedExecutedEvent [k: string]: any } +/** + * + * Result of signing a message. + * + */ +export interface SignMessageResult { + signature: Signature + [k: string]: any +} /** * * Ledger Api configuration options @@ -491,7 +471,7 @@ export type AccountsChangedEvent = Wallet[] * An array of accounts that the user has authorized the dapp to access.. * */ -export type RequestAccountsResult = Wallet[] +export type ListAccountsResult = Wallet[] /** * * Event emitted when a transaction changes. @@ -511,18 +491,19 @@ export type TxChangedEvent = export type Status = () => Promise export type Connect = () => Promise export type Disconnect = () => Promise -export type DarsAvailable = () => Promise -export type PrepareReturn = ( - params: PrepareReturnParams -) => Promise +export type GetActiveNetwork = () => Promise export type PrepareExecute = (params: PrepareExecuteParams) => Promise export type PrepareExecuteAndWait = ( params: PrepareExecuteParams ) => Promise +export type SignMessage = ( + params: SignMessageParams +) => Promise export type LedgerApi = (params: LedgerApiParams) => Promise -export type OnAccountsChanged = () => Promise -export type RequestAccounts = () => Promise -export type OnTxChanged = () => Promise +export type AccountsChanged = () => Promise +export type GetPrimaryAccount = () => Promise +export type ListAccounts = () => Promise +export type TxChanged = () => Promise export class SpliceWalletJSONRPCDAppAPI { public transport: RpcTransport @@ -563,18 +544,9 @@ export class SpliceWalletJSONRPCDAppAPI { */ // tslint:disable-next-line:max-line-length public async request( - method: 'darsAvailable', - ...params: Parameters - ): ReturnType - - /** - * - */ - // tslint:disable-next-line:max-line-length - public async request( - method: 'prepareReturn', - ...params: Parameters - ): ReturnType + method: 'getActiveNetwork', + ...params: Parameters + ): ReturnType /** * @@ -594,6 +566,15 @@ export class SpliceWalletJSONRPCDAppAPI { ...params: Parameters ): ReturnType + /** + * + */ + // tslint:disable-next-line:max-line-length + public async request( + method: 'signMessage', + ...params: Parameters + ): ReturnType + /** * */ @@ -608,27 +589,36 @@ export class SpliceWalletJSONRPCDAppAPI { */ // tslint:disable-next-line:max-line-length public async request( - method: 'onAccountsChanged', - ...params: Parameters - ): ReturnType + method: 'accountsChanged', + ...params: Parameters + ): ReturnType + + /** + * + */ + // tslint:disable-next-line:max-line-length + public async request( + method: 'getPrimaryAccount', + ...params: Parameters + ): ReturnType /** * */ // tslint:disable-next-line:max-line-length public async request( - method: 'requestAccounts', - ...params: Parameters - ): ReturnType + method: 'listAccounts', + ...params: Parameters + ): ReturnType /** * */ // tslint:disable-next-line:max-line-length public async request( - method: 'onTxChanged', - ...params: Parameters - ): ReturnType + method: 'txChanged', + ...params: Parameters + ): ReturnType public async request( method: string, diff --git a/core/wallet-dapp-rpc-client/src/openrpc.json b/core/wallet-dapp-rpc-client/src/openrpc.json index 6073f6a32..8a59bb8b8 100644 --- a/core/wallet-dapp-rpc-client/src/openrpc.json +++ b/core/wallet-dapp-rpc-client/src/openrpc.json @@ -40,52 +40,15 @@ "description": "Invoke a disconnect of the wallet gateway session." }, { - "name": "darsAvailable", + "name": "getActiveNetwork", "params": [], "result": { "name": "result", "schema": { - "title": "darsAvailableResult", - "type": "object", - "properties": { - "dars": { - "title": "dars", - "type": "array", - "items": { - "title": "dar", - "type": "string" - } - } - }, - "required": ["dars"] + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/Network" } }, - "description": "Lists DARs currently available to the connected Validator node, such that dApp can verify whether the app is installed." - }, - { - "name": "prepareReturn", - "params": [ - { - "name": "params", - "schema": { - "title": "prepareReturnParams", - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/JsPrepareSubmissionRequest" - } - } - ], - "result": { - "name": "result", - "schema": { - "title": "prepareReturnResult", - "properties": { - "response": { - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/JsPrepareSubmissionResponse" - } - }, - "required": ["response"] - } - }, - "description": "Processes the prepare step and returns the data to sign." + "description": "Returns the active network." }, { "name": "prepareExecute", @@ -132,6 +95,25 @@ }, "description": "Like prepareExecute, but waits for the transaction to be executed on the ledger." }, + { + "name": "signMessage", + "params": [ + { + "name": "params", + "schema": { + "title": "signMessageParams", + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/SignMessageRequest" + } + } + ], + "result": { + "name": "result", + "schema": { + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/SignMessageResult" + } + }, + "description": "Signs a message." + }, { "name": "ledgerApi", "params": [ @@ -152,7 +134,7 @@ "description": "Proxy for the JSON-API endpoints. Injects authorization headers automatically." }, { - "name": "onAccountsChanged", + "name": "accountsChanged", "params": [], "result": { "name": "result", @@ -162,18 +144,29 @@ } }, { - "name": "requestAccounts", + "name": "getPrimaryAccount", + "params": [], + "result": { + "name": "result", + "schema": { + "$ref": "api-specs/openrpc-user-api.json#/components/schemas/Wallet" + } + }, + "description": "Returns the primary account." + }, + { + "name": "listAccounts", "params": [], "result": { "name": "result", "schema": { - "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/RequestAccountsResult" + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/ListAccountsResult" } }, "description": "Lists the addresses (wallets) with their properties; including which network they are associated to and with signing provider is used." }, { - "name": "onTxChanged", + "name": "txChanged", "params": [], "result": { "name": "result", @@ -368,8 +361,8 @@ "$ref": "api-specs/openrpc-user-api.json#/components/schemas/Wallet" } }, - "RequestAccountsResult": { - "title": "RequestAccountsResult", + "ListAccountsResult": { + "title": "ListAccountsResult", "type": "array", "description": "An array of accounts that the user has authorized the dapp to access..", "items": { @@ -542,31 +535,7 @@ "description": "If not connected to a network, the reason why." }, "network": { - "title": "network", - "type": "object", - "description": "Network information, if connected to a network.", - "properties": { - "networkId": { - "title": "networkId", - "type": "string", - "description": "A CAIP-2 compliant chain ID, e.g. 'canton:da-mainnet'." - }, - "ledgerApi": { - "title": "LedgerApiConfig", - "type": "object", - "description": "Ledger API configuration.", - "properties": { - "baseUrl": { - "title": "baseUrl", - "type": "string", - "format": "uri", - "description": "The base URL of the ledger API." - } - }, - "required": ["baseUrl"] - } - }, - "required": ["networkId"] + "$ref": "api-specs/openrpc-dapp-api.json#/components/schemas/Network" }, "session": { "title": "session", @@ -588,6 +557,59 @@ } }, "required": ["kernel", "isConnected", "isNetworkConnected"] + }, + "SignMessageRequest": { + "title": "SignMessageRequest", + "type": "object", + "description": "Request to sign a message.", + "properties": { + "message": { + "title": "message", + "type": "string", + "description": "The message to sign." + } + }, + "required": ["message"] + }, + "SignMessageResult": { + "title": "SignMessageResult", + "type": "object", + "description": "Result of signing a message.", + "properties": { + "signature": { + "title": "signature", + "type": "string", + "description": "The signature of the message." + } + }, + "required": ["signature"] + }, + "Network": { + "title": "network", + "type": "object", + "description": "Network information, if connected to a network.", + "properties": { + "networkId": { + "title": "networkId", + "type": "string", + "description": "A CAIP-2 compliant chain ID, e.g. 'canton:da-mainnet'." + }, + "ledgerApi": { + "title": "LedgerApiConfig", + "type": "object", + "description": "Ledger API configuration.", + "properties": { + "baseUrl": { + "title": "baseUrl", + "type": "string", + "format": "uri", + "description": "The base URL of the ledger API." + } + }, + "required": ["baseUrl"] + } + }, + "required": ["networkId"] } } } diff --git a/docs/wallet-gateway/src/dapp-sdk/usage.rst b/docs/wallet-gateway/src/dapp-sdk/usage.rst index de25cc430..16400b902 100644 --- a/docs/wallet-gateway/src/dapp-sdk/usage.rst +++ b/docs/wallet-gateway/src/dapp-sdk/usage.rst @@ -43,13 +43,13 @@ The status returns an object containing network connection- and session-related .catch(() => setStatus('disconnected')) -**Requesting Accounts** +**Listing Accounts** -The dApp SDK provides a `requestAccounts` method to request the accounts of the Wallet. +The dApp SDK provides a `listAccounts` method to list the accounts of the Wallet. .. code:: typescript - const accounts = await sdk.requestAccounts() + const accounts = await sdk.listAccounts() console.log(accounts) **Executing a Transaction** @@ -75,7 +75,7 @@ The transaction is returned as an object containing the command ID, update ID an }) // Get the primary party the user selected in the Wallet - const primaryParty = (await sdk.requestAccounts()).find((w) => w.primary)?.partyId + const primaryParty = (await sdk.listAccounts()).find((w) => w.primary)?.partyId // Request user's signature and execute the transaction await sdk.prepareExecute(createPingCommand(primaryParty!)) diff --git a/docs/wallet-integration-guide/src/signing-transactions-from-dapps/index.rst b/docs/wallet-integration-guide/src/signing-transactions-from-dapps/index.rst index be8badbbc..b3682a37f 100644 --- a/docs/wallet-integration-guide/src/signing-transactions-from-dapps/index.rst +++ b/docs/wallet-integration-guide/src/signing-transactions-from-dapps/index.rst @@ -15,8 +15,8 @@ through to the wallet provider for signing. Receiving a Transaction ----------------------- -A dApp would usually call the ``prepareReturn`` endpoint or the ``prepareExecute`` endpoint. In both cases the Wallet Provider -would prepare and sign the transaction (but for the ``prepareExecute`` it would also submit it to the ledger). +A dApp would usually call the ``prepareExecute`` endpoint or the ``prepareExecuteAndWait`` endpoint. In both cases the Wallet Provider +would prepare, sign and submit the transaction to the ledger. You can prepare the incoming transaction using the Wallet SDK: diff --git a/examples/ping/src/hooks/useAccounts.ts b/examples/ping/src/hooks/useAccounts.ts index a071b7d30..3b60f9798 100644 --- a/examples/ping/src/hooks/useAccounts.ts +++ b/examples/ping/src/hooks/useAccounts.ts @@ -12,9 +12,9 @@ export function useAccounts(status?: sdk.dappAPI.StatusEvent) { useEffect(() => { if (status?.isConnected) { - sdk.requestAccounts() - .then((wallets) => { - setAccounts(wallets) + sdk.listAccounts() + .then((accounts) => { + setAccounts(accounts) }) .catch((err) => { console.error('Error requesting wallets:', err) diff --git a/examples/portfolio/src/contexts/ConnectionProvider.tsx b/examples/portfolio/src/contexts/ConnectionProvider.tsx index 36f6e3c31..2947e1c26 100644 --- a/examples/portfolio/src/contexts/ConnectionProvider.tsx +++ b/examples/portfolio/src/contexts/ConnectionProvider.tsx @@ -64,11 +64,11 @@ export const ConnectionProvider: React.FC<{ children: React.ReactNode }> = ({ if (!provider || !connectionStatus?.isConnected) return provider .request({ - method: 'requestAccounts', + method: 'listAccounts', }) .then((wallets) => { const requestedAccounts = - wallets as sdk.dappAPI.RequestAccountsResult + wallets as sdk.dappAPI.ListAccountsResult setAccounts(requestedAccounts) }) .catch((err) => { diff --git a/sdk/dapp-sdk/src/dapp-api/rpc-gen/index.ts b/sdk/dapp-sdk/src/dapp-api/rpc-gen/index.ts index 6e005f3ab..43dbb9c3e 100644 --- a/sdk/dapp-sdk/src/dapp-api/rpc-gen/index.ts +++ b/sdk/dapp-sdk/src/dapp-api/rpc-gen/index.ts @@ -4,27 +4,29 @@ import { Status } from './typings.js' import { Connect } from './typings.js' import { Disconnect } from './typings.js' -import { DarsAvailable } from './typings.js' -import { PrepareReturn } from './typings.js' +import { GetActiveNetwork } from './typings.js' import { PrepareExecute } from './typings.js' import { PrepareExecuteAndWait } from './typings.js' +import { SignMessage } from './typings.js' import { LedgerApi } from './typings.js' -import { OnAccountsChanged } from './typings.js' -import { RequestAccounts } from './typings.js' -import { OnTxChanged } from './typings.js' +import { AccountsChanged } from './typings.js' +import { GetPrimaryAccount } from './typings.js' +import { ListAccounts } from './typings.js' +import { TxChanged } from './typings.js' export type Methods = { status: Status connect: Connect disconnect: Disconnect - darsAvailable: DarsAvailable - prepareReturn: PrepareReturn + getActiveNetwork: GetActiveNetwork prepareExecute: PrepareExecute prepareExecuteAndWait: PrepareExecuteAndWait + signMessage: SignMessage ledgerApi: LedgerApi - onAccountsChanged: OnAccountsChanged - requestAccounts: RequestAccounts - onTxChanged: OnTxChanged + accountsChanged: AccountsChanged + getPrimaryAccount: GetPrimaryAccount + listAccounts: ListAccounts + txChanged: TxChanged } function buildController(methods: Methods) { @@ -32,14 +34,15 @@ function buildController(methods: Methods) { status: methods.status, connect: methods.connect, disconnect: methods.disconnect, - darsAvailable: methods.darsAvailable, - prepareReturn: methods.prepareReturn, + getActiveNetwork: methods.getActiveNetwork, prepareExecute: methods.prepareExecute, prepareExecuteAndWait: methods.prepareExecuteAndWait, + signMessage: methods.signMessage, ledgerApi: methods.ledgerApi, - onAccountsChanged: methods.onAccountsChanged, - requestAccounts: methods.requestAccounts, - onTxChanged: methods.onTxChanged, + accountsChanged: methods.accountsChanged, + getPrimaryAccount: methods.getPrimaryAccount, + listAccounts: methods.listAccounts, + txChanged: methods.txChanged, } } diff --git a/sdk/dapp-sdk/src/dapp-api/rpc-gen/typings.ts b/sdk/dapp-sdk/src/dapp-api/rpc-gen/typings.ts index d68cf2744..e8c813aae 100644 --- a/sdk/dapp-sdk/src/dapp-api/rpc-gen/typings.ts +++ b/sdk/dapp-sdk/src/dapp-api/rpc-gen/typings.ts @@ -84,6 +84,12 @@ export type PackageId = string * */ export type PackageIdSelectionPreference = PackageId[] +/** + * + * The message to sign. + * + */ +export type Message = string export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' export type Resource = string export type Body = string @@ -194,30 +200,6 @@ export interface Session { userId: UserId [k: string]: any } -export type Dar = string -export type Dars = Dar[] -/** - * - * The prepared transaction data. - * - */ -export type PreparedTransaction = string -/** - * - * The hash of the prepared transaction. - * - */ -export type PreparedTransactionHash = string -/** - * - * Structure representing the result of a prepareReturn call - * - */ -export interface JsPrepareSubmissionResponse { - preparedTransaction?: PreparedTransaction - preparedTransactionHash?: PreparedTransactionHash - [k: string]: any -} /** * * The status of the transaction. @@ -250,6 +232,12 @@ export interface TxChangedExecutedEvent { commandId: CommandId payload: TxChangedExecutedPayload } +/** + * + * The signature of the transaction. + * + */ +export type Signature = string export type Response = string /** * @@ -358,12 +346,6 @@ export interface TxChangedPendingEvent { * */ export type StatusSigned = 'signed' -/** - * - * The signature of the transaction. - * - */ -export type Signature = string /** * * The identifier of the provider that signed the transaction. @@ -410,7 +392,7 @@ export interface TxChangedFailedEvent { * Structure representing the request for prepare and execute calls * */ -export interface PrepareReturnParams { +export interface PrepareExecuteParams { commandId?: CommandId commands: JsCommands actAs?: ActAs @@ -422,17 +404,11 @@ export interface PrepareReturnParams { } /** * - * Structure representing the request for prepare and execute calls + * Request to sign a message. * */ -export interface PrepareExecuteParams { - commandId?: CommandId - commands: JsCommands - actAs?: ActAs - readAs?: ReadAs - disclosedContracts?: DisclosedContracts - synchronizerId?: SynchronizerId - packageIdSelectionPreference?: PackageIdSelectionPreference +export interface SignMessageParams { + message: Message [k: string]: any } /** @@ -461,15 +437,19 @@ export interface StatusEvent { * */ export type Null = null -export interface DarsAvailableResult { - dars: Dars - [k: string]: any -} -export type PrepareReturnResult = any export interface PrepareExecuteAndWaitResult { tx: TxChangedExecutedEvent [k: string]: any } +/** + * + * Result of signing a message. + * + */ +export interface SignMessageResult { + signature: Signature + [k: string]: any +} /** * * Ledger Api configuration options @@ -490,7 +470,7 @@ export type AccountsChangedEvent = Wallet[] * An array of accounts that the user has authorized the dapp to access.. * */ -export type RequestAccountsResult = Wallet[] +export type ListAccountsResult = Wallet[] /** * * Event emitted when a transaction changes. @@ -510,15 +490,16 @@ export type TxChangedEvent = export type Status = () => Promise export type Connect = () => Promise export type Disconnect = () => Promise -export type DarsAvailable = () => Promise -export type PrepareReturn = ( - params: PrepareReturnParams -) => Promise +export type GetActiveNetwork = () => Promise export type PrepareExecute = (params: PrepareExecuteParams) => Promise export type PrepareExecuteAndWait = ( params: PrepareExecuteParams ) => Promise +export type SignMessage = ( + params: SignMessageParams +) => Promise export type LedgerApi = (params: LedgerApiParams) => Promise -export type OnAccountsChanged = () => Promise -export type RequestAccounts = () => Promise -export type OnTxChanged = () => Promise +export type AccountsChanged = () => Promise +export type GetPrimaryAccount = () => Promise +export type ListAccounts = () => Promise +export type TxChanged = () => Promise diff --git a/sdk/dapp-sdk/src/provider.ts b/sdk/dapp-sdk/src/provider.ts index a385a4c96..d47125d7d 100644 --- a/sdk/dapp-sdk/src/provider.ts +++ b/sdk/dapp-sdk/src/provider.ts @@ -19,7 +19,13 @@ import { PrepareExecuteParams, } from '@canton-network/core-wallet-dapp-rpc-client' import { ErrorCode } from './error.js' -import { Session, StatusEvent } from './dapp-api/rpc-gen/typings' +import { + Network, + Session, + SignMessageResult, + StatusEvent, + Wallet, +} from './dapp-api/rpc-gen/typings' import { popup } from '@canton-network/core-wallet-ui-components' /** @@ -57,8 +63,6 @@ export class Provider implements SpliceProvider { return controller.connect() as Promise case 'disconnect': return controller.disconnect() as Promise - case 'darsAvailable': - return controller.darsAvailable() as Promise case 'ledgerApi': return controller.ledgerApi( args.params as LedgerApiParams @@ -67,16 +71,12 @@ export class Provider implements SpliceProvider { return controller.prepareExecute( args.params as PrepareExecuteParams ) as Promise + case 'listAccounts': + return controller.listAccounts() as Promise case 'prepareExecuteAndWait': return controller.prepareExecuteAndWait( args.params as PrepareExecuteParams ) as Promise - case 'prepareReturn': - return controller.prepareReturn( - args.params as dappAPI.PrepareReturnParams - ) as Promise - case 'requestAccounts': - return controller.requestAccounts() as Promise default: throw new Error('Unsupported method') } @@ -133,7 +133,7 @@ export const dappController = (provider: SpliceProvider) => 5 * 60 * 1000 ) provider.on( - 'onConnected', + 'connected', (event) => { clearTimeout(timeout) resolve(event) @@ -150,11 +150,6 @@ export const dappController = (provider: SpliceProvider) => method: 'disconnect', }) }, - darsAvailable: async () => { - return provider.request({ - method: 'darsAvailable', - }) - }, ledgerApi: async (params: LedgerApiParams) => provider.request({ method: 'ledgerApi', @@ -218,22 +213,28 @@ export const dappController = (provider: SpliceProvider) => return promise }, - prepareReturn: async (params: dappAPI.PrepareReturnParams) => - provider.request({ - method: 'prepareReturn', - params, - }), status: async () => { return provider.request({ method: 'status' }) }, - requestAccounts: async () => - provider.request({ - method: 'requestAccounts', + listAccounts: async () => + provider.request({ + method: 'listAccounts', }), - onAccountsChanged: async () => { + accountsChanged: async () => { throw new Error('Only for events.') }, - onTxChanged: async () => { + txChanged: async () => { throw new Error('Only for events.') }, + getActiveNetwork: function (): Promise { + throw new Error('Function not implemented.') + }, + signMessage: function (): Promise { + throw new Error('Function not implemented.') + }, + getPrimaryAccount: function (): Promise { + return provider.request({ + method: 'getPrimaryAccount', + }) + }, }) diff --git a/sdk/dapp-sdk/src/provider/request.ts b/sdk/dapp-sdk/src/provider/request.ts index 1c1eed3d5..b08e54cea 100644 --- a/sdk/dapp-sdk/src/provider/request.ts +++ b/sdk/dapp-sdk/src/provider/request.ts @@ -88,15 +88,9 @@ export async function status(): Promise { }) } -export async function darsAvailable(): Promise { - return await assertProvider().request({ - method: 'darsAvailable', - }) -} - -export async function requestAccounts(): Promise { - return await assertProvider().request({ - method: 'requestAccounts', +export async function listAccounts(): Promise { + return await assertProvider().request({ + method: 'listAccounts', }) } diff --git a/wallet-gateway/extension/src/dapp-api/controller.ts b/wallet-gateway/extension/src/dapp-api/controller.ts index 9988d6917..2bcd899ff 100644 --- a/wallet-gateway/extension/src/dapp-api/controller.ts +++ b/wallet-gateway/extension/src/dapp-api/controller.ts @@ -9,8 +9,11 @@ import buildController from './rpc-gen' import { KernelInfo, LedgerApiParams, + Network, PrepareExecuteParams, - PrepareReturnParams, + SignMessageParams, + SignMessageResult, + Wallet, } from './rpc-gen/typings.js' import { Store } from '@canton-network/core-wallet-store' @@ -42,7 +45,6 @@ export const dappController = (store?: Store) => }, }), disconnect: async () => Promise.resolve(null), - darsAvailable: async () => Promise.resolve({ dars: ['default-dar'] }), ledgerApi: async (params: LedgerApiParams) => Promise.resolve({ response: 'default-response' }), prepareExecute: async (params: PrepareExecuteParams) => { @@ -51,19 +53,28 @@ export const dappController = (store?: Store) => prepareExecuteAndWait: async (params: PrepareExecuteParams) => { throw new Error('Function not implemented.') }, - prepareReturn: async (params: PrepareReturnParams) => - Promise.resolve({}), status: async () => { throw new Error('Function not implemented.') }, - requestAccounts: async () => { + listAccounts: async () => { const wallets = await store!.getWallets() return wallets }, - onAccountsChanged: async () => { + accountsChanged: async () => { throw new Error('Only for events.') }, - onTxChanged: async () => { + txChanged: async () => { throw new Error('Only for events.') }, + getActiveNetwork: function (): Promise { + throw new Error('Function not implemented.') + }, + signMessage: function ( + params: SignMessageParams + ): Promise { + throw new Error('Function not implemented.') + }, + getPrimaryAccount: async function (): Promise { + throw new Error('Function not implemented.') + }, }) diff --git a/wallet-gateway/extension/src/dapp-api/rpc-gen/index.ts b/wallet-gateway/extension/src/dapp-api/rpc-gen/index.ts index 6e005f3ab..43dbb9c3e 100644 --- a/wallet-gateway/extension/src/dapp-api/rpc-gen/index.ts +++ b/wallet-gateway/extension/src/dapp-api/rpc-gen/index.ts @@ -4,27 +4,29 @@ import { Status } from './typings.js' import { Connect } from './typings.js' import { Disconnect } from './typings.js' -import { DarsAvailable } from './typings.js' -import { PrepareReturn } from './typings.js' +import { GetActiveNetwork } from './typings.js' import { PrepareExecute } from './typings.js' import { PrepareExecuteAndWait } from './typings.js' +import { SignMessage } from './typings.js' import { LedgerApi } from './typings.js' -import { OnAccountsChanged } from './typings.js' -import { RequestAccounts } from './typings.js' -import { OnTxChanged } from './typings.js' +import { AccountsChanged } from './typings.js' +import { GetPrimaryAccount } from './typings.js' +import { ListAccounts } from './typings.js' +import { TxChanged } from './typings.js' export type Methods = { status: Status connect: Connect disconnect: Disconnect - darsAvailable: DarsAvailable - prepareReturn: PrepareReturn + getActiveNetwork: GetActiveNetwork prepareExecute: PrepareExecute prepareExecuteAndWait: PrepareExecuteAndWait + signMessage: SignMessage ledgerApi: LedgerApi - onAccountsChanged: OnAccountsChanged - requestAccounts: RequestAccounts - onTxChanged: OnTxChanged + accountsChanged: AccountsChanged + getPrimaryAccount: GetPrimaryAccount + listAccounts: ListAccounts + txChanged: TxChanged } function buildController(methods: Methods) { @@ -32,14 +34,15 @@ function buildController(methods: Methods) { status: methods.status, connect: methods.connect, disconnect: methods.disconnect, - darsAvailable: methods.darsAvailable, - prepareReturn: methods.prepareReturn, + getActiveNetwork: methods.getActiveNetwork, prepareExecute: methods.prepareExecute, prepareExecuteAndWait: methods.prepareExecuteAndWait, + signMessage: methods.signMessage, ledgerApi: methods.ledgerApi, - onAccountsChanged: methods.onAccountsChanged, - requestAccounts: methods.requestAccounts, - onTxChanged: methods.onTxChanged, + accountsChanged: methods.accountsChanged, + getPrimaryAccount: methods.getPrimaryAccount, + listAccounts: methods.listAccounts, + txChanged: methods.txChanged, } } diff --git a/wallet-gateway/extension/src/dapp-api/rpc-gen/typings.ts b/wallet-gateway/extension/src/dapp-api/rpc-gen/typings.ts index d68cf2744..e8c813aae 100644 --- a/wallet-gateway/extension/src/dapp-api/rpc-gen/typings.ts +++ b/wallet-gateway/extension/src/dapp-api/rpc-gen/typings.ts @@ -84,6 +84,12 @@ export type PackageId = string * */ export type PackageIdSelectionPreference = PackageId[] +/** + * + * The message to sign. + * + */ +export type Message = string export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' export type Resource = string export type Body = string @@ -194,30 +200,6 @@ export interface Session { userId: UserId [k: string]: any } -export type Dar = string -export type Dars = Dar[] -/** - * - * The prepared transaction data. - * - */ -export type PreparedTransaction = string -/** - * - * The hash of the prepared transaction. - * - */ -export type PreparedTransactionHash = string -/** - * - * Structure representing the result of a prepareReturn call - * - */ -export interface JsPrepareSubmissionResponse { - preparedTransaction?: PreparedTransaction - preparedTransactionHash?: PreparedTransactionHash - [k: string]: any -} /** * * The status of the transaction. @@ -250,6 +232,12 @@ export interface TxChangedExecutedEvent { commandId: CommandId payload: TxChangedExecutedPayload } +/** + * + * The signature of the transaction. + * + */ +export type Signature = string export type Response = string /** * @@ -358,12 +346,6 @@ export interface TxChangedPendingEvent { * */ export type StatusSigned = 'signed' -/** - * - * The signature of the transaction. - * - */ -export type Signature = string /** * * The identifier of the provider that signed the transaction. @@ -410,7 +392,7 @@ export interface TxChangedFailedEvent { * Structure representing the request for prepare and execute calls * */ -export interface PrepareReturnParams { +export interface PrepareExecuteParams { commandId?: CommandId commands: JsCommands actAs?: ActAs @@ -422,17 +404,11 @@ export interface PrepareReturnParams { } /** * - * Structure representing the request for prepare and execute calls + * Request to sign a message. * */ -export interface PrepareExecuteParams { - commandId?: CommandId - commands: JsCommands - actAs?: ActAs - readAs?: ReadAs - disclosedContracts?: DisclosedContracts - synchronizerId?: SynchronizerId - packageIdSelectionPreference?: PackageIdSelectionPreference +export interface SignMessageParams { + message: Message [k: string]: any } /** @@ -461,15 +437,19 @@ export interface StatusEvent { * */ export type Null = null -export interface DarsAvailableResult { - dars: Dars - [k: string]: any -} -export type PrepareReturnResult = any export interface PrepareExecuteAndWaitResult { tx: TxChangedExecutedEvent [k: string]: any } +/** + * + * Result of signing a message. + * + */ +export interface SignMessageResult { + signature: Signature + [k: string]: any +} /** * * Ledger Api configuration options @@ -490,7 +470,7 @@ export type AccountsChangedEvent = Wallet[] * An array of accounts that the user has authorized the dapp to access.. * */ -export type RequestAccountsResult = Wallet[] +export type ListAccountsResult = Wallet[] /** * * Event emitted when a transaction changes. @@ -510,15 +490,16 @@ export type TxChangedEvent = export type Status = () => Promise export type Connect = () => Promise export type Disconnect = () => Promise -export type DarsAvailable = () => Promise -export type PrepareReturn = ( - params: PrepareReturnParams -) => Promise +export type GetActiveNetwork = () => Promise export type PrepareExecute = (params: PrepareExecuteParams) => Promise export type PrepareExecuteAndWait = ( params: PrepareExecuteParams ) => Promise +export type SignMessage = ( + params: SignMessageParams +) => Promise export type LedgerApi = (params: LedgerApiParams) => Promise -export type OnAccountsChanged = () => Promise -export type RequestAccounts = () => Promise -export type OnTxChanged = () => Promise +export type AccountsChanged = () => Promise +export type GetPrimaryAccount = () => Promise +export type ListAccounts = () => Promise +export type TxChanged = () => Promise diff --git a/wallet-gateway/remote/src/dapp-api/controller.ts b/wallet-gateway/remote/src/dapp-api/controller.ts index 50d6389df..34056d2ae 100644 --- a/wallet-gateway/remote/src/dapp-api/controller.ts +++ b/wallet-gateway/remote/src/dapp-api/controller.ts @@ -5,10 +5,12 @@ import { assertConnected, AuthContext } from '@canton-network/core-wallet-auth' import buildController from './rpc-gen/index.js' import { LedgerApiParams, + Network, PrepareExecuteParams, - PrepareReturnParams, + SignMessageResult, StatusEvent, StatusEventAsync, + Wallet, } from './rpc-gen/typings.js' import { Store, Transaction } from '@canton-network/core-wallet-store' import { @@ -91,7 +93,6 @@ export const dappController = ( return null }, - darsAvailable: async () => ({ dars: ['default-dar'] }), ledgerApi: async (params: LedgerApiParams) => { const network = await store.getCurrentNetwork() const ledgerClient = new LedgerClient({ @@ -180,33 +181,6 @@ export const dappController = ( userUrl: `${userUrl}/approve/index.html?commandId=${commandId}`, } }, - prepareReturn: async (params: PrepareReturnParams) => { - const wallet = await store.getPrimaryWallet() - const network = await store.getCurrentNetwork() - - if (context === undefined) { - throw new Error('Unauthenticated context') - } - - if (wallet === undefined) { - throw new Error('No primary wallet found') - } - - const ledgerClient = new LedgerClient({ - baseUrl: new URL(network.ledgerApi.baseUrl), - logger, - isAdmin: false, - accessToken: context.accessToken, - }) - - return prepareSubmission( - context.userId, - wallet.partyId, - network.synchronizerId, - params, - ledgerClient - ) - }, status: async () => { if (!context || !(await store.getSession())) { return { @@ -245,22 +219,35 @@ export const dappController = ( userUrl: `${userUrl}/login/`, } as StatusEventAsync }, - onConnected: async () => { + connected: async () => { throw new Error('Only for events.') }, onStatusChanged: async () => { throw new Error('Only for events.') }, - onAccountsChanged: async () => { + accountsChanged: async () => { throw new Error('Only for events.') }, - requestAccounts: async () => { + listAccounts: async () => { const wallets = await store.getWallets() return wallets }, - onTxChanged: async () => { + txChanged: async () => { throw new Error('Only for events.') }, + getActiveNetwork: function (): Promise { + throw new Error('Function not implemented.') + }, + signMessage: function (): Promise { + throw new Error('Function not implemented.') + }, + getPrimaryAccount: async function (): Promise { + const wallet = await store.getPrimaryWallet() + if (!wallet) { + throw new Error('No primary wallet found') + } + return wallet + }, }) } @@ -268,7 +255,7 @@ async function prepareSubmission( userId: string, partyId: string, synchronizerId: string, - params: PrepareExecuteParams | PrepareReturnParams, + params: PrepareExecuteParams, ledgerClient: LedgerClient ): Promise> { return await ledgerClient.postWithRetry( diff --git a/wallet-gateway/remote/src/dapp-api/rpc-gen/index.ts b/wallet-gateway/remote/src/dapp-api/rpc-gen/index.ts index 25b3cf40e..7c484517a 100644 --- a/wallet-gateway/remote/src/dapp-api/rpc-gen/index.ts +++ b/wallet-gateway/remote/src/dapp-api/rpc-gen/index.ts @@ -4,29 +4,31 @@ import { Status } from './typings.js' import { Connect } from './typings.js' import { Disconnect } from './typings.js' -import { DarsAvailable } from './typings.js' -import { PrepareReturn } from './typings.js' +import { GetActiveNetwork } from './typings.js' import { PrepareExecute } from './typings.js' +import { SignMessage } from './typings.js' import { LedgerApi } from './typings.js' -import { OnConnected } from './typings.js' +import { Connected } from './typings.js' import { OnStatusChanged } from './typings.js' -import { OnAccountsChanged } from './typings.js' -import { RequestAccounts } from './typings.js' -import { OnTxChanged } from './typings.js' +import { AccountsChanged } from './typings.js' +import { GetPrimaryAccount } from './typings.js' +import { ListAccounts } from './typings.js' +import { TxChanged } from './typings.js' export type Methods = { status: Status connect: Connect disconnect: Disconnect - darsAvailable: DarsAvailable - prepareReturn: PrepareReturn + getActiveNetwork: GetActiveNetwork prepareExecute: PrepareExecute + signMessage: SignMessage ledgerApi: LedgerApi - onConnected: OnConnected + connected: Connected onStatusChanged: OnStatusChanged - onAccountsChanged: OnAccountsChanged - requestAccounts: RequestAccounts - onTxChanged: OnTxChanged + accountsChanged: AccountsChanged + getPrimaryAccount: GetPrimaryAccount + listAccounts: ListAccounts + txChanged: TxChanged } function buildController(methods: Methods) { @@ -34,15 +36,16 @@ function buildController(methods: Methods) { status: methods.status, connect: methods.connect, disconnect: methods.disconnect, - darsAvailable: methods.darsAvailable, - prepareReturn: methods.prepareReturn, + getActiveNetwork: methods.getActiveNetwork, prepareExecute: methods.prepareExecute, + signMessage: methods.signMessage, ledgerApi: methods.ledgerApi, - onConnected: methods.onConnected, + connected: methods.connected, onStatusChanged: methods.onStatusChanged, - onAccountsChanged: methods.onAccountsChanged, - requestAccounts: methods.requestAccounts, - onTxChanged: methods.onTxChanged, + accountsChanged: methods.accountsChanged, + getPrimaryAccount: methods.getPrimaryAccount, + listAccounts: methods.listAccounts, + txChanged: methods.txChanged, } } diff --git a/wallet-gateway/remote/src/dapp-api/rpc-gen/typings.ts b/wallet-gateway/remote/src/dapp-api/rpc-gen/typings.ts index d23c8b5db..ac3d31652 100644 --- a/wallet-gateway/remote/src/dapp-api/rpc-gen/typings.ts +++ b/wallet-gateway/remote/src/dapp-api/rpc-gen/typings.ts @@ -84,6 +84,12 @@ export type PackageId = string * */ export type PackageIdSelectionPreference = PackageId[] +/** + * + * The message to sign. + * + */ +export type Message = string export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' export type Resource = string export type Body = string @@ -207,30 +213,12 @@ export interface ConnectResult { userUrl: UserUrl [k: string]: any } -export type Dar = string -export type Dars = Dar[] /** * - * The prepared transaction data. - * - */ -export type PreparedTransaction = string -/** - * - * The hash of the prepared transaction. - * - */ -export type PreparedTransactionHash = string -/** - * - * Structure representing the result of a prepareReturn call + * The signature of the transaction. * */ -export interface JsPrepareSubmissionResponse { - preparedTransaction?: PreparedTransaction - preparedTransactionHash?: PreparedTransactionHash - [k: string]: any -} +export type Signature = string export type Response = string /** * @@ -339,12 +327,6 @@ export interface TxChangedPendingEvent { * */ export type StatusSigned = 'signed' -/** - * - * The signature of the transaction. - * - */ -export type Signature = string /** * * The identifier of the provider that signed the transaction. @@ -423,7 +405,7 @@ export interface TxChangedFailedEvent { * Structure representing the request for prepare and execute calls * */ -export interface PrepareReturnParams { +export interface PrepareExecuteParams { commandId?: CommandId commands: JsCommands actAs?: ActAs @@ -435,17 +417,11 @@ export interface PrepareReturnParams { } /** * - * Structure representing the request for prepare and execute calls + * Request to sign a message. * */ -export interface PrepareExecuteParams { - commandId?: CommandId - commands: JsCommands - actAs?: ActAs - readAs?: ReadAs - disclosedContracts?: DisclosedContracts - synchronizerId?: SynchronizerId - packageIdSelectionPreference?: PackageIdSelectionPreference +export interface SignMessageParams { + message: Message [k: string]: any } /** @@ -466,15 +442,19 @@ export type StatusEventAsync = StatusEvent & ConnectResult * */ export type Null = null -export interface DarsAvailableResult { - dars: Dars - [k: string]: any -} -export type PrepareReturnResult = any export interface PrepareExecuteResult { userUrl: UserUrl [k: string]: any } +/** + * + * Result of signing a message. + * + */ +export interface SignMessageResult { + signature: Signature + [k: string]: any +} /** * * Ledger Api configuration options @@ -495,7 +475,7 @@ export type AccountsChangedEvent = Wallet[] * An array of accounts that the user has authorized the dapp to access.. * */ -export type RequestAccountsResult = Wallet[] +export type ListAccountsResult = Wallet[] /** * * Event emitted when a transaction changes. @@ -515,16 +495,17 @@ export type TxChangedEvent = export type Status = () => Promise export type Connect = () => Promise export type Disconnect = () => Promise -export type DarsAvailable = () => Promise -export type PrepareReturn = ( - params: PrepareReturnParams -) => Promise +export type GetActiveNetwork = () => Promise export type PrepareExecute = ( params: PrepareExecuteParams ) => Promise +export type SignMessage = ( + params: SignMessageParams +) => Promise export type LedgerApi = (params: LedgerApiParams) => Promise -export type OnConnected = () => Promise +export type Connected = () => Promise export type OnStatusChanged = () => Promise -export type OnAccountsChanged = () => Promise -export type RequestAccounts = () => Promise -export type OnTxChanged = () => Promise +export type AccountsChanged = () => Promise +export type GetPrimaryAccount = () => Promise +export type ListAccounts = () => Promise +export type TxChanged = () => Promise diff --git a/wallet-gateway/remote/src/dapp-api/server.ts b/wallet-gateway/remote/src/dapp-api/server.ts index 658e410fb..1ba03b25d 100644 --- a/wallet-gateway/remote/src/dapp-api/server.ts +++ b/wallet-gateway/remote/src/dapp-api/server.ts @@ -88,14 +88,14 @@ export const dapp = ( io.to(sessionId).emit('statusChanged', ...event) } const onConnected = (...event: unknown[]) => { - io.to(sessionId).emit('onConnected', ...event) + io.to(sessionId).emit('connected', ...event) } const onTxChanged = (...event: unknown[]) => { io.to(sessionId).emit('txChanged', ...event) } notifier.on('accountsChanged', onAccountsChanged) - notifier.on('onConnected', onConnected) + notifier.on('connected', onConnected) notifier.on('statusChanged', onStatusChanged) notifier.on('txChanged', onTxChanged) @@ -103,7 +103,7 @@ export const dapp = ( logger.debug('Socket.io client disconnected') notifier.removeListener('accountsChanged', onAccountsChanged) - notifier.removeListener('onConnected', onConnected) + notifier.removeListener('connected', onConnected) notifier.removeListener('statusChanged', onStatusChanged) notifier.removeListener('txChanged', onTxChanged) }) diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 783c6d5fd..98cdf6550 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -774,7 +774,7 @@ export const userController = ( accessToken, }) const status = await networkStatus(ledgerClient) - notifier.emit('onConnected', { + notifier.emit('connected', { kernel: { ...kernelInfo, userUrl: `${userUrl}/login/`, diff --git a/yarn.lock b/yarn.lock index 16cf773d6..99852dd46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1756,13 +1756,13 @@ __metadata: "@types/isomorphic-fetch": "npm:^0.0.39" "@types/jest": "npm:^30.0.0" "@types/json-schema": "npm:7.0.15" - "@types/lodash": "npm:^4.17.23" + "@types/lodash": "npm:^4.17.21" "@types/ws": "npm:^8.18.1" globals: "npm:^16.5.0" - lodash: "npm:^4.17.23" - prettier: "npm:^3.8.1" + lodash: "npm:^4.17.21" + prettier: "npm:^3.7.1" tsup: "npm:^8.5.1" - typedoc: "npm:^0.28.16" + typedoc: "npm:^0.28.14" typescript: "npm:^5.9.3" languageName: unknown linkType: soft @@ -1776,13 +1776,13 @@ __metadata: "@types/isomorphic-fetch": "npm:^0.0.39" "@types/jest": "npm:^30.0.0" "@types/json-schema": "npm:7.0.15" - "@types/lodash": "npm:^4.17.23" + "@types/lodash": "npm:^4.17.21" "@types/ws": "npm:^8.18.1" globals: "npm:^16.5.0" - lodash: "npm:^4.17.23" - prettier: "npm:^3.8.1" + lodash: "npm:^4.17.21" + prettier: "npm:^3.7.1" tsup: "npm:^8.5.1" - typedoc: "npm:^0.28.16" + typedoc: "npm:^0.28.14" typescript: "npm:^5.9.3" languageName: unknown linkType: soft @@ -7413,7 +7413,7 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:^4.17.20, @types/lodash@npm:^4.17.23, @types/lodash@npm:^4.5": +"@types/lodash@npm:^4.17.20, @types/lodash@npm:^4.17.21, @types/lodash@npm:^4.17.23, @types/lodash@npm:^4.5": version: 4.17.23 resolution: "@types/lodash@npm:4.17.23" checksum: 10c0/9d9cbfb684e064a2b78aab9e220d398c9c2a7d36bc51a07b184ff382fa043a99b3d00c16c7f109b4eb8614118f4869678dbae7d5c6700ed16fb9340e26cc0bf6 @@ -16137,7 +16137,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.5.0, prettier@npm:^3.8.1": +"prettier@npm:^3.5.0, prettier@npm:^3.7.1, prettier@npm:^3.8.1": version: 3.8.1 resolution: "prettier@npm:3.8.1" bin: @@ -18385,7 +18385,7 @@ __metadata: languageName: node linkType: hard -"typedoc@npm:^0.28.16": +"typedoc@npm:^0.28.14, typedoc@npm:^0.28.16": version: 0.28.16 resolution: "typedoc@npm:0.28.16" dependencies: From 6e7926430d256d992c9273cbf0092d064015ca8b Mon Sep 17 00:00:00 2001 From: Fayi <112705750+fayi-da@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:26:28 +0000 Subject: [PATCH 03/16] feat(example-portfolio): add network banner (#1186) Signed-off-by: fayi-da --- .../src/components/network-banner.tsx | 27 +++++++++++++++++++ examples/portfolio/src/routes/__root.tsx | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 examples/portfolio/src/components/network-banner.tsx diff --git a/examples/portfolio/src/components/network-banner.tsx b/examples/portfolio/src/components/network-banner.tsx new file mode 100644 index 000000000..ff99775b6 --- /dev/null +++ b/examples/portfolio/src/components/network-banner.tsx @@ -0,0 +1,27 @@ +import { Box, Typography } from '@mui/material' +import { useConnection } from '../contexts/ConnectionContext' + +export const NetworkBanner = () => { + const { status } = useConnection() + const networkId = status?.network?.networkId + + return ( + theme.zIndex.appBar + 1, + }} + > + + {networkId + ? `Network: ${networkId}` + : 'Not Connected to a Network'} + + + ) +} diff --git a/examples/portfolio/src/routes/__root.tsx b/examples/portfolio/src/routes/__root.tsx index f6a531a70..453aa0423 100644 --- a/examples/portfolio/src/routes/__root.tsx +++ b/examples/portfolio/src/routes/__root.tsx @@ -3,6 +3,7 @@ import { TanStackDevtools } from '@tanstack/react-devtools' import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' import { Header } from '../components/header' +import { NetworkBanner } from '../components/network-banner' import { Container } from '@mui/material' import type { QueryClient } from '@tanstack/react-query' @@ -17,6 +18,7 @@ export const Route = createRootRouteWithContext()({ function RootComponent() { return ( <> +
From a6ecd7bfdf2af0149271e02015a79d362f08c310 Mon Sep 17 00:00:00 2001 From: Marc Juchli <120378272+mjuchli-da@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:38:41 +0100 Subject: [PATCH 04/16] fix: revert part of #869 to make signing-store-sql non-breaking (#1193) Instead of altering the columns in the 001 migration, we copy the data to a new table with the corrected schema. Lastly, the table is renamed back to the original name. Signed-off-by: Marc Juchli --- .../src/migrations/001-init.ts | 8 +- .../src/migrations/003-alter-date-fields.ts | 303 ++++++++++++++++++ 2 files changed, 307 insertions(+), 4 deletions(-) create mode 100644 core/signing-store-sql/src/migrations/003-alter-date-fields.ts diff --git a/core/signing-store-sql/src/migrations/001-init.ts b/core/signing-store-sql/src/migrations/001-init.ts index fe8f7cdfe..1fd755bde 100644 --- a/core/signing-store-sql/src/migrations/001-init.ts +++ b/core/signing-store-sql/src/migrations/001-init.ts @@ -17,8 +17,8 @@ export async function up(db: Kysely): Promise { .addColumn('public_key', 'text', (col) => col.notNull()) .addColumn('private_key', 'text') // Encrypted for internal driver .addColumn('metadata', 'text') // JSON string for driver-specific data - .addColumn('created_at', 'text', (col) => col.notNull()) - .addColumn('updated_at', 'text', (col) => col.notNull()) + .addColumn('created_at', 'integer', (col) => col.notNull()) + .addColumn('updated_at', 'integer', (col) => col.notNull()) .addUniqueConstraint('signing_keys_user_id_id_unique', [ 'user_id', 'id', @@ -36,8 +36,8 @@ export async function up(db: Kysely): Promise { .addColumn('public_key', 'text', (col) => col.notNull()) .addColumn('status', 'text', (col) => col.notNull()) .addColumn('metadata', 'text') // JSON string for driver-specific data - .addColumn('created_at', 'text', (col) => col.notNull()) - .addColumn('updated_at', 'text', (col) => col.notNull()) + .addColumn('created_at', 'integer', (col) => col.notNull()) + .addColumn('updated_at', 'integer', (col) => col.notNull()) .addUniqueConstraint('signing_transactions_user_id_id_unique', [ 'user_id', 'id', diff --git a/core/signing-store-sql/src/migrations/003-alter-date-fields.ts b/core/signing-store-sql/src/migrations/003-alter-date-fields.ts new file mode 100644 index 000000000..d74eab7aa --- /dev/null +++ b/core/signing-store-sql/src/migrations/003-alter-date-fields.ts @@ -0,0 +1,303 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Kysely, sql } from 'kysely' +import { DB } from '../schema.js' + +export async function up(db: Kysely): Promise { + console.log('Altering date fields to text (SQLite compatible)') + + // --- signing_transactions --- + // Create temporary table with new schema + await db.schema + .createTable('signing_transactions_tmp') + .addColumn('id', 'text', (col) => col.primaryKey()) + .addColumn('user_id', 'text', (col) => col.notNull()) + .addColumn('hash', 'text', (col) => col.notNull()) + .addColumn('signature', 'text') + .addColumn('public_key', 'text', (col) => col.notNull()) + .addColumn('status', 'text', (col) => col.notNull()) + .addColumn('metadata', 'text') + .addColumn('created_at', 'text', (col) => col.notNull()) + .addColumn('updated_at', 'text', (col) => col.notNull()) + .addColumn('signed_at', 'text') + .addUniqueConstraint('signing_transactions_user_id_id_unique', [ + 'user_id', + 'id', + ]) + .execute() + + // Copy data, converting integer timestamps to text + // Check if signed_at column exists + const tableInfo = await sql<{ name: string }>` + SELECT name FROM pragma_table_info('signing_transactions') WHERE name = 'signed_at' + `.execute(db) + + const hasSignedAt = tableInfo.rows.length > 0 + + if (hasSignedAt) { + await sql` + INSERT INTO signing_transactions_tmp + SELECT + id, + user_id, + hash, + signature, + public_key, + status, + metadata, + CAST(created_at AS TEXT) as created_at, + CAST(updated_at AS TEXT) as updated_at, + signed_at + FROM signing_transactions + `.execute(db) + } else { + await sql` + INSERT INTO signing_transactions_tmp + SELECT + id, + user_id, + hash, + signature, + public_key, + status, + metadata, + CAST(created_at AS TEXT) as created_at, + CAST(updated_at AS TEXT) as updated_at, + NULL as signed_at + FROM signing_transactions + `.execute(db) + } + + // Drop old table + await db.schema.dropTable('signing_transactions').execute() + + // Rename temporary table + await db.schema + .alterTable('signing_transactions_tmp') + .renameTo('signing_transactions') + .execute() + + // Recreate indexes + await db.schema + .createIndex('idx_signing_transactions_user_id') + .on('signing_transactions') + .column('user_id') + .execute() + + await db.schema + .createIndex('idx_signing_transactions_status') + .on('signing_transactions') + .column('status') + .execute() + + await db.schema + .createIndex('idx_signing_transactions_created_at') + .on('signing_transactions') + .column('created_at') + .execute() + + // --- signing_keys --- + // Create temporary table with new schema + await db.schema + .createTable('signing_keys_tmp') + .addColumn('id', 'text', (col) => col.primaryKey()) + .addColumn('user_id', 'text', (col) => col.notNull()) + .addColumn('name', 'text', (col) => col.notNull()) + .addColumn('public_key', 'text', (col) => col.notNull()) + .addColumn('private_key', 'text') + .addColumn('metadata', 'text') + .addColumn('created_at', 'text', (col) => col.notNull()) + .addColumn('updated_at', 'text', (col) => col.notNull()) + .addUniqueConstraint('signing_keys_user_id_id_unique', [ + 'user_id', + 'id', + ]) + .execute() + + // Copy data, converting integer timestamps to text + await sql` + INSERT INTO signing_keys_tmp + SELECT + id, + user_id, + name, + public_key, + private_key, + metadata, + CAST(created_at AS TEXT) as created_at, + CAST(updated_at AS TEXT) as updated_at + FROM signing_keys + `.execute(db) + + // Drop old table + await db.schema.dropTable('signing_keys').execute() + + // Rename temporary table + await db.schema + .alterTable('signing_keys_tmp') + .renameTo('signing_keys') + .execute() + + // Recreate indexes + await db.schema + .createIndex('idx_signing_keys_user_id') + .on('signing_keys') + .column('user_id') + .execute() + + await db.schema + .createIndex('idx_signing_keys_public_key') + .on('signing_keys') + .column('public_key') + .execute() +} + +export async function down(db: Kysely): Promise { + console.log('Reverting date fields to integer (SQLite compatible)') + + // --- signing_transactions --- + // Create temporary table with old schema + await db.schema + .createTable('signing_transactions_tmp') + .addColumn('id', 'text', (col) => col.primaryKey()) + .addColumn('user_id', 'text', (col) => col.notNull()) + .addColumn('hash', 'text', (col) => col.notNull()) + .addColumn('signature', 'text') + .addColumn('public_key', 'text', (col) => col.notNull()) + .addColumn('status', 'text', (col) => col.notNull()) + .addColumn('metadata', 'text') + .addColumn('created_at', 'integer', (col) => col.notNull()) + .addColumn('updated_at', 'integer', (col) => col.notNull()) + .addColumn('signed_at', 'text') + .addUniqueConstraint('signing_transactions_user_id_id_unique', [ + 'user_id', + 'id', + ]) + .execute() + + // Copy data, converting text timestamps to integer + // Check if signed_at column exists + const tableInfo = await sql<{ name: string }>` + SELECT name FROM pragma_table_info('signing_transactions') WHERE name = 'signed_at' + `.execute(db) + + const hasSignedAt = tableInfo.rows.length > 0 + + if (hasSignedAt) { + await sql` + INSERT INTO signing_transactions_tmp + SELECT + id, + user_id, + hash, + signature, + public_key, + status, + metadata, + CAST(created_at AS INTEGER) as created_at, + CAST(updated_at AS INTEGER) as updated_at, + signed_at + FROM signing_transactions + `.execute(db) + } else { + await sql` + INSERT INTO signing_transactions_tmp + SELECT + id, + user_id, + hash, + signature, + public_key, + status, + metadata, + CAST(created_at AS INTEGER) as created_at, + CAST(updated_at AS INTEGER) as updated_at, + NULL as signed_at + FROM signing_transactions + `.execute(db) + } + + // Drop old table + await db.schema.dropTable('signing_transactions').execute() + + // Rename temporary table + await db.schema + .alterTable('signing_transactions_tmp') + .renameTo('signing_transactions') + .execute() + + // Recreate indexes + await db.schema + .createIndex('idx_signing_transactions_user_id') + .on('signing_transactions') + .column('user_id') + .execute() + + await db.schema + .createIndex('idx_signing_transactions_status') + .on('signing_transactions') + .column('status') + .execute() + + await db.schema + .createIndex('idx_signing_transactions_created_at') + .on('signing_transactions') + .column('created_at') + .execute() + + // --- signing_keys --- + // Create temporary table with old schema + await db.schema + .createTable('signing_keys_tmp') + .addColumn('id', 'text', (col) => col.primaryKey()) + .addColumn('user_id', 'text', (col) => col.notNull()) + .addColumn('name', 'text', (col) => col.notNull()) + .addColumn('public_key', 'text', (col) => col.notNull()) + .addColumn('private_key', 'text') + .addColumn('metadata', 'text') + .addColumn('created_at', 'integer', (col) => col.notNull()) + .addColumn('updated_at', 'integer', (col) => col.notNull()) + .addUniqueConstraint('signing_keys_user_id_id_unique', [ + 'user_id', + 'id', + ]) + .execute() + + // Copy data, converting text timestamps to integer + await sql` + INSERT INTO signing_keys_tmp + SELECT + id, + user_id, + name, + public_key, + private_key, + metadata, + CAST(created_at AS INTEGER) as created_at, + CAST(updated_at AS INTEGER) as updated_at + FROM signing_keys + `.execute(db) + + // Drop old table + await db.schema.dropTable('signing_keys').execute() + + // Rename temporary table + await db.schema + .alterTable('signing_keys_tmp') + .renameTo('signing_keys') + .execute() + + // Recreate indexes + await db.schema + .createIndex('idx_signing_keys_user_id') + .on('signing_keys') + .column('user_id') + .execute() + + await db.schema + .createIndex('idx_signing_keys_public_key') + .on('signing_keys') + .column('public_key') + .execute() +} From cc57979a1e8958eb858d5e46eb54742aee997c8d Mon Sep 17 00:00:00 2001 From: Fayi <112705750+fayi-da@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:22:18 +0000 Subject: [PATCH 05/16] feat(example-portfolio): ensure registries are always set (#1191) --------- Signed-off-by: fayi-da --- .../src/components/registry-settings.tsx | 11 +- .../components/registry-validation-modal.tsx | 108 ++++++++++++++++++ .../src/hooks/useRegistryValidation.ts | 69 +++++++++++ examples/portfolio/src/routes/__root.tsx | 6 + examples/portfolio/tests/example.spec.ts | 3 +- 5 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 examples/portfolio/src/components/registry-validation-modal.tsx create mode 100644 examples/portfolio/src/hooks/useRegistryValidation.ts diff --git a/examples/portfolio/src/components/registry-settings.tsx b/examples/portfolio/src/components/registry-settings.tsx index 3a2ab8077..1a37f7b15 100644 --- a/examples/portfolio/src/components/registry-settings.tsx +++ b/examples/portfolio/src/components/registry-settings.tsx @@ -78,11 +78,16 @@ export function RegistrySettings() { Registries + + The API URLs provided by the party managing the token + standard compliant instruments + - - Add New Registry -
{ e.preventDefault() diff --git a/examples/portfolio/src/components/registry-validation-modal.tsx b/examples/portfolio/src/components/registry-validation-modal.tsx new file mode 100644 index 000000000..161d5dbaf --- /dev/null +++ b/examples/portfolio/src/components/registry-validation-modal.tsx @@ -0,0 +1,108 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + Dialog, + DialogContent, + DialogActions, + Typography, + Button, + Box, + Link as MuiLink, +} from '@mui/material' +import { createLink, useRouterState } from '@tanstack/react-router' +import WarningAmberIcon from '@mui/icons-material/WarningAmber' +import type { RegistryValidationStatus } from '../hooks/useRegistryValidation' + +const RouterLink = createLink(MuiLink) + +interface RegistryValidationModalProps { + validationStatus: RegistryValidationStatus +} + +export function RegistryValidationModal({ + validationStatus, +}: RegistryValidationModalProps) { + const routerState = useRouterState() + const currentPath = routerState.location.pathname + + // TODO: remove this once old components are removed. + const isOnSkippablePage = ['/settings', '/old'].includes(currentPath) + + const shouldShowModal = + !isOnSkippablePage && + (validationStatus === 'no-registries' || + validationStatus === 'all-unreachable') + + if (!shouldShowModal) { + return null + } + + const isNoRegistries = validationStatus === 'no-registries' + + return ( + e.stopPropagation(), + }, + }} + > + + + + + Registry Configuration Required + + + + {isNoRegistries ? ( + <> + + No token registries are configured. You need to + add at least one registry to use the portfolio. + + + ) : ( + <> + + Unable to connect to any configured registries. + + + All configured registries are unreachable. + Please check your network connection or update + the registry URLs. + + + )} + + + + + + + + ) +} diff --git a/examples/portfolio/src/hooks/useRegistryValidation.ts b/examples/portfolio/src/hooks/useRegistryValidation.ts new file mode 100644 index 000000000..e9de0728e --- /dev/null +++ b/examples/portfolio/src/hooks/useRegistryValidation.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useQuery } from '@tanstack/react-query' +import { useRegistryUrls } from '../contexts/RegistryServiceContext' +import { resolveTokenStandardClient } from '../services/resolve' + +export type RegistryValidationStatus = + | 'valid' + | 'no-registries' + | 'all-unreachable' + | 'checking' + +interface RegistryCheckResult { + partyId: string + url: string + reachable: boolean +} + +async function checkRegistryReachability( + url: string, + partyId: string +): Promise { + try { + const client = await resolveTokenStandardClient({ registryUrl: url }) + await client.get('/registry/metadata/v1/info') + return { partyId, url, reachable: true } + } catch { + return { partyId, url, reachable: false } + } +} + +export function useRegistryValidation(): RegistryValidationStatus { + const registryUrls = useRegistryUrls() + + const registryEntries = Array.from(registryUrls.entries()) + + const query = useQuery({ + queryKey: ['registry-validation', registryEntries], + queryFn: async (): Promise => { + if (registryEntries.length === 0) { + return 'no-registries' + } + + const results = await Promise.all( + registryEntries.map(([partyId, url]) => + checkRegistryReachability(url, partyId) + ) + ) + + const anyIsReachable = results.some((r) => r.reachable) + if (anyIsReachable) { + return 'valid' + } + + return 'all-unreachable' + }, + retry: 1, // retrying just once here incase of transient networks errors. + refetchInterval: 30_000, + refetchOnWindowFocus: true, + placeholderData: (prev) => prev, + }) + + if (query.isLoading && !query.data) { + return 'checking' + } + + return query.data ?? 'checking' +} diff --git a/examples/portfolio/src/routes/__root.tsx b/examples/portfolio/src/routes/__root.tsx index 453aa0423..e6b64794f 100644 --- a/examples/portfolio/src/routes/__root.tsx +++ b/examples/portfolio/src/routes/__root.tsx @@ -4,6 +4,8 @@ import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' import { Header } from '../components/header' import { NetworkBanner } from '../components/network-banner' +import { RegistryValidationModal } from '../components/registry-validation-modal' +import { useRegistryValidation } from '../hooks/useRegistryValidation' import { Container } from '@mui/material' import type { QueryClient } from '@tanstack/react-query' @@ -16,6 +18,8 @@ export const Route = createRootRouteWithContext()({ }) function RootComponent() { + const validationStatus = useRegistryValidation() + return ( <> @@ -37,6 +41,8 @@ function RootComponent() { ]} /> + + ) } diff --git a/examples/portfolio/tests/example.spec.ts b/examples/portfolio/tests/example.spec.ts index eab212a3c..e7a7e8f39 100644 --- a/examples/portfolio/tests/example.spec.ts +++ b/examples/portfolio/tests/example.spec.ts @@ -22,7 +22,8 @@ const setupRegistry = async (page: Page): Promise => { .getByRole('textbox', { name: 'url' }) .fill('http://scan.localhost:4000') await page.getByRole('button', { name: 'Add registry' }).click() - await expect(page.getByText('DSO::')).toBeVisible() + // await expect(page.getByText('DSO::')).toBeVisible() + await expect(page.getByRole('cell', { name: /^DSO::/ })).toBeVisible() } const tap = async ( From df4335a4cf823053f92debf375e691b9db6c63e7 Mon Sep 17 00:00:00 2001 From: Marc Juchli <120378272+mjuchli-da@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:06:06 +0100 Subject: [PATCH 06/16] chore: fix methods in doc (#1198) Signed-off-by: Marc Juchli --- .../src/wallet-gateway/apis/index.rst | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/wallet-gateway/src/wallet-gateway/apis/index.rst b/docs/wallet-gateway/src/wallet-gateway/apis/index.rst index ba0182041..eb69d1df2 100644 --- a/docs/wallet-gateway/src/wallet-gateway/apis/index.rst +++ b/docs/wallet-gateway/src/wallet-gateway/apis/index.rst @@ -39,16 +39,34 @@ The User API enables users to manage their wallets, configure networks, manage i **Key Methods:** +**Sessions:** +- ``addSession()`` - Create a new session (unauthenticated, used for initial connection) +- ``removeSession()`` - End the current session +- ``listSessions()`` - List sessions for the current user + +**Networks:** - ``listNetworks()`` - List all configured networks - ``addNetwork()`` - Add a new network configuration - ``removeNetwork()`` - Remove a network configuration + +**Identity providers:** - ``listIdps()`` - List all identity providers - ``addIdp()`` - Add a new identity provider - ``removeIdp()`` - Remove an identity provider -- ``addSession()`` - Create a new session (unauthenticated, used for initial connection) -- ``createParty()`` - Create a new party on a network -- ``listParties()`` - List all parties for the current user -- ``listWallets()`` - List all wallets + +**Wallets:** +- ``createWallet()`` - Create a new wallet (party) on a network +- ``listWallets()`` - List all wallets for the current user +- ``setPrimaryWallet()`` - Set the primary wallet +- ``removeWallet()`` - Remove a wallet +- ``syncWallets()`` - Sync wallets with the ledger +- ``isWalletSyncNeeded()`` - Check if wallet sync is needed + +**Transactions:** +- ``sign()`` - Sign a transaction +- ``execute()`` - Execute a signed transaction +- ``getTransaction()`` - Get a transaction by ID +- ``listTransactions()`` - List transactions **Authentication:** From 1be3b68fecd0881aa8a2066fdc328f5edaecd7ab Mon Sep 17 00:00:00 2001 From: Marc Juchli <120378272+mjuchli-da@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:52:54 +0100 Subject: [PATCH 07/16] chore: doc wallet gateway overview section (#1199) Signed-off-by: Marc Juchli --- docs/wallet-gateway/src/dapp-sdk/index.rst | 2 + .../src/dapp-sdk/installation.rst | 2 + docs/wallet-gateway/src/dapp-sdk/usage.rst | 2 + docs/wallet-gateway/src/overview/index.rst | 54 ++++++++++++- .../src/wallet-gateway/usage/index.rst | 81 ++++++++++++++++++- 5 files changed, 138 insertions(+), 3 deletions(-) diff --git a/docs/wallet-gateway/src/dapp-sdk/index.rst b/docs/wallet-gateway/src/dapp-sdk/index.rst index c223cbc76..e6d529b6d 100644 --- a/docs/wallet-gateway/src/dapp-sdk/index.rst +++ b/docs/wallet-gateway/src/dapp-sdk/index.rst @@ -1,3 +1,5 @@ +.. _dapp-sdk: + dApp SDK ======== diff --git a/docs/wallet-gateway/src/dapp-sdk/installation.rst b/docs/wallet-gateway/src/dapp-sdk/installation.rst index e92bac3c1..13e1224d0 100644 --- a/docs/wallet-gateway/src/dapp-sdk/installation.rst +++ b/docs/wallet-gateway/src/dapp-sdk/installation.rst @@ -1,3 +1,5 @@ +.. _dapp-sdk-installation: + Installation ============ diff --git a/docs/wallet-gateway/src/dapp-sdk/usage.rst b/docs/wallet-gateway/src/dapp-sdk/usage.rst index 16400b902..67468f8b6 100644 --- a/docs/wallet-gateway/src/dapp-sdk/usage.rst +++ b/docs/wallet-gateway/src/dapp-sdk/usage.rst @@ -1,3 +1,5 @@ +.. _dapp-sdk-usage: + dApp SDK Usage ============== diff --git a/docs/wallet-gateway/src/overview/index.rst b/docs/wallet-gateway/src/overview/index.rst index 0966df4e6..06a43b86b 100644 --- a/docs/wallet-gateway/src/overview/index.rst +++ b/docs/wallet-gateway/src/overview/index.rst @@ -1,5 +1,57 @@ Overview ======== -This section will provide an overview of the dApp building process. +This guide helps you build **dApps** (decentralized applications) that interact with the **Canton Network** through the **Wallet Gateway** or other dApp API-compatible wallets. +You use the **dApp SDK** in your frontend to connect users to their wallets, and the Wallet Gateway mediates between your dApp, Canton validator nodes, and signing providers. +What You're Building +-------------------- + +A typical setup involves: + +- **dApp** — A web or mobile application that lets users view ledger data, create contracts, and submit transactions. Your dApp uses the dApp SDK to connect to a wallet and call the dApp API. +- **Wallet Gateway** — A server that exposes the dApp API and User API, manages sessions, and talks to Canton validators and signing providers. +- **Canton Network** — The distributed ledger. Validator nodes expose a Ledger API; the Wallet Gateway connects to them on behalf of authenticated users. +- **Signing** — Transaction signing is handled by a **signing provider** (e.g. Canton participant, Fireblocks or Blockdaemon). Users create wallets (parties) tied to a network and a signing provider. For testing purposes the Gateway allows using it for signing. + +High-Level Architecture +----------------------- + +:: + + ┌─────────────┐ dApp API ┌──────────────────┐ Ledger API ┌─────────────────┐ + │ Your dApp │ ◄──────────────────► │ Wallet Gateway │ ◄─────────────────► │ Canton Validator│ + │ (dApp SDK) │ (HTTP / WebSocket) │ │ │ │ + └─────────────┘ │ ┌────────────┐ │ Signing └─────────────────┘ + │ │ │ User API │ │ + │ User interactions │ │ User UI │ │ ┌─────────────────┐ + └────────────────────────────►│ └────────────┘ │ ◄──►│ Signing Provider│ + (User UI / User API) │ │ │ (Participant, │ + └──────────────────┘ │ Fireblocks…) │ + └─────────────────┘ + +- **dApp → Wallet Gateway**: Your dApp uses the dApp SDK to call the **dApp API** (connect, list accounts, prepare and execute transactions). The SDK can use HTTP (remote Wallet Gateway) or ``postMessage`` (browser extension). +- **User → Wallet Gateway**: Users manage wallets and approve transactions via the **User UI** or programmatically via the **User API** (sessions, networks, IDPs, wallets, sign, execute). +- **Wallet Gateway → Canton / Signing**: The Gateway authenticates to validator Ledger APIs and forwards signing requests to the configured signing provider. + +dApp API vs User API +-------------------- + +- **dApp API** (``/api/v0/dapp``): For **dApps**. Used by your frontend (via the dApp SDK) to connect to a wallet, list accounts, prepare and execute transactions, and receive real-time updates. Requires a valid session (JWT). See :ref:`apis` and the :ref:`dapp-sdk` documentation. + +- **User API** (``/api/v0/user``): For **users** and **automation**. Used to manage sessions, networks, identity providers, wallets, and transactions (sign, execute, list). The **User UI** is built on the User API. Use it for custom UIs, scripts, or when integrating with your own auth and wallet flows. + +Discovery and Connection Flow +----------------------------- + +1. **Discovery**: Your dApp discovers available Wallet Gateway instances (e.g. via well-known URLs or a registry). Each Gateway exposes a base URL and kernel info. +2. **Connect**: The user chooses a Gateway. Your dApp calls ``connect()`` (dApp SDK). Depending on configuration, the user may be redirected to the Gateway’s Web UI to log in (OAuth or self-signed). +3. **Session**: After login, the Gateway creates a session and returns a JWT. The dApp SDK uses this to call the dApp API (``listAccounts``, ``prepareExecute``, etc.). +4. **Transactions**: When your dApp calls ``prepareExecute``, the user may need to approve the transaction in the User UI. Once signed and executed, your dApp receives the result and can react to ``TxChanged`` events. + +Where to Go Next +---------------- + +- **Building a dApp?** → Install the :ref:`dApp SDK `, follow :ref:`dApp SDK usage `, and use the :ref:`apis` (dApp API) as needed. +- **Running or configuring the Wallet Gateway?** → Start with :ref:`getting-started`, then :ref:`configuring-wallet-gateway`, :ref:`signing-providers`, and :ref:`apis` (User API). +- **Using the User UI or User API?** → See :ref:`usage` for typical workflows and when to use which interface. diff --git a/docs/wallet-gateway/src/wallet-gateway/usage/index.rst b/docs/wallet-gateway/src/wallet-gateway/usage/index.rst index 2d8983f77..e00d376ad 100644 --- a/docs/wallet-gateway/src/wallet-gateway/usage/index.rst +++ b/docs/wallet-gateway/src/wallet-gateway/usage/index.rst @@ -1,6 +1,83 @@ .. _usage: Using the Wallet Gateway -=============== +======================== -This section will help you use the Wallet Gateway in your project. +You can use the Wallet Gateway in two ways: + +- mainly through the **User UI** (Web UI) for end users +- or through the **User API** (for automation, custom UIs, or integration with your own systems). + +The **dApp API** is used by your dApp via the dApp SDK when users connect their wallet. See the :ref:`dApp SDK ` for more details. + +This section describes typical workflows, the User UI, session handling, and when to use which interface. + +User UI +------ + +The Wallet Gateway serves a **Web UI** at the Gateway root URL (e.g. ``http://localhost:3030``). Users manage wallets, approve transactions, and adjust settings there. + +**Main pages:** + +- **Login** (``/login``): Choose a network and identity provider (IDP), then sign in (OAuth redirect or self-signed). Unauthenticated users are redirected here when they need to log in. + +- **Wallets** (``/wallets``): List wallets, create new wallets (choose network, signing provider, party id), set the primary wallet, and remove wallets. This is the default landing page after login. + +- **Transactions** (``/transactions``): List transactions. View status and details for prepared, signed, and executed transactions. + +- **Approve** (``/approve``): Shown when a dApp requests a transaction (e.g. via ``prepareExecute``). The user reviews the transaction and signs or rejects it. The dApp is notified of the result. + +- **Settings** (``/settings``): Manage networks and identity providers (add, edit, remove), view sessions, and see Gateway version info. + +- **Callback** (``/callback``): Used internally for OAuth redirects after login. Users are redirected back to the intended page (e.g. ``/wallets``) or to the dApp. + +Users **log out** via the layout logout control. Logout calls ``removeSession``, clears local auth state, and redirects to ``/login`` (or closes the window if the UI was opened in a popup for approval). + + +When to use which interface +--------------------------- + +- **User UI**: Best for end users. They log in, create and manage wallets, view transactions, and approve dApp requests. No code required. + +- **User API**: Use when you need to: + - Drive wallet setup or management from scripts or your own backend. + - Build a custom wallet UI (e.g. embedded in your app) instead of the default User UI. + - Automate session, network, IDP, or wallet operations. + +- **dApp API** (via dApp SDK): Use from your **dApp** frontend. The SDK calls the dApp API to connect, list accounts, and prepare/execute transactions. Users approve via the Web UI or browser extension. See :ref:`dApp SDK usage ` and :ref:`apis` for details. + +Typical flows +------------- + +**1. User sets up a wallet** + +- User opens the User UI and goes to **Login**. +- Selects network and IDP, completes login (e.g. OAuth). +- Lands on **Wallets**, creates a wallet (network, signing provider, party id), optionally sets it as primary. +- Can add networks or IDPs under **Settings** if needed. + +**2. dApp connects and sends a transaction** + +- Your dApp uses the dApp SDK: ``connect()`` → user is redirected to Gateway to log in if needed → ``listAccounts()`` → ``prepareExecute(commands)``. +- User is sent to **Approve** to sign (or reject) the transaction. +- Once signed and executed, the dApp receives the result and can react to ``onTxChanged``. + +**3. User checks activity and manages wallets** + +- User opens **Transactions** to list and inspect transactions. +- User opens **Wallets** to add wallets, change primary, or remove wallets. +- User opens **Settings** to manage networks, IDPs, or sessions. + +**4. Automated wallet setup (User API)** + +- Your script or backend calls ``addSession()``, then your auth flow provides a JWT. +- Calls ``listNetworks()`` / ``listIdps()``, then ``createWallet()`` with desired network and signing provider. +- Uses ``listWallets()``, ``sign()``, ``execute()``, etc. as needed for your use case. + +Next steps +---------- + +- Configure the Gateway: :ref:`configuring-wallet-gateway` +- Explore User API and dApp API: :ref:`apis` +- Set up signing: :ref:`signing-providers` +- Run and operate the Gateway: :ref:`getting-started`, :ref:`troubleshooting` From 14c3a6b826cec146a53ca441e3b3af014dc9b2ea Mon Sep 17 00:00:00 2001 From: Fayi <112705750+fayi-da@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:21:45 +0000 Subject: [PATCH 08/16] feat(example-portfolio): show holdings for each wallet in portfolio (#1195) Signed-off-by: fayi-da --- .../portfolio/src/components/holding-row.tsx | 49 +++++++ .../src/components/instrument-accordion.tsx | 121 ++++++++++++++++++ .../src/components/instrument-select.tsx | 2 +- .../src/components/wallet-preview.tsx | 97 ++++++++------ .../src/components/wallets-preview.tsx | 4 +- ...nstruments.ts => useAggregatedHoldings.ts} | 0 .../hooks/useInstrumentAvailableBalance.ts | 2 +- .../portfolio/src/hooks/useWalletHoldings.ts | 52 ++++++++ examples/portfolio/src/routeTree.gen.ts | 24 +++- .../portfolio/src/routes/wallet.$walletId.tsx | 99 ++++++++++++++ 10 files changed, 405 insertions(+), 45 deletions(-) create mode 100644 examples/portfolio/src/components/holding-row.tsx create mode 100644 examples/portfolio/src/components/instrument-accordion.tsx rename examples/portfolio/src/hooks/{useWalletInstruments.ts => useAggregatedHoldings.ts} (100%) create mode 100644 examples/portfolio/src/hooks/useWalletHoldings.ts create mode 100644 examples/portfolio/src/routes/wallet.$walletId.tsx diff --git a/examples/portfolio/src/components/holding-row.tsx b/examples/portfolio/src/components/holding-row.tsx new file mode 100644 index 000000000..8bcf39f49 --- /dev/null +++ b/examples/portfolio/src/components/holding-row.tsx @@ -0,0 +1,49 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Box, Typography } from '@mui/material' +import LockIcon from '@mui/icons-material/Lock' +import { + TokenStandardService, + type Holding, +} from '@canton-network/core-ledger-client' +import { CopyableIdentifier } from './copyable-identifier' + +interface HoldingRowProps { + holding: Holding + symbol: string +} + +export const HoldingRow: React.FC = ({ holding, symbol }) => { + const isLocked = TokenStandardService.isHoldingLocked(holding, new Date()) + + return ( + + `1px solid ${theme.palette.divider}`, + }, + }} + > + + + {holding.amount} {symbol} + + {isLocked && ( + + )} + + + + ) +} diff --git a/examples/portfolio/src/components/instrument-accordion.tsx b/examples/portfolio/src/components/instrument-accordion.tsx new file mode 100644 index 000000000..d99b4381e --- /dev/null +++ b/examples/portfolio/src/components/instrument-accordion.tsx @@ -0,0 +1,121 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + Accordion, + AccordionSummary, + AccordionDetails, + Avatar, + Box, + Typography, +} from '@mui/material' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import type { Holding } from '@canton-network/core-ledger-client' +import type { AggregatedHolding } from '../utils/aggregate-holdings' +import { HoldingRow } from './holding-row' + +interface InstrumentAccordionProps { + aggregatedHolding: AggregatedHolding + holdings: Holding[] +} + +export const InstrumentAccordion: React.FC = ({ + aggregatedHolding, + holdings, +}) => { + const symbol = + aggregatedHolding.instrument?.symbol ?? + aggregatedHolding.instrumentId.id + const name = + aggregatedHolding.instrument?.name ?? aggregatedHolding.instrumentId.id + const hasLockedAmount = aggregatedHolding.lockedAmount !== '0' + + return ( + + `1px solid ${theme.palette.divider}`, + }, + }} + > + } + sx={{ + '&:hover': { + backgroundColor: 'action.hover', + }, + }} + > + + + {symbol[0]} + + + + {name} + + + {aggregatedHolding.totalAmount} {symbol} + + + {hasLockedAmount && ( + + + {aggregatedHolding.availableAmount} available + + + {aggregatedHolding.lockedAmount} locked + + + )} + + + + + {aggregatedHolding.numOfHoldings} holding + {aggregatedHolding.numOfHoldings !== 1 ? 's' : ''} + + + {holdings.map((holding) => ( + + ))} + + + + ) +} diff --git a/examples/portfolio/src/components/instrument-select.tsx b/examples/portfolio/src/components/instrument-select.tsx index 30eecd760..295e0fdce 100644 --- a/examples/portfolio/src/components/instrument-select.tsx +++ b/examples/portfolio/src/components/instrument-select.tsx @@ -11,7 +11,7 @@ import { Box, Typography, } from '@mui/material' -import { useAggregatedHoldings } from '../hooks/useWalletInstruments' +import { useAggregatedHoldings } from '../hooks/useAggregatedHoldings' import type { InstrumentId } from '@canton-network/core-token-standard' import Decimal from 'decimal.js' diff --git a/examples/portfolio/src/components/wallet-preview.tsx b/examples/portfolio/src/components/wallet-preview.tsx index 34fa1ba97..49c00658b 100644 --- a/examples/portfolio/src/components/wallet-preview.tsx +++ b/examples/portfolio/src/components/wallet-preview.tsx @@ -1,15 +1,18 @@ -import { Box, Avatar, Typography, Paper, Skeleton } from '@mui/material' -import { useAggregatedHoldings } from '../hooks/useWalletInstruments' +import { Box, Avatar, Typography, Paper, Skeleton, Chip } from '@mui/material' +import { Link } from '@tanstack/react-router' +import { useAggregatedHoldings } from '../hooks/useAggregatedHoldings' import type { AggregatedHolding } from '../utils/aggregate-holdings' interface WalletPreviewProps { partyId: string walletName: string + isPrimary?: boolean } export const WalletPreview: React.FC = ({ partyId, walletName, + isPrimary, }) => { const { instruments, isLoading } = useAggregatedHoldings(partyId) const hasInstruments = instruments.length > 0 @@ -48,47 +51,63 @@ export const WalletPreview: React.FC = ({ )) return ( - - - + + - {walletName} - - - - t.palette.mode === 'dark' - ? undefined - : t.palette.grey[100], - display: 'flex', - flexDirection: 'column', - }} - > - {isLoading - ? loadingState - : hasInstruments - ? renderedAssets - : noInstruments} + + {walletName} + + {isPrimary && ( + + )} + + + t.palette.mode === 'dark' + ? undefined + : t.palette.grey[100], + display: 'flex', + flexDirection: 'column', + cursor: 'pointer', + transition: 'all 0.2s ease-in-out', + '&:hover': { + borderColor: 'primary.main', + boxShadow: 1, + }, + }} + > + {isLoading + ? loadingState + : hasInstruments + ? renderedAssets + : noInstruments} + - + ) } diff --git a/examples/portfolio/src/components/wallets-preview.tsx b/examples/portfolio/src/components/wallets-preview.tsx index 0124dffed..74b7c5ac3 100644 --- a/examples/portfolio/src/components/wallets-preview.tsx +++ b/examples/portfolio/src/components/wallets-preview.tsx @@ -4,7 +4,8 @@ import { WalletPreview } from './wallet-preview' export const WalletsPreview = () => { const wallets = useAccounts() - console.log(wallets) + .slice() + .sort((a, b) => Number(a.primary) - Number(b.primary)) // make primary wallet first in the grid return ( @@ -27,6 +28,7 @@ export const WalletsPreview = () => { key={w.partyId} partyId={w.partyId} walletName={w.hint} + isPrimary={w.primary} /> ))} diff --git a/examples/portfolio/src/hooks/useWalletInstruments.ts b/examples/portfolio/src/hooks/useAggregatedHoldings.ts similarity index 100% rename from examples/portfolio/src/hooks/useWalletInstruments.ts rename to examples/portfolio/src/hooks/useAggregatedHoldings.ts diff --git a/examples/portfolio/src/hooks/useInstrumentAvailableBalance.ts b/examples/portfolio/src/hooks/useInstrumentAvailableBalance.ts index 18e75be11..62a667efe 100644 --- a/examples/portfolio/src/hooks/useInstrumentAvailableBalance.ts +++ b/examples/portfolio/src/hooks/useInstrumentAvailableBalance.ts @@ -1,6 +1,6 @@ import type { InstrumentId } from '@canton-network/core-token-standard' import type { AggregatedHolding } from '../utils/aggregate-holdings' -import { useAggregatedHoldings } from './useWalletInstruments' +import { useAggregatedHoldings } from './useAggregatedHoldings' import { useMemo } from 'react' export const useInstrumentAvailableBalance = ( diff --git a/examples/portfolio/src/hooks/useWalletHoldings.ts b/examples/portfolio/src/hooks/useWalletHoldings.ts new file mode 100644 index 000000000..2a566b4b1 --- /dev/null +++ b/examples/portfolio/src/hooks/useWalletHoldings.ts @@ -0,0 +1,52 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useMemo } from 'react' +import { useQuery } from '@tanstack/react-query' +import type { Holding } from '@canton-network/core-ledger-client' +import { listHoldings } from '../services/portfolio-service-implementation' +import { useInstruments } from '../contexts/RegistryServiceContext' +import { + aggregateHoldings, + enrichWithInstrumentInfo, + type AggregatedHolding, +} from '../utils/aggregate-holdings' +import { queryKeys } from './query-keys' + +export interface WalletHoldingsResult { + instruments: AggregatedHolding[] + holdings: Holding[] + isLoading: boolean + isError: boolean + error: Error | null + refetch: () => void +} + +export const useWalletHoldings = ( + partyId: string | undefined +): WalletHoldingsResult => { + const registryInstruments = useInstruments() + + const holdingsQuery = useQuery({ + queryKey: queryKeys.listHoldings.forParty(partyId), + queryFn: () => listHoldings({ party: partyId as string }), + enabled: !!partyId, + }) + + const aggregatedInstruments = useMemo(() => { + if (!holdingsQuery.data) return [] + return enrichWithInstrumentInfo( + aggregateHoldings(holdingsQuery.data), + registryInstruments + ) + }, [holdingsQuery.data, registryInstruments]) + + return { + instruments: aggregatedInstruments, + holdings: holdingsQuery.data ?? [], + isLoading: holdingsQuery.isLoading, + isError: holdingsQuery.isError, + error: holdingsQuery.error, + refetch: holdingsQuery.refetch, + } +} diff --git a/examples/portfolio/src/routeTree.gen.ts b/examples/portfolio/src/routeTree.gen.ts index 4bcf17723..5cd4c9f5b 100644 --- a/examples/portfolio/src/routeTree.gen.ts +++ b/examples/portfolio/src/routeTree.gen.ts @@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as SettingsRouteImport } from './routes/settings' import { Route as OldRouteImport } from './routes/old' import { Route as IndexRouteImport } from './routes/index' +import { Route as WalletWalletIdRouteImport } from './routes/wallet.$walletId' const SettingsRoute = SettingsRouteImport.update({ id: '/settings', @@ -28,35 +29,44 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const WalletWalletIdRoute = WalletWalletIdRouteImport.update({ + id: '/wallet/$walletId', + path: '/wallet/$walletId', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/old': typeof OldRoute '/settings': typeof SettingsRoute + '/wallet/$walletId': typeof WalletWalletIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/old': typeof OldRoute '/settings': typeof SettingsRoute + '/wallet/$walletId': typeof WalletWalletIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/old': typeof OldRoute '/settings': typeof SettingsRoute + '/wallet/$walletId': typeof WalletWalletIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/old' | '/settings' + fullPaths: '/' | '/old' | '/settings' | '/wallet/$walletId' fileRoutesByTo: FileRoutesByTo - to: '/' | '/old' | '/settings' - id: '__root__' | '/' | '/old' | '/settings' + to: '/' | '/old' | '/settings' | '/wallet/$walletId' + id: '__root__' | '/' | '/old' | '/settings' | '/wallet/$walletId' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute OldRoute: typeof OldRoute SettingsRoute: typeof SettingsRoute + WalletWalletIdRoute: typeof WalletWalletIdRoute } declare module '@tanstack/react-router' { @@ -82,6 +92,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/wallet/$walletId': { + id: '/wallet/$walletId' + path: '/wallet/$walletId' + fullPath: '/wallet/$walletId' + preLoaderRoute: typeof WalletWalletIdRouteImport + parentRoute: typeof rootRouteImport + } } } @@ -89,6 +106,7 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, OldRoute: OldRoute, SettingsRoute: SettingsRoute, + WalletWalletIdRoute: WalletWalletIdRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/examples/portfolio/src/routes/wallet.$walletId.tsx b/examples/portfolio/src/routes/wallet.$walletId.tsx new file mode 100644 index 000000000..86f9c6078 --- /dev/null +++ b/examples/portfolio/src/routes/wallet.$walletId.tsx @@ -0,0 +1,99 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { createFileRoute, Link } from '@tanstack/react-router' +import { Box, Typography, Paper, Skeleton, Button } from '@mui/material' +import ArrowBackIcon from '@mui/icons-material/ArrowBack' +import { useAccounts } from '../hooks/useAccounts' +import { useWalletHoldings } from '../hooks/useWalletHoldings' +import { CopyableIdentifier } from '../components/copyable-identifier' +import { InstrumentAccordion } from '../components/instrument-accordion' + +export const Route = createFileRoute('/wallet/$walletId')({ + component: WalletDetailPage, +}) + +function WalletDetailPage() { + const { walletId } = Route.useParams() + const accounts = useAccounts() + const wallet = accounts.find((a) => a.partyId === walletId) + const walletName = wallet?.hint ?? 'Unknown Wallet' + + const { instruments, holdings, isLoading, isError } = + useWalletHoldings(walletId) + + const getHoldingsForInstrument = (instrumentId: { + admin: string + id: string + }) => + holdings.filter( + (h) => + h.instrumentId.admin === instrumentId.admin && + h.instrumentId.id === instrumentId.id + ) + + return ( + + + + + + {walletName} + + + + + + + + Holdings + + + + {isLoading ? ( + + + + + ) : isError ? ( + + + Failed to load holdings + + + ) : instruments.length === 0 ? ( + + + No holdings in this wallet + + + ) : ( + instruments.map((instrument) => ( + + )) + )} + + + ) +} From 7ecc2bcec03bbc20fd12c04b91362a9092514b50 Mon Sep 17 00:00:00 2001 From: Marc Juchli <120378272+mjuchli-da@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:57:21 +0100 Subject: [PATCH 09/16] chore: doc intro improvement (#1203) Signed-off-by: Marc Juchli --- docs/wallet-gateway/src/overview/index.rst | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/wallet-gateway/src/overview/index.rst b/docs/wallet-gateway/src/overview/index.rst index 06a43b86b..95eafbb19 100644 --- a/docs/wallet-gateway/src/overview/index.rst +++ b/docs/wallet-gateway/src/overview/index.rst @@ -34,18 +34,29 @@ High-Level Architecture - **User → Wallet Gateway**: Users manage wallets and approve transactions via the **User UI** or programmatically via the **User API** (sessions, networks, IDPs, wallets, sign, execute). - **Wallet Gateway → Canton / Signing**: The Gateway authenticates to validator Ledger APIs and forwards signing requests to the configured signing provider. -dApp API vs User API +dApp API and dApp SDK +--------------------- + +The **dApp API** is a JSON-RPC 2.0 interface specified by **CIP-103**. +You can call it directly (e.g. over HTTP or WebSocket) from your frontend or backend. +In practice, most developers use the **dApp SDK**, which implements the same protocol and adds a simpler API, multi-transport support (HTTP for remote Gateways, ``postMessage`` for browser-extension wallets), and an EIP-1193–style provider (``window.canton``). +The dApp API lets your frontend connect to a wallet, list accounts, prepare and execute transactions, and receive real-time updates; all of this requires a valid session (JWT). +See :ref:`apis` and the :ref:`dapp-sdk` documentation. + +User API and User UI -------------------- -- **dApp API** (``/api/v0/dapp``): For **dApps**. Used by your frontend (via the dApp SDK) to connect to a wallet, list accounts, prepare and execute transactions, and receive real-time updates. Requires a valid session (JWT). See :ref:`apis` and the :ref:`dapp-sdk` documentation. +The **User API** is for users and automation: sessions, networks, identity providers, wallets, and transaction signing. +The **User UI** (served by the Wallet Gateway) is a web interface that uses the User API so users can log in, create and manage wallets, approve dApp transactions, and change settings. +For custom integrations or scripts, you can call the User API directly instead of using the User UI. +See :ref:`usage` and :ref:`apis`. -- **User API** (``/api/v0/user``): For **users** and **automation**. Used to manage sessions, networks, identity providers, wallets, and transactions (sign, execute, list). The **User UI** is built on the User API. Use it for custom UIs, scripts, or when integrating with your own auth and wallet flows. Discovery and Connection Flow ----------------------------- 1. **Discovery**: Your dApp discovers available Wallet Gateway instances (e.g. via well-known URLs or a registry). Each Gateway exposes a base URL and kernel info. -2. **Connect**: The user chooses a Gateway. Your dApp calls ``connect()`` (dApp SDK). Depending on configuration, the user may be redirected to the Gateway’s Web UI to log in (OAuth or self-signed). +2. **Connect**: The user chooses a Gateway. Your dApp calls ``connect()`` (dApp SDK). Depending on configuration, the user may be redirected to the Gateway’s User UI to log in (OAuth or self-signed). 3. **Session**: After login, the Gateway creates a session and returns a JWT. The dApp SDK uses this to call the dApp API (``listAccounts``, ``prepareExecute``, etc.). 4. **Transactions**: When your dApp calls ``prepareExecute``, the user may need to approve the transaction in the User UI. Once signed and executed, your dApp receives the result and can react to ``TxChanged`` events. From 945f6e25198e7d58fc0bffa04c27763bf6727cb8 Mon Sep 17 00:00:00 2001 From: Phillip Olesen Date: Wed, 28 Jan 2026 15:12:26 +0100 Subject: [PATCH 10/16] fix(wallet-gateway-remote): bump the default rate limit from 100 to 10000 (#1201) * fix(wallet-gateway-remote): bump the default rate limit from 100 to 10000 Signed-off-by: phillip olesen * updated comment Signed-off-by: phillip olesen * added to docs Signed-off-by: phillip olesen --------- Signed-off-by: phillip olesen --- .../wallet-gateway/src/wallet-gateway/configuration/index.rst | 4 +++- wallet-gateway/remote/src/config/Config.ts | 4 ++-- wallet-gateway/remote/src/example-config.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/wallet-gateway/src/wallet-gateway/configuration/index.rst b/docs/wallet-gateway/src/wallet-gateway/configuration/index.rst index aab7b2832..37caea3f1 100644 --- a/docs/wallet-gateway/src/wallet-gateway/configuration/index.rst +++ b/docs/wallet-gateway/src/wallet-gateway/configuration/index.rst @@ -87,6 +87,7 @@ The **server** section configures network binding, ports, and API paths. - *userPath* (optional, default: ``'/api/v0/user'``): The API path for user JSON-RPC requests. This is used by the web UI and user-facing applications. - *allowedOrigins* (optional, default: ``['*']``): CORS allowed origins. For production, specify exact origins instead of ``'*'`` for better security. Example: ``["https://my-dapp.com", "https://another-dapp.com"]``. - *requestSizeLimit* (optional, default: ``'1mb'``): Maximum request body size the server will accept. Use standard size notation (e.g., ``'1mb'``, ``'10mb'``, ``'50kb'``). + - *requestRateLimit* (optional, default: ``10000``): Maximum number of requests per minute from a single IP address (this excludes health endpoints). **Example:** @@ -98,7 +99,8 @@ The **server** section configures network binding, ports, and API paths. "dAppPath": "/api/v0/dapp", "userPath": "/api/v0/user", "allowedOrigins": ["https://my-dapp.example.com"], - "requestSizeLimit": "10mb" + "requestSizeLimit": "10mb", + "requestRateLimit": 10000 } } diff --git a/wallet-gateway/remote/src/config/Config.ts b/wallet-gateway/remote/src/config/Config.ts index 7f57e8aa4..f015d8b43 100644 --- a/wallet-gateway/remote/src/config/Config.ts +++ b/wallet-gateway/remote/src/config/Config.ts @@ -53,9 +53,9 @@ export const serverConfigSchema = z.object({ requestSizeLimit: z.string().default('1mb').meta({ description: 'The maximum size of incoming requests. Defaults to 1mb.', }), - requestRateLimit: z.number().default(100).meta({ + requestRateLimit: z.number().default(10000).meta({ description: - 'The maximum number of requests per minute from a single IP address. Defaults to 100.', + 'The maximum number of requests per minute from a single IP address. Defaults to 10000.', }), }) diff --git a/wallet-gateway/remote/src/example-config.ts b/wallet-gateway/remote/src/example-config.ts index 982efa865..725678212 100644 --- a/wallet-gateway/remote/src/example-config.ts +++ b/wallet-gateway/remote/src/example-config.ts @@ -14,7 +14,7 @@ export default { userPath: '/api/v0/user', allowedOrigins: '*', requestSizeLimit: '5mb', - requestRateLimit: 100, + requestRateLimit: 10000, }, signingStore: { connection: { From 897d4d1c04aa42eb1d3878747c0244c0124fff55 Mon Sep 17 00:00:00 2001 From: Phillip Olesen Date: Thu, 29 Jan 2026 10:44:56 +0100 Subject: [PATCH 11/16] fix(wallet-gateway-remote): catching errors during login (#1204) * fix(wallet-gateway-remote): catching errors during login Signed-off-by: phillip olesen * prettier Signed-off-by: phillip olesen --------- Signed-off-by: phillip olesen --- core/rpc-transport/src/index.ts | 8 ++++++-- core/wallet-ui-components/src/handle-errors.ts | 3 +++ examples/ping/package.json | 1 + examples/ping/src/hooks/useConnect.ts | 2 ++ wallet-gateway/remote/src/web/frontend/login/login.ts | 9 +++++++-- yarn.lock | 1 + 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/core/rpc-transport/src/index.ts b/core/rpc-transport/src/index.ts index 5bfba1186..e1918123e 100644 --- a/core/rpc-transport/src/index.ts +++ b/core/rpc-transport/src/index.ts @@ -92,8 +92,12 @@ export class HttpTransport implements RpcTransport { const body = await response.text() // if the response uses the RPC error format, throw it as is - if (ErrorResponse.safeParse(JSON.parse(body)).success) { - throw JSON.parse(body) + try { + if (ErrorResponse.safeParse(JSON.parse(body)).success) { + throw JSON.parse(body) + } + } catch { + // ignore JSON parse errors } throw { diff --git a/core/wallet-ui-components/src/handle-errors.ts b/core/wallet-ui-components/src/handle-errors.ts index 6a98fc5ea..b9f024c38 100644 --- a/core/wallet-ui-components/src/handle-errors.ts +++ b/core/wallet-ui-components/src/handle-errors.ts @@ -49,6 +49,9 @@ export function handleErrorToast(e: unknown, fallback?: FallbackType) { case 413: toast.title = 'Payload Too Large' break + case 429: + toast.title = 'Too Many Requests' + break default: toast.title = fallback?.title || 'Unexpected Error' break diff --git a/examples/ping/package.json b/examples/ping/package.json index 331e5a706..6abc0d3e0 100644 --- a/examples/ping/package.json +++ b/examples/ping/package.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@canton-network/core-wallet-test-utils": "workspace:^", + "@canton-network/core-wallet-ui-components": "workspace:^", "@eslint/js": "^9.39.2", "@playwright/test": "^1.57.0", "@types/node": "^25.0.10", diff --git a/examples/ping/src/hooks/useConnect.ts b/examples/ping/src/hooks/useConnect.ts index 657fcf94b..eac3b7851 100644 --- a/examples/ping/src/hooks/useConnect.ts +++ b/examples/ping/src/hooks/useConnect.ts @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react' import * as sdk from '@canton-network/dapp-sdk' +import { handleErrorToast } from '@canton-network/core-wallet-ui-components' /** * React hook that manages the connection to the wallet gateway. @@ -24,6 +25,7 @@ export function useConnect(): { }) .catch((err) => { console.error('Error connecting to wallet:', err) + handleErrorToast(err) throw err }) } diff --git a/wallet-gateway/remote/src/web/frontend/login/login.ts b/wallet-gateway/remote/src/web/frontend/login/login.ts index c744d9912..048fed03f 100644 --- a/wallet-gateway/remote/src/web/frontend/login/login.ts +++ b/wallet-gateway/remote/src/web/frontend/login/login.ts @@ -14,6 +14,7 @@ import { ClientCredentials, } from '@canton-network/core-wallet-auth' import { redirectToIntendedOrDefault, addUserSession } from '../index' +import { handleErrorToast } from '@canton-network/core-wallet-ui-components' @customElement('user-ui-login') export class LoginUI extends LitElement { @@ -220,8 +221,12 @@ export class LoginUI extends LitElement { async connectedCallback() { super.connectedCallback() - this.networks = await this.loadNetworks() - this.idps = await this.loadIdps() + try { + this.networks = await this.loadNetworks() + this.idps = await this.loadIdps() + } catch (e) { + handleErrorToast(e) + } } private async handleConnectToIDP() { diff --git a/yarn.lock b/yarn.lock index 99852dd46..7a93ec674 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1936,6 +1936,7 @@ __metadata: resolution: "@canton-network/example-ping@workspace:examples/ping" dependencies: "@canton-network/core-wallet-test-utils": "workspace:^" + "@canton-network/core-wallet-ui-components": "workspace:^" "@canton-network/dapp-sdk": "workspace:^" "@eslint/js": "npm:^9.39.2" "@playwright/test": "npm:^1.57.0" From 9643d2ee14227504fbc4fa17ecf0420fa7c7ac91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:07:07 +0000 Subject: [PATCH 12/16] chore(deps): bump tar from 7.5.6 to 7.5.7 (#1206) Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.6 to 7.5.7. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.5.6...v7.5.7) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7a93ec674..e30002cf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17893,15 +17893,15 @@ __metadata: linkType: hard "tar@npm:^7.4.0, tar@npm:^7.5.2": - version: 7.5.6 - resolution: "tar@npm:7.5.6" + version: 7.5.7 + resolution: "tar@npm:7.5.7" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10c0/08af3807035957650ad5f2a300c49ca4fe0566ac0ea5a23741a5b5103c6da42891a9eeaed39bc1fbcf21c5cac4dc846828a004727fb08b9d946322d3144d1fd2 + checksum: 10c0/51f261afc437e1112c3e7919478d6176ea83f7f7727864d8c2cce10f0b03a631d1911644a567348c3063c45abdae39718ba97abb073d22aa3538b9a53ae1e31c languageName: node linkType: hard From 27d2e9c38c07c9df6562003398771757576bc2bb Mon Sep 17 00:00:00 2001 From: Phillip Olesen Date: Thu, 29 Jan 2026 15:29:52 +0100 Subject: [PATCH 13/16] fix(wallet-gateway-remote,dapp-sdk): proper event handling (#1205) * fix(wallet-gateway-remote,sdk-dapp-sdk): proper event handling Signed-off-by: phillip olesen * huh Signed-off-by: phillip olesen * .. Signed-off-by: phillip olesen * package upgrading Signed-off-by: phillip olesen * feedback and yarn install Signed-off-by: phillip olesen * fix playwright Signed-off-by: phillip olesen * prettier Signed-off-by: phillip olesen --------- Signed-off-by: phillip olesen --- .../templates/client/typescript/_package.json | 8 +- .../splice-provider/src/SpliceProviderHttp.ts | 6 +- core/types/src/index.ts | 1 + .../package.json | 8 +- core/wallet-dapp-rpc-client/package.json | 8 +- examples/ping/package.json | 1 + examples/ping/src/App.tsx | 6 ++ examples/ping/src/components/PostEvents.tsx | 79 ++++++++++++++++ .../ping/src/components/WindowMessages.tsx | 93 +++++++++++++++++++ examples/ping/src/hooks/useAllEvents.ts | 67 +++++++++++++ examples/ping/src/hooks/useWindowMessages.ts | 44 +++++++++ examples/ping/src/utils.ts | 2 +- examples/ping/tests/example.spec.ts | 6 +- sdk/dapp-sdk/src/listener.ts | 7 ++ sdk/dapp-sdk/src/provider.ts | 8 +- sdk/dapp-sdk/src/util.ts | 2 +- .../remote/src/dapp-api/controller.ts | 9 +- wallet-gateway/remote/src/dapp-api/server.ts | 5 - .../remote/src/user-api/controller.ts | 4 +- .../remote/src/web/frontend/index.ts | 4 + yarn.lock | 23 ++--- 21 files changed, 348 insertions(+), 43 deletions(-) create mode 100644 examples/ping/src/components/PostEvents.tsx create mode 100644 examples/ping/src/components/WindowMessages.tsx create mode 100644 examples/ping/src/hooks/useAllEvents.ts create mode 100644 examples/ping/src/hooks/useWindowMessages.ts diff --git a/core/rpc-generator/templates/client/typescript/_package.json b/core/rpc-generator/templates/client/typescript/_package.json index 25fef5420..e92ab6dc8 100644 --- a/core/rpc-generator/templates/client/typescript/_package.json +++ b/core/rpc-generator/templates/client/typescript/_package.json @@ -29,18 +29,18 @@ "dependencies": { "@canton-network/core-rpc-transport": "workspace:^", "@canton-network/core-types": "workspace:^", - "lodash": "^4.17.21" + "lodash": "^4.17.23" }, "devDependencies": { "@types/isomorphic-fetch": "^0.0.39", "@types/jest": "^30.0.0", "@types/json-schema": "7.0.15", - "@types/lodash": "^4.17.21", + "@types/lodash": "^4.17.23", "@types/ws": "^8.18.1", "globals": "^16.5.0", - "prettier": "^3.7.1", + "prettier": "^3.8.1", "tsup": "^8.5.1", - "typedoc": "^0.28.14", + "typedoc": "^0.28.16", "typescript": "^5.9.3" }, "repository": "github:hyperledger-labs/splice-wallet-kernel" diff --git a/core/splice-provider/src/SpliceProviderHttp.ts b/core/splice-provider/src/SpliceProviderHttp.ts index bf25b9c83..6692f80d8 100644 --- a/core/splice-provider/src/SpliceProviderHttp.ts +++ b/core/splice-provider/src/SpliceProviderHttp.ts @@ -85,11 +85,11 @@ export class SpliceProviderHttp extends SpliceProviderBase { this.openSocket(this.url, event.data.token) // We requery the status explicitly here, as it's not guaranteed that the socket will be open & authenticated - // before the `connected` event is fired from the `addSession` RPC call. The dappApi.StatusResult and - // dappApi.ConnectedEvent are mapped manually to avoid dependency. + // before the `statusChanged` event is fired from the `addSession` RPC call. The dappApi.StatusResult and + // dappApi.StatusEvent are mapped manually to avoid dependency. this.request({ method: 'status' }) .then((status) => { - this.emit('connected', status) + this.emit('statusChanged', status) }) .catch((err) => { console.error( diff --git a/core/types/src/index.ts b/core/types/src/index.ts index 6e879c232..d90802205 100644 --- a/core/types/src/index.ts +++ b/core/types/src/index.ts @@ -77,6 +77,7 @@ export enum WalletEvent { SPLICE_WALLET_EXT_OPEN = 'SPLICE_WALLET_EXT_OPEN', // A request from the dApp to the browser extension to open the wallet UI // Auth events SPLICE_WALLET_IDP_AUTH_SUCCESS = 'SPLICE_WALLET_IDP_AUTH_SUCCESS', + SPLICE_WALLET_LOGOUT = 'SPLICE_WALLET_LOGOUT', } export type SpliceMessageEvent = MessageEvent diff --git a/core/wallet-dapp-remote-rpc-client/package.json b/core/wallet-dapp-remote-rpc-client/package.json index 0d25afbd6..a87a29b47 100644 --- a/core/wallet-dapp-remote-rpc-client/package.json +++ b/core/wallet-dapp-remote-rpc-client/package.json @@ -31,18 +31,18 @@ "dependencies": { "@canton-network/core-rpc-transport": "workspace:^", "@canton-network/core-types": "workspace:^", - "lodash": "^4.17.21" + "lodash": "^4.17.23" }, "devDependencies": { "@types/isomorphic-fetch": "^0.0.39", "@types/jest": "^30.0.0", "@types/json-schema": "7.0.15", - "@types/lodash": "^4.17.21", + "@types/lodash": "^4.17.23", "@types/ws": "^8.18.1", "globals": "^16.5.0", - "prettier": "^3.7.1", + "prettier": "^3.8.1", "tsup": "^8.5.1", - "typedoc": "^0.28.14", + "typedoc": "^0.28.16", "typescript": "^5.9.3" }, "repository": { diff --git a/core/wallet-dapp-rpc-client/package.json b/core/wallet-dapp-rpc-client/package.json index c02defe8f..c1d048465 100644 --- a/core/wallet-dapp-rpc-client/package.json +++ b/core/wallet-dapp-rpc-client/package.json @@ -31,18 +31,18 @@ "dependencies": { "@canton-network/core-rpc-transport": "workspace:^", "@canton-network/core-types": "workspace:^", - "lodash": "^4.17.21" + "lodash": "^4.17.23" }, "devDependencies": { "@types/isomorphic-fetch": "^0.0.39", "@types/jest": "^30.0.0", "@types/json-schema": "7.0.15", - "@types/lodash": "^4.17.21", + "@types/lodash": "^4.17.23", "@types/ws": "^8.18.1", "globals": "^16.5.0", - "prettier": "^3.7.1", + "prettier": "^3.8.1", "tsup": "^8.5.1", - "typedoc": "^0.28.14", + "typedoc": "^0.28.16", "typescript": "^5.9.3" }, "repository": { diff --git a/examples/ping/package.json b/examples/ping/package.json index 6abc0d3e0..bda422ccd 100644 --- a/examples/ping/package.json +++ b/examples/ping/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@canton-network/core-types": "workspace:^", "@canton-network/dapp-sdk": "workspace:^", "react": "^19.2.3", "react-dom": "^19.2.3" diff --git a/examples/ping/src/App.tsx b/examples/ping/src/App.tsx index d9f93bab0..5d218596f 100644 --- a/examples/ping/src/App.tsx +++ b/examples/ping/src/App.tsx @@ -8,6 +8,8 @@ import { ErrorContext } from './ErrorContext' import { LedgerQuery } from './components/LedgerQuery' import { LedgerSubmission } from './components/LedgerSubmission' import { Accounts } from './components/Accounts' +import { PostEvents } from './components/PostEvents' +import { WindowMessages } from './components/WindowMessages' function App() { const { errorMsg, setErrorMsg } = useContext(ErrorContext) @@ -98,6 +100,10 @@ function App() { + + + + { + switch (type) { + case 'TxChanged': + return '#0ff' + case 'StatusChanged': + return '#f0f' + case 'AccountsChanged': + return '#ff0' + default: + return '#fff' + } + } + + return ( + providerAvailable && ( +
+

Post Events

+ {events.length === 0 ? ( + No post events received yet. + ) : ( +
+

Total events received: {events.length}

+
+
+                                {events.map((item, index) => (
+                                    
+
+ Event #{events.length - index} -{' '} + {item.type} ( + {item.timestamp.toLocaleTimeString()} + ) +
+ {prettyjson(item.event)} +
+ ))} +
+
+
+ )} +
+ ) + ) +} diff --git a/examples/ping/src/components/WindowMessages.tsx b/examples/ping/src/components/WindowMessages.tsx new file mode 100644 index 000000000..a19589d27 --- /dev/null +++ b/examples/ping/src/components/WindowMessages.tsx @@ -0,0 +1,93 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useWindowMessages } from '../hooks/useWindowMessages' +import { prettyjson } from '../utils' +import { WalletEvent } from '@canton-network/core-types' + +export function WindowMessages() { + const messages = useWindowMessages() + + const getMessageColor = (type: string) => { + switch (type) { + case WalletEvent.SPLICE_WALLET_LOGOUT: + return '#f80' + case WalletEvent.SPLICE_WALLET_IDP_AUTH_SUCCESS: + return '#0f0' + case WalletEvent.SPLICE_WALLET_REQUEST: + return '#0ff' + case WalletEvent.SPLICE_WALLET_RESPONSE: + return '#f0f' + case WalletEvent.SPLICE_WALLET_EXT_READY: + return '#ff0' + case WalletEvent.SPLICE_WALLET_EXT_ACK: + return '#0f8' + case WalletEvent.SPLICE_WALLET_EXT_OPEN: + return '#f08' + default: + return '#888' + } + } + + return ( +
+

Window Messages (postMessage)

+ {messages.length === 0 ? ( + No window messages received yet. + ) : ( +
+

Total messages received: {messages.length}

+
+
+                            {messages.map((item, index) => (
+                                
+
+ Message #{messages.length - index} -{' '} + {item.type} ( + {item.timestamp.toLocaleTimeString()}) +
+
+ Origin: {item.origin} +
+ {prettyjson(item.data)} +
+ ))} +
+
+
+ )} +
+ ) +} diff --git a/examples/ping/src/hooks/useAllEvents.ts b/examples/ping/src/hooks/useAllEvents.ts new file mode 100644 index 000000000..b680b212d --- /dev/null +++ b/examples/ping/src/hooks/useAllEvents.ts @@ -0,0 +1,67 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect, useState } from 'react' +import * as sdk from '@canton-network/dapp-sdk' + +export type AllEvents = + | { type: 'TxChanged'; event: sdk.dappAPI.TxChangedEvent; timestamp: Date } + | { + type: 'StatusChanged' + event: sdk.dappAPI.StatusEvent + timestamp: Date + } + | { + type: 'AccountsChanged' + event: sdk.dappAPI.AccountsChangedEvent + timestamp: Date + } + +export function useAllEvents() { + const [events, setEvents] = useState([]) + + useEffect(() => { + //we use window.canton here since we want to capture the initial login event as well + if (window.canton) { + const txListener = (event: sdk.dappAPI.TxChangedEvent) => { + console.debug('[use-all-events] Adding tx changed listener') + setEvents((prev) => [ + { type: 'TxChanged', event, timestamp: new Date() }, + ...prev, + ]) + } + + const statusListener = (event: sdk.dappAPI.StatusEvent) => { + console.debug('[use-all-events] Adding status changed listener') + setEvents((prev) => [ + { type: 'StatusChanged', event, timestamp: new Date() }, + ...prev, + ]) + } + + const accountsListener = ( + event: sdk.dappAPI.AccountsChangedEvent + ) => { + console.debug( + '[use-all-events] Adding accounts changed listener' + ) + setEvents((prev) => [ + { type: 'AccountsChanged', event, timestamp: new Date() }, + ...prev, + ]) + } + + sdk.onTxChanged(txListener) + sdk.onStatusChanged(statusListener) + sdk.onAccountsChanged(accountsListener) + + return () => { + sdk.removeOnTxChanged(txListener) + sdk.removeOnStatusChanged(statusListener) + sdk.removeOnAccountsChanged(accountsListener) + } + } + }, []) + + return events +} diff --git a/examples/ping/src/hooks/useWindowMessages.ts b/examples/ping/src/hooks/useWindowMessages.ts new file mode 100644 index 000000000..baa3a9026 --- /dev/null +++ b/examples/ping/src/hooks/useWindowMessages.ts @@ -0,0 +1,44 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect, useState } from 'react' +import { WalletEvent } from '@canton-network/core-types' + +export type WindowMessageEvent = { + type: WalletEvent | string + data: object + origin: string + timestamp: Date +} + +export function useWindowMessages() { + const [messages, setMessages] = useState([]) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const messageType = + event.data.type && + Object.values(WalletEvent).includes(event.data.type) + ? event.data.type + : event.data.type || 'unknown' + + setMessages((prev) => [ + { + type: messageType, + data: event.data, + origin: event.origin, + timestamp: new Date(), + }, + ...prev, + ]) + } + + window.addEventListener('message', handleMessage) + + return () => { + window.removeEventListener('message', handleMessage) + } + }, []) + + return messages +} diff --git a/examples/ping/src/utils.ts b/examples/ping/src/utils.ts index a4bbfca89..53e410e09 100644 --- a/examples/ping/src/utils.ts +++ b/examples/ping/src/utils.ts @@ -2,5 +2,5 @@ // SPDX-License-Identifier: Apache-2.0 export function prettyjson(obj: object): string { - return JSON.stringify(obj, Object.keys(obj).sort(), 2) + return JSON.stringify(obj, null, 2) } diff --git a/examples/ping/tests/example.spec.ts b/examples/ping/tests/example.spec.ts index ce5a041d6..130d00407 100644 --- a/examples/ping/tests/example.spec.ts +++ b/examples/ping/tests/example.spec.ts @@ -57,10 +57,12 @@ test('dApp: execute externally signed tx', async ({ page: dappPage }) => { ).toBeEnabled() // Create a Ping contract through the dapp with the new party - const { commandId } = await wg.approveTransaction(() => + await wg.approveTransaction(() => dappPage.getByRole('button', { name: 'create Ping contract' }).click() ) // Wait for command to have fully executed - await expect(dappPage.getByText(commandId)).toHaveCount(3) + //TODO: we use 2 because we have one in the transaction list and one in the event list + //TODO: fix this so we check each list properly once + await expect(dappPage.getByText('executed')).toHaveCount(2) }) diff --git a/sdk/dapp-sdk/src/listener.ts b/sdk/dapp-sdk/src/listener.ts index 21d95b4fe..60c9857cc 100644 --- a/sdk/dapp-sdk/src/listener.ts +++ b/sdk/dapp-sdk/src/listener.ts @@ -3,6 +3,7 @@ import { onStatusChanged } from './provider/events.js' import { clearAllLocalState } from './util.js' +import { WalletEvent } from '@canton-network/core-types' if (window.canton) { // Clean up session on disconnect @@ -11,4 +12,10 @@ if (window.canton) { clearAllLocalState({ closePopup: true }) } }) + // Clean up session on logout message + window.addEventListener('message', (event: MessageEvent) => { + if (event.data?.type === WalletEvent.SPLICE_WALLET_LOGOUT) { + clearAllLocalState({ closePopup: true }) + } + }) } diff --git a/sdk/dapp-sdk/src/provider.ts b/sdk/dapp-sdk/src/provider.ts index d47125d7d..358aed7e8 100644 --- a/sdk/dapp-sdk/src/provider.ts +++ b/sdk/dapp-sdk/src/provider.ts @@ -133,10 +133,12 @@ export const dappController = (provider: SpliceProvider) => 5 * 60 * 1000 ) provider.on( - 'connected', + 'statusChanged', (event) => { - clearTimeout(timeout) - resolve(event) + if (event.isConnected) { + clearTimeout(timeout) + resolve(event) + } } ) } diff --git a/sdk/dapp-sdk/src/util.ts b/sdk/dapp-sdk/src/util.ts index 785739c82..32ab4794a 100644 --- a/sdk/dapp-sdk/src/util.ts +++ b/sdk/dapp-sdk/src/util.ts @@ -7,7 +7,7 @@ import { removeKernelDiscovery, removeKernelSession } from './storage' export const clearAllLocalState = ({ closePopup, }: { closePopup?: boolean } = {}) => { - window.canton = undefined // Clear global canton provider + //window.canton = undefined // Clear global canton provider removeKernelSession() removeKernelDiscovery() diff --git a/wallet-gateway/remote/src/dapp-api/controller.ts b/wallet-gateway/remote/src/dapp-api/controller.ts index 34056d2ae..b42b338f7 100644 --- a/wallet-gateway/remote/src/dapp-api/controller.ts +++ b/wallet-gateway/remote/src/dapp-api/controller.ts @@ -57,7 +57,8 @@ export const dappController = ( accessToken: context.accessToken, }) const status = await networkStatus(ledgerClient) - return { + const notifier = notificationService.getNotifier(context.userId) + const StatusEvent: StatusEvent = { kernel: kernelInfo, isConnected: true, isNetworkConnected: status.isConnected, @@ -74,7 +75,9 @@ export const dappController = ( userId: context.userId, }, userUrl: `${userUrl}/login/`, - } as StatusEventAsync + } + notifier.emit('statusChanged', StatusEvent) + return StatusEvent as StatusEventAsync }, disconnect: async () => { if (!context) { @@ -86,7 +89,7 @@ export const dappController = ( kernel: kernelInfo, isConnected: false, isNetworkConnected: false, - networkReason: 'Unauthenticated', + networkReason: 'disconnect', userUrl: `${userUrl}/login/`, } as StatusEvent) } diff --git a/wallet-gateway/remote/src/dapp-api/server.ts b/wallet-gateway/remote/src/dapp-api/server.ts index 1ba03b25d..f13e3cbb5 100644 --- a/wallet-gateway/remote/src/dapp-api/server.ts +++ b/wallet-gateway/remote/src/dapp-api/server.ts @@ -87,15 +87,11 @@ export const dapp = ( ) io.to(sessionId).emit('statusChanged', ...event) } - const onConnected = (...event: unknown[]) => { - io.to(sessionId).emit('connected', ...event) - } const onTxChanged = (...event: unknown[]) => { io.to(sessionId).emit('txChanged', ...event) } notifier.on('accountsChanged', onAccountsChanged) - notifier.on('connected', onConnected) notifier.on('statusChanged', onStatusChanged) notifier.on('txChanged', onTxChanged) @@ -103,7 +99,6 @@ export const dapp = ( logger.debug('Socket.io client disconnected') notifier.removeListener('accountsChanged', onAccountsChanged) - notifier.removeListener('connected', onConnected) notifier.removeListener('statusChanged', onStatusChanged) notifier.removeListener('txChanged', onTxChanged) }) diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 98cdf6550..5baef5e92 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -774,7 +774,7 @@ export const userController = ( accessToken, }) const status = await networkStatus(ledgerClient) - notifier.emit('connected', { + notifier.emit('statusChanged', { kernel: { ...kernelInfo, userUrl: `${userUrl}/login/`, @@ -846,7 +846,7 @@ export const userController = ( kernel: kernelInfo, isConnected: false, isNetworkConnected: false, - networkReason: 'Unauthenticated', + networkReason: 'removed session', userUrl: `${userUrl}/login/`, } as StatusEvent) diff --git a/wallet-gateway/remote/src/web/frontend/index.ts b/wallet-gateway/remote/src/web/frontend/index.ts index 60c360568..a43fb921d 100644 --- a/wallet-gateway/remote/src/web/frontend/index.ts +++ b/wallet-gateway/remote/src/web/frontend/index.ts @@ -49,6 +49,10 @@ export class UserApp extends LitElement { stateManager.clearAuthState() if (window.opener && !window.opener.closed) { + window.opener.postMessage( + { type: WalletEvent.SPLICE_WALLET_LOGOUT }, + '*' + ) // close the gateway UI automatically if we are within a popup window.close() } else { diff --git a/yarn.lock b/yarn.lock index e30002cf0..21caecfb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1756,13 +1756,13 @@ __metadata: "@types/isomorphic-fetch": "npm:^0.0.39" "@types/jest": "npm:^30.0.0" "@types/json-schema": "npm:7.0.15" - "@types/lodash": "npm:^4.17.21" + "@types/lodash": "npm:^4.17.23" "@types/ws": "npm:^8.18.1" globals: "npm:^16.5.0" - lodash: "npm:^4.17.21" - prettier: "npm:^3.7.1" + lodash: "npm:^4.17.23" + prettier: "npm:^3.8.1" tsup: "npm:^8.5.1" - typedoc: "npm:^0.28.14" + typedoc: "npm:^0.28.16" typescript: "npm:^5.9.3" languageName: unknown linkType: soft @@ -1776,13 +1776,13 @@ __metadata: "@types/isomorphic-fetch": "npm:^0.0.39" "@types/jest": "npm:^30.0.0" "@types/json-schema": "npm:7.0.15" - "@types/lodash": "npm:^4.17.21" + "@types/lodash": "npm:^4.17.23" "@types/ws": "npm:^8.18.1" globals: "npm:^16.5.0" - lodash: "npm:^4.17.21" - prettier: "npm:^3.7.1" + lodash: "npm:^4.17.23" + prettier: "npm:^3.8.1" tsup: "npm:^8.5.1" - typedoc: "npm:^0.28.14" + typedoc: "npm:^0.28.16" typescript: "npm:^5.9.3" languageName: unknown linkType: soft @@ -1935,6 +1935,7 @@ __metadata: version: 0.0.0-use.local resolution: "@canton-network/example-ping@workspace:examples/ping" dependencies: + "@canton-network/core-types": "workspace:^" "@canton-network/core-wallet-test-utils": "workspace:^" "@canton-network/core-wallet-ui-components": "workspace:^" "@canton-network/dapp-sdk": "workspace:^" @@ -7414,7 +7415,7 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:^4.17.20, @types/lodash@npm:^4.17.21, @types/lodash@npm:^4.17.23, @types/lodash@npm:^4.5": +"@types/lodash@npm:^4.17.20, @types/lodash@npm:^4.17.23, @types/lodash@npm:^4.5": version: 4.17.23 resolution: "@types/lodash@npm:4.17.23" checksum: 10c0/9d9cbfb684e064a2b78aab9e220d398c9c2a7d36bc51a07b184ff382fa043a99b3d00c16c7f109b4eb8614118f4869678dbae7d5c6700ed16fb9340e26cc0bf6 @@ -16138,7 +16139,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.5.0, prettier@npm:^3.7.1, prettier@npm:^3.8.1": +"prettier@npm:^3.5.0, prettier@npm:^3.8.1": version: 3.8.1 resolution: "prettier@npm:3.8.1" bin: @@ -18386,7 +18387,7 @@ __metadata: languageName: node linkType: hard -"typedoc@npm:^0.28.14, typedoc@npm:^0.28.16": +"typedoc@npm:^0.28.16": version: 0.28.16 resolution: "typedoc@npm:0.28.16" dependencies: From a816ae395d01b8c21fcc9d4a7e73de5597561053 Mon Sep 17 00:00:00 2001 From: pawelstepien-da Date: Fri, 30 Jan 2026 13:31:59 +0100 Subject: [PATCH 14/16] feat: multi network wallets (#1174) Signed-off-by: Pawel Stepien Signed-off-by: Marc Juchli --- .../src/migrations/003-alter-date-fields.ts | 132 ++++- core/signing-store-sql/src/utils.ts | 15 + core/wallet-store-inmemory/src/Store.test.ts | 384 ++++++++++++++- .../src/StoreInternal.ts | 101 ++-- ...-change-wallet-primary-key-to-composite.ts | 262 ++++++++++ .../007-add-unique-primary-per-network.ts | 66 +++ core/wallet-store-sql/src/store-sql.test.ts | 334 ++++++++++++- core/wallet-store-sql/src/store-sql.ts | 150 +++++- core/wallet-store-sql/src/utils.ts | 15 + core/wallet-store/src/Store.ts | 6 +- core/wallet-test-utils/src/wallet-gateway.ts | 12 +- .../src/components/wallets-sync.ts | 7 + .../remote/src/dapp-api/controller.ts | 3 +- .../src/ledger/wallet-sync-service.test.ts | 459 +++++++++++++++++- .../remote/src/ledger/wallet-sync-service.ts | 72 ++- .../remote/src/user-api/controller.ts | 19 +- .../remote/src/web/frontend/approve/index.ts | 3 +- .../remote/src/web/frontend/wallets/index.ts | 24 +- 18 files changed, 1944 insertions(+), 120 deletions(-) create mode 100644 core/signing-store-sql/src/utils.ts create mode 100644 core/wallet-store-sql/src/migrations/006-change-wallet-primary-key-to-composite.ts create mode 100644 core/wallet-store-sql/src/migrations/007-add-unique-primary-per-network.ts create mode 100644 core/wallet-store-sql/src/utils.ts diff --git a/core/signing-store-sql/src/migrations/003-alter-date-fields.ts b/core/signing-store-sql/src/migrations/003-alter-date-fields.ts index d74eab7aa..90008a967 100644 --- a/core/signing-store-sql/src/migrations/003-alter-date-fields.ts +++ b/core/signing-store-sql/src/migrations/003-alter-date-fields.ts @@ -3,10 +3,91 @@ import { Kysely, sql } from 'kysely' import { DB } from '../schema.js' +import { isPostgres } from '../utils.js' + +async function columnExists( + db: Kysely, + tableName: string, + columnName: string, + isPg: boolean +): Promise { + if (isPg) { + const result = await sql<{ exists: boolean }>` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = ${tableName} + AND column_name = ${columnName} + ) as exists + `.execute(db) + return result.rows[0]?.exists ?? false + } else { + const result = await sql<{ name: string }>` + SELECT name FROM pragma_table_info(${tableName}) WHERE name = ${columnName} + `.execute(db) + return result.rows.length > 0 + } +} + +async function dropConstraintIfExists( + db: Kysely, + tableName: string, + constraintName: string, + isPg: boolean +): Promise { + if (isPg) { + // PostgreSQL: Drop constraint if it exists + const result = await sql<{ constraint_name: string }>` + SELECT constraint_name + FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = ${tableName} + AND constraint_name = ${constraintName} + `.execute(db) + + if (result.rows.length > 0) { + const quotedTable = `"${tableName}"` + const quotedConstraint = `"${constraintName}"` + await sql + .raw( + `ALTER TABLE ${quotedTable} DROP CONSTRAINT IF EXISTS ${quotedConstraint}` + ) + .execute(db) + } + } + // SQLite: Constraints are dropped when table is dropped, so no action needed +} + +async function dropIndexIfExists( + db: Kysely, + indexName: string +): Promise { + await sql.raw(`DROP INDEX IF EXISTS "${indexName}"`).execute(db) +} export async function up(db: Kysely): Promise { + const isPg = await isPostgres(db) console.log('Altering date fields to text (SQLite compatible)') + // Drop temp tables if they exist (from previous failed migrations) + await db.schema.dropTable('signing_transactions_tmp').ifExists().execute() + await db.schema.dropTable('signing_keys_tmp').ifExists().execute() + + // For PostgreSQL, drop constraints from original tables before creating temp tables + // to avoid name conflicts during reset operations + await dropConstraintIfExists( + db, + 'signing_transactions', + 'signing_transactions_user_id_id_unique', + isPg + ) + await dropConstraintIfExists( + db, + 'signing_keys', + 'signing_keys_user_id_id_unique', + isPg + ) + // --- signing_transactions --- // Create temporary table with new schema await db.schema @@ -29,11 +110,12 @@ export async function up(db: Kysely): Promise { // Copy data, converting integer timestamps to text // Check if signed_at column exists - const tableInfo = await sql<{ name: string }>` - SELECT name FROM pragma_table_info('signing_transactions') WHERE name = 'signed_at' - `.execute(db) - - const hasSignedAt = tableInfo.rows.length > 0 + const hasSignedAt = await columnExists( + db, + 'signing_transactions', + 'signed_at', + isPg + ) if (hasSignedAt) { await sql` @@ -79,18 +161,21 @@ export async function up(db: Kysely): Promise { .execute() // Recreate indexes + await dropIndexIfExists(db, 'idx_signing_transactions_user_id') await db.schema .createIndex('idx_signing_transactions_user_id') .on('signing_transactions') .column('user_id') .execute() + await dropIndexIfExists(db, 'idx_signing_transactions_status') await db.schema .createIndex('idx_signing_transactions_status') .on('signing_transactions') .column('status') .execute() + await dropIndexIfExists(db, 'idx_signing_transactions_created_at') await db.schema .createIndex('idx_signing_transactions_created_at') .on('signing_transactions') @@ -140,12 +225,14 @@ export async function up(db: Kysely): Promise { .execute() // Recreate indexes + await dropIndexIfExists(db, 'idx_signing_keys_user_id') await db.schema .createIndex('idx_signing_keys_user_id') .on('signing_keys') .column('user_id') .execute() + await dropIndexIfExists(db, 'idx_signing_keys_public_key') await db.schema .createIndex('idx_signing_keys_public_key') .on('signing_keys') @@ -154,8 +241,27 @@ export async function up(db: Kysely): Promise { } export async function down(db: Kysely): Promise { + const isPg = await isPostgres(db) console.log('Reverting date fields to integer (SQLite compatible)') + // Drop temp tables if they exist + await db.schema.dropTable('signing_transactions_tmp').ifExists().execute() + await db.schema.dropTable('signing_keys_tmp').ifExists().execute() + + // For PostgreSQL, drop constraints from original tables before creating temp tables + await dropConstraintIfExists( + db, + 'signing_transactions', + 'signing_transactions_user_id_id_unique', + isPg + ) + await dropConstraintIfExists( + db, + 'signing_keys', + 'signing_keys_user_id_id_unique', + isPg + ) + // --- signing_transactions --- // Create temporary table with old schema await db.schema @@ -178,11 +284,12 @@ export async function down(db: Kysely): Promise { // Copy data, converting text timestamps to integer // Check if signed_at column exists - const tableInfo = await sql<{ name: string }>` - SELECT name FROM pragma_table_info('signing_transactions') WHERE name = 'signed_at' - `.execute(db) - - const hasSignedAt = tableInfo.rows.length > 0 + const hasSignedAt = await columnExists( + db, + 'signing_transactions', + 'signed_at', + isPg + ) if (hasSignedAt) { await sql` @@ -228,18 +335,21 @@ export async function down(db: Kysely): Promise { .execute() // Recreate indexes + await dropIndexIfExists(db, 'idx_signing_transactions_user_id') await db.schema .createIndex('idx_signing_transactions_user_id') .on('signing_transactions') .column('user_id') .execute() + await dropIndexIfExists(db, 'idx_signing_transactions_status') await db.schema .createIndex('idx_signing_transactions_status') .on('signing_transactions') .column('status') .execute() + await dropIndexIfExists(db, 'idx_signing_transactions_created_at') await db.schema .createIndex('idx_signing_transactions_created_at') .on('signing_transactions') @@ -289,12 +399,14 @@ export async function down(db: Kysely): Promise { .execute() // Recreate indexes + await dropIndexIfExists(db, 'idx_signing_keys_user_id') await db.schema .createIndex('idx_signing_keys_user_id') .on('signing_keys') .column('user_id') .execute() + await dropIndexIfExists(db, 'idx_signing_keys_public_key') await db.schema .createIndex('idx_signing_keys_public_key') .on('signing_keys') diff --git a/core/signing-store-sql/src/utils.ts b/core/signing-store-sql/src/utils.ts new file mode 100644 index 000000000..a38aa2584 --- /dev/null +++ b/core/signing-store-sql/src/utils.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Kysely, sql } from 'kysely' +import { DB } from './schema' + +export async function isPostgres(db: Kysely): Promise { + try { + // Try to query PostgreSQL system catalog + await sql`SELECT 1 FROM pg_database LIMIT 1`.execute(db) + return true + } catch { + return false + } +} diff --git a/core/wallet-store-inmemory/src/Store.test.ts b/core/wallet-store-inmemory/src/Store.test.ts index eedd41e00..e97dfeb16 100644 --- a/core/wallet-store-inmemory/src/Store.test.ts +++ b/core/wallet-store-inmemory/src/Store.test.ts @@ -24,11 +24,6 @@ const authContextMock: AuthContext = { accessToken: 'test-access-token', } -const storeConfig: StoreInternalConfig = { - idps: [], - networks: [], -} - type StoreCtor = new ( config: StoreInternalConfig, logger: Logger, @@ -44,10 +39,44 @@ implementations.forEach(([name, StoreImpl]) => { let store: Store beforeEach(() => { + // Create a fresh config for each test to avoid shared state between tests + const storeConfig: StoreInternalConfig = { + idps: [], + networks: [], + } store = new StoreImpl(storeConfig, pino(sink()), authContextMock) }) test('should add and retrieve wallets', async () => { + const idp: Idp = { + id: 'idp1', + type: 'oauth' as const, + issuer: 'http://auth', + configUrl: 'http://auth/.well-known/openid-configuration', + } + const network: Network = { + id: 'network1', + name: 'testnet', + synchronizerId: 'sync1::fingerprint', + description: 'Test Network', + identityProviderId: 'idp1', + ledgerApi: { baseUrl: 'http://api' }, + auth: { + method: 'authorization_code', + clientId: 'cid', + scope: 'scope', + audience: 'aud', + }, + } + const session: Session = { + id: 'session1', + network: 'network1', + accessToken: 'token', + } + await store.addIdp(idp) + await store.addNetwork(network) + await store.setSession(session) + const wallet: Wallet = { primary: false, partyId: 'party1', @@ -64,6 +93,50 @@ implementations.forEach(([name, StoreImpl]) => { }) test('should filter wallets', async () => { + const idp: Idp = { + id: 'idp1', + type: 'oauth' as const, + issuer: 'http://auth', + configUrl: 'http://auth/.well-known/openid-configuration', + } + const network1: Network = { + id: 'network1', + name: 'testnet-1', + synchronizerId: 'sync1::fingerprint', + description: 'Test Network 1', + identityProviderId: 'idp1', + ledgerApi: { baseUrl: 'http://api' }, + auth: { + method: 'authorization_code', + clientId: 'cid', + scope: 'scope', + audience: 'aud', + }, + } + const network2: Network = { + id: 'network2', + name: 'testnet-2', + synchronizerId: 'sync2::fingerprint', + description: 'Test Network 2', + identityProviderId: 'idp1', + ledgerApi: { baseUrl: 'http://api' }, + auth: { + method: 'authorization_code', + clientId: 'cid', + scope: 'scope', + audience: 'aud', + }, + } + const session: Session = { + id: 'session1', + network: 'network1', + accessToken: 'token', + } + await store.addIdp(idp) + await store.addNetwork(network1) + await store.addNetwork(network2) + await store.setSession(session) + const wallet1: Wallet = { primary: false, partyId: 'party1', @@ -97,25 +170,55 @@ implementations.forEach(([name, StoreImpl]) => { await store.addWallet(wallet1) await store.addWallet(wallet2) await store.addWallet(wallet3) + const getAllWallets = await store.getWallets() - const getWalletsByNetworkId = await store.getWallets({ + const getAllWalletsAcrossNetworks = await store.getAllWallets({ + networkIds: ['network1', 'network2'], + }) + const getWalletsByNetworkId = await store.getAllWallets({ networkIds: ['network1'], }) - const getWalletsBySigningProviderId = await store.getWallets({ + const getWalletsBySigningProviderId = await store.getAllWallets({ signingProviderIds: ['internal2'], }) const getWalletsByNetworkIdAndSigningProviderId = - await store.getWallets({ + await store.getAllWallets({ networkIds: ['network1'], signingProviderIds: ['internal2'], }) - expect(getAllWallets).toHaveLength(3) + + expect(getAllWallets).toHaveLength(2) + expect(getAllWalletsAcrossNetworks).toHaveLength(3) expect(getWalletsByNetworkId).toHaveLength(2) expect(getWalletsBySigningProviderId).toHaveLength(2) expect(getWalletsByNetworkIdAndSigningProviderId).toHaveLength(1) }) test('should set and get primary wallet', async () => { + const idp: Idp = { + id: 'idp1', + type: 'oauth' as const, + issuer: 'http://auth', + configUrl: 'http://auth/.well-known/openid-configuration', + } + const ledgerApi: LedgerApi = { + baseUrl: 'http://api', + } + const auth: AuthorizationCodeAuth = { + method: 'authorization_code', + clientId: 'cid', + scope: 'scope', + audience: 'aud', + } + const network: Network = { + id: 'network1', + name: 'testnet', + synchronizerId: 'sync1::fingerprint', + description: 'Test Network', + identityProviderId: 'idp1', + ledgerApi, + auth, + } const wallet1: Wallet = { primary: false, partyId: 'party1', @@ -136,6 +239,14 @@ implementations.forEach(([name, StoreImpl]) => { namespace: 'namespace', networkId: 'network1', } + const session: Session = { + id: 'sess-123', + network: 'network1', + accessToken: 'token', + } + await store.addIdp(idp) + await store.addNetwork(network) + await store.setSession(session) await store.addWallet(wallet1) await store.addWallet(wallet2) await store.setPrimaryWallet('party2') @@ -145,7 +256,11 @@ implementations.forEach(([name, StoreImpl]) => { }) test('should set and get session', async () => { - const session: Session = { network: 'net', accessToken: 'token' } + const session: Session = { + id: 'sess-123', + network: 'net', + accessToken: 'token', + } await store.setSession(session) const result = await store.getSession() expect(result).toEqual(session) @@ -180,8 +295,7 @@ implementations.forEach(([name, StoreImpl]) => { auth, } await store.addIdp(idp) - await store.updateIdp(idp) - await store.updateNetwork(network) + await store.addNetwork(network) const listed = await store.listNetworks() expect(listed).toHaveLength(1) expect(listed[0].name).toBe('testnet') @@ -201,5 +315,251 @@ implementations.forEach(([name, StoreImpl]) => { test('should throw when getting current network if none set', async () => { await expect(store.getCurrentNetwork()).rejects.toThrow() }) + + test('should allow same party ID across different networks', async () => { + const idp: Idp = { + id: 'idp1', + type: 'oauth' as const, + issuer: 'http://auth', + configUrl: 'http://auth/.well-known/openid-configuration', + } + const network1: Network = { + id: 'network1', + name: 'testnet-1', + synchronizerId: 'sync1::fingerprint', + description: 'Test Network 1', + identityProviderId: 'idp1', + ledgerApi: { baseUrl: 'http://api' }, + auth: { + method: 'authorization_code', + clientId: 'cid', + scope: 'scope', + audience: 'aud', + }, + } + const network2: Network = { + id: 'network2', + name: 'testnet-2', + synchronizerId: 'sync2::fingerprint', + description: 'Test Network 2', + identityProviderId: 'idp1', + ledgerApi: { baseUrl: 'http://api' }, + auth: { + method: 'authorization_code', + clientId: 'cid', + scope: 'scope', + audience: 'aud', + }, + } + const session: Session = { + id: 'session1', + network: 'network1', + accessToken: 'token', + } + await store.addIdp(idp) + await store.addNetwork(network1) + await store.addNetwork(network2) + await store.setSession(session) + + const wallet1: Wallet = { + primary: false, + partyId: 'party1::namespace', + status: 'allocated', + hint: 'party1', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network1', + } + const wallet2: Wallet = { + primary: false, + partyId: 'party1::namespace', // Same party ID + status: 'allocated', + hint: 'party1', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network2', // Different network + } + await store.addWallet(wallet1) + await store.addWallet(wallet2) // Should not throw + + const wallets = await store.getWallets() + expect(wallets).toHaveLength(1) + const allWallets = await store.getAllWallets({ + networkIds: ['network1', 'network2'], + }) + expect(allWallets).toHaveLength(2) + expect( + allWallets.filter((w) => w.partyId === 'party1::namespace') + ).toHaveLength(2) + }) + + test('should have separate primary wallets per network', async () => { + const idp: Idp = { + id: 'idp1', + type: 'oauth' as const, + issuer: 'http://auth', + configUrl: 'http://auth/.well-known/openid-configuration', + } + const ledgerApi: LedgerApi = { + baseUrl: 'http://api', + } + const auth: AuthorizationCodeAuth = { + method: 'authorization_code', + clientId: 'cid', + scope: 'scope', + audience: 'aud', + } + const network1: Network = { + id: 'network1', + name: 'testnet-1', + synchronizerId: 'sync1::fingerprint', + description: 'Test Network 1', + identityProviderId: 'idp1', + ledgerApi, + auth, + } + const network2: Network = { + id: 'network2', + name: 'testnet-2', + synchronizerId: 'sync2::fingerprint', + description: 'Test Network 2', + identityProviderId: 'idp1', + ledgerApi, + auth, + } + await store.addIdp(idp) + const wallet1: Wallet = { + primary: false, + partyId: 'party1', + status: 'allocated', + hint: 'hint1', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network1', + } + const wallet2: Wallet = { + primary: false, + partyId: 'party2', + status: 'allocated', + hint: 'hint2', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network1', + } + const wallet3: Wallet = { + primary: false, + partyId: 'party3', + status: 'allocated', + hint: 'hint3', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network2', + } + const wallet4: Wallet = { + primary: false, + partyId: 'party4', + status: 'allocated', + hint: 'hint4', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network2', + } + + await store.addNetwork(network1) + await store.addNetwork(network2) + + const session1: Session = { + id: 'sess-1', + network: 'network1', + accessToken: 'token', + } + await store.setSession(session1) + await store.addWallet(wallet1) + await store.addWallet(wallet2) + await store.setPrimaryWallet('party2') + const primary1 = await store.getPrimaryWallet() + expect(primary1?.partyId).toBe('party2') + expect(primary1?.networkId).toBe('network1') + + const session2: Session = { + id: 'sess-2', + network: 'network2', + accessToken: 'token', + } + await store.setSession(session2) + await store.addWallet(wallet3) + await store.addWallet(wallet4) + await store.setPrimaryWallet('party4') + const primary2 = await store.getPrimaryWallet() + expect(primary2?.partyId).toBe('party4') + expect(primary2?.networkId).toBe('network2') + + // Verify network1 still has party2 as primary + await store.setSession(session1) + const primary1Again = await store.getPrimaryWallet() + expect(primary1Again?.partyId).toBe('party2') + expect(primary1Again?.networkId).toBe('network1') + }) + + test('addWallet should upsert when same party exists on different network', async () => { + const wallet1: Wallet = { + primary: false, + partyId: 'party1::namespace', + status: 'allocated', + hint: 'party1', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network1', + } + const wallet2: Wallet = { + primary: false, + partyId: 'party1::namespace', // Same party ID + status: 'allocated', + hint: 'party1', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network2', // Different network + } + + // Set session for network1 + const session1: Session = { + id: 'sess-1', + network: 'network1', + accessToken: 'token', + } + await store.setSession(session1) + await store.addWallet(wallet1) + + // Switch to network2 and add same party + const session2: Session = { + id: 'sess-2', + network: 'network2', + accessToken: 'token', + } + await store.setSession(session2) + await store.addWallet(wallet2) // Should not throw, should create new entry + + const wallets = await store.getAllWallets({ + networkIds: ['network1', 'network2'], + }) + expect(wallets).toHaveLength(2) + expect( + wallets.filter((w) => w.partyId === 'party1::namespace') + ).toHaveLength(2) + expect( + wallets.find((w) => w.networkId === 'network1')?.partyId + ).toBe('party1::namespace') + expect( + wallets.find((w) => w.networkId === 'network2')?.partyId + ).toBe('party1::namespace') + }) }) }) diff --git a/core/wallet-store-inmemory/src/StoreInternal.ts b/core/wallet-store-inmemory/src/StoreInternal.ts index daad46a34..670b18d0a 100644 --- a/core/wallet-store-inmemory/src/StoreInternal.ts +++ b/core/wallet-store-inmemory/src/StoreInternal.ts @@ -24,6 +24,7 @@ import { LedgerClient, defaultRetryableOptions, } from '@canton-network/core-ledger-client' +import { CurrentNetworkWalletFilter } from '@canton-network/core-wallet-store' interface UserStorage { wallets: Array @@ -129,15 +130,20 @@ export class StoreInternal implements Store, AuthAware { throw new Error('Unexpected right kind') }) - // Merge Wallets - const existingWallets = await this.getWallets() - const existingPartyIds = new Set( - existingWallets.map((w) => w.partyId) + // Merge Wallets - check for duplicates by (partyId, networkId) + const existingWallets = await this.getAllWallets({ + networkIds: [network.id], + }) + const existingPartyNetworkPairs = new Set( + existingWallets.map((w) => `${w.partyId}:${w.networkId}`) ) const participantWallets: Array = parties ?.filter( - (party) => !existingPartyIds.has(party) + (party) => + !existingPartyNetworkPairs.has( + `${party}:${network.id}` + ) // todo: filter on idp id ) .map((party) => { @@ -156,10 +162,13 @@ export class StoreInternal implements Store, AuthAware { const storage = this.getStorage() const wallets = [...storage.wallets, ...participantWallets] - // Set primary wallet if none exists - const hasPrimary = wallets.some((w) => w.primary) - if (!hasPrimary && wallets.length > 0) { - wallets[0].primary = true + // Set primary wallet if none exists in this network + const networkWallets = wallets.filter( + (w) => w.networkId === network.id + ) + const hasPrimary = networkWallets.some((w) => w.primary) + if (!hasPrimary && networkWallets.length > 0) { + networkWallets[0].primary = true } this.logger.debug(wallets, 'Wallets synchronized') @@ -172,7 +181,7 @@ export class StoreInternal implements Store, AuthAware { } } - async getWallets(filter: WalletFilter = {}): Promise> { + async getAllWallets(filter: WalletFilter = {}): Promise> { const { networkIds, signingProviderIds } = filter const networkIdSet = networkIds ? new Set(networkIds) : null const signingProviderIdSet = signingProviderIds @@ -190,21 +199,41 @@ export class StoreInternal implements Store, AuthAware { }) } + async getWallets( + filter: CurrentNetworkWalletFilter = {} + ): Promise> { + const network = await this.getCurrentNetwork() + return this.getAllWallets({ + ...filter, + networkIds: [network.id], + }) + } + async getPrimaryWallet(): Promise { const wallets = await this.getWallets() return wallets.find((w) => w.primary === true) } async setPrimaryWallet(partyId: PartyId): Promise { + const network = await this.getCurrentNetwork() const storage = this.getStorage() - if (!storage.wallets.some((w) => w.partyId === partyId)) { - throw new Error(`Wallet with partyId "${partyId}" not found`) + const networkWallets = storage.wallets.filter( + (w) => w.networkId === network.id + ) + + if (!networkWallets.some((w) => w.partyId === partyId)) { + throw new Error( + `Wallet with partyId "${partyId}" not found in network "${network.id}"` + ) } + const wallets = storage.wallets.map((w) => { - if (w.partyId === partyId) { - w.primary = true - } else { - w.primary = false + if (w.networkId === network.id) { + if (w.partyId === partyId) { + w.primary = true + } else { + w.primary = false + } } return w }) @@ -214,35 +243,48 @@ export class StoreInternal implements Store, AuthAware { async addWallet(wallet: Wallet): Promise { const storage = this.getStorage() - if (storage.wallets.some((w) => w.partyId === wallet.partyId)) { + if ( + storage.wallets.some( + (w) => + w.partyId === wallet.partyId && + w.networkId === wallet.networkId + ) + ) { throw new Error( - `Wallet with partyId "${wallet.partyId}" already exists` + `Wallet with partyId "${wallet.partyId}" already exists in network "${wallet.networkId}"` ) } - const wallets = await this.getWallets() + const networkWallets = await this.getAllWallets({ + networkIds: [wallet.networkId], + }) - if (wallets.length === 0) { - // If this is the first wallet, set it as primary automatically + // If this is the first wallet in this network, set it as primary automatically + if (networkWallets.length === 0) { wallet.primary = true } if (wallet.primary) { - // If the new wallet is primary, set all others to non-primary - storage.wallets.map((w) => (w.primary = false)) + // If the new wallet is primary, set all others in the same network to non-primary + storage.wallets + .filter((w) => w.networkId === wallet.networkId) + .map((w) => (w.primary = false)) } - wallets.push(wallet) - storage.wallets = wallets + storage.wallets.push(wallet) this.updateStorage(storage) } async updateWallet({ status, partyId, + networkId, externalTxId, }: UpdateWallet): Promise { const storage = this.getStorage() - const wallets = (await this.getWallets()).map((wallet) => - wallet.partyId === partyId + // Use provided networkId or get current network from session + const targetNetworkId = networkId ?? (await this.getCurrentNetwork()).id + + const wallets = storage.wallets.map((wallet) => + wallet.partyId === partyId && wallet.networkId === targetNetworkId ? { ...wallet, status, externalTxId } : wallet ) @@ -252,9 +294,10 @@ export class StoreInternal implements Store, AuthAware { } async removeWallet(partyId: PartyId): Promise { + const network = await this.getCurrentNetwork() const storage = this.getStorage() - const wallets = (await this.getWallets()).filter( - (w) => w.partyId !== partyId + const wallets = storage.wallets.filter( + (w) => !(w.partyId === partyId && w.networkId === network.id) ) storage.wallets = wallets diff --git a/core/wallet-store-sql/src/migrations/006-change-wallet-primary-key-to-composite.ts b/core/wallet-store-sql/src/migrations/006-change-wallet-primary-key-to-composite.ts new file mode 100644 index 000000000..45209deeb --- /dev/null +++ b/core/wallet-store-sql/src/migrations/006-change-wallet-primary-key-to-composite.ts @@ -0,0 +1,262 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Kysely, sql } from 'kysely' +import { DB } from '../schema.js' +import { isPostgres } from '../utils.js' + +async function tableExists( + db: Kysely, + tableName: string, + isPg: boolean +): Promise { + if (isPg) { + const result = await sql<{ exists: boolean }>` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = ${tableName} + ) AS exists + `.execute(db) + return result.rows[0]?.exists ?? false + } else { + const result = await sql<{ name: string }>` + SELECT name + FROM sqlite_master + WHERE type='table' AND name = ${tableName} + `.execute(db) + return result.rows.length > 0 + } +} + +export async function up(db: Kysely): Promise { + const isPg = await isPostgres(db) + + if (isPg) { + // PostgreSQL: idempotent PK change to (party_id, network_id) + // Drop current PK if it isn't already the composite (party_id, network_id) + await sql` + DO $$ + DECLARE + pk_name text; + pk_def text; + BEGIN + SELECT c.conname, pg_get_constraintdef(c.oid) + INTO pk_name, pk_def + FROM pg_constraint c + WHERE c.conrelid = 'public.wallets'::regclass + AND c.contype = 'p'; + + IF pk_name IS NOT NULL + AND pk_def NOT ILIKE 'PRIMARY KEY (party_id, network_id)%' THEN + EXECUTE format('ALTER TABLE public.wallets DROP CONSTRAINT %I', pk_name); + END IF; + END $$; + `.execute(db) + + // Ensure network_id has no NULLs and is NOT NULL (required for PK) + await sql` + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM public.wallets WHERE network_id IS NULL) THEN + RAISE EXCEPTION 'Cannot add composite PK: wallets.network_id has NULLs'; + END IF; + END $$; + `.execute(db) + + await sql` + ALTER TABLE public.wallets + ALTER COLUMN network_id SET NOT NULL + `.execute(db) + + // Drop any leftover UNIQUE index that still enforces uniqueness on party_id alone + await sql` + DO $$ + DECLARE r record; + BEGIN + FOR r IN + SELECT indexname + FROM pg_indexes + WHERE schemaname='public' + AND tablename='wallets' + AND indexdef ILIKE 'CREATE UNIQUE INDEX % ON % ("party_id")%' + LOOP + EXECUTE format('DROP INDEX IF EXISTS %I', r.indexname); + END LOOP; + END $$; + `.execute(db) + + // Add the new composite primary key + await sql` + DO $$ + DECLARE + has_pk boolean; + BEGIN + SELECT EXISTS ( + SELECT 1 + FROM pg_constraint c + WHERE c.conrelid = 'public.wallets'::regclass + AND c.contype = 'p' + AND pg_get_constraintdef(c.oid) ILIKE 'PRIMARY KEY (party_id, network_id)%' + ) INTO has_pk; + + IF NOT has_pk THEN + EXECUTE 'ALTER TABLE public.wallets + ADD CONSTRAINT wallets_pkey PRIMARY KEY (party_id, network_id)'; + END IF; + END $$; + `.execute(db) + } else { + // SQLite: Recreate table (SQLite doesn't support altering primary keys) + // Drop temporary table if it exists + await db.schema.dropTable('wallets_new').ifExists().execute() + + const hasWallets = await tableExists(db, 'wallets', false) + const hasWalletsNew = await tableExists(db, 'wallets_new', false) + + if (!hasWallets && hasWalletsNew) { + await db.schema + .alterTable('wallets_new') + .renameTo('wallets') + .execute() + return + } + + await db.schema + .createTable('wallets_new') + .addColumn('party_id', 'text', (col) => col.notNull()) + .addColumn('network_id', 'text', (col) => + col.references('networks.id').onDelete('cascade').notNull() + ) + .addColumn('primary', 'boolean', (col) => + col.notNull().defaultTo(false) + ) + .addColumn('hint', 'text', (col) => col.notNull()) + .addColumn('public_key', 'text', (col) => col.notNull()) + .addColumn('namespace', 'text', (col) => col.notNull()) + .addColumn('user_id', 'text', (col) => col.notNull()) + .addColumn('signing_provider_id', 'text', (col) => col.notNull()) + .addColumn('status', 'text') + .addColumn('external_tx_id', 'text') + .addColumn('topology_transactions', 'text') + .addColumn('disabled', 'integer', (col) => + col.notNull().defaultTo(0) + ) + .addColumn('reason', 'text') + .addPrimaryKeyConstraint('wallets_pk', ['party_id', 'network_id']) + .execute() + + await sql` + INSERT INTO wallets_new ( + party_id, network_id, "primary", hint, public_key, namespace, + user_id, signing_provider_id, status, external_tx_id, + topology_transactions, disabled, reason + ) + SELECT + party_id, network_id, "primary", hint, public_key, namespace, + user_id, signing_provider_id, status, external_tx_id, + topology_transactions, COALESCE(disabled, 0) AS disabled, reason + FROM wallets + `.execute(db) + + await db.schema.dropTable('wallets').execute() + await db.schema.alterTable('wallets_new').renameTo('wallets').execute() + } +} + +export async function down(db: Kysely): Promise { + const isPg = await isPostgres(db) + + if (isPg) { + if (!(await tableExists(db, 'wallets', true))) return + + // PostgreSQL: Use ALTER TABLE to revert to single primary key + // drop the composite primary key + await sql` + DO $$ + DECLARE + pk_name text; + BEGIN + SELECT c.conname + INTO pk_name + FROM pg_constraint c + WHERE c.conrelid = 'public.wallets'::regclass + AND c.contype = 'p'; + + IF pk_name IS NOT NULL THEN + EXECUTE format('ALTER TABLE public.wallets DROP CONSTRAINT %I', pk_name); + END IF; + END $$; + `.execute(db) + + // Keep only one wallet per party_id (data loss if duplicates exist) + // Use DISTINCT ON to pick first row per party_id + await sql` + DELETE FROM public.wallets w1 + WHERE EXISTS ( + SELECT 1 + FROM public.wallets w2 + WHERE w2.party_id = w1.party_id + AND w2.network_id < w1.network_id + ) + `.execute(db) + + // Make network_id nullable again + await sql` + ALTER TABLE public.wallets + ALTER COLUMN network_id DROP NOT NULL + `.execute(db) + + // Add back single-column PK on party_id + await sql` + ALTER TABLE public.wallets + ADD CONSTRAINT wallets_pkey PRIMARY KEY (party_id) + `.execute(db) + } else { + // SQLite: Recreate table with single primary key + // Create old table structure with single primary key + await db.schema + .createTable('wallets_new') + .addColumn('party_id', 'text', (col) => col.primaryKey()) + .addColumn('network_id', 'text', (col) => + col.references('networks.id').onDelete('cascade') + ) + .addColumn('primary', 'boolean', (col) => + col.notNull().defaultTo(false) + ) + .addColumn('hint', 'text', (col) => col.notNull()) + .addColumn('public_key', 'text', (col) => col.notNull()) + .addColumn('namespace', 'text', (col) => col.notNull()) + .addColumn('user_id', 'text', (col) => col.notNull()) + .addColumn('signing_provider_id', 'text', (col) => col.notNull()) + .addColumn('status', 'text') + .addColumn('external_tx_id', 'text') + .addColumn('topology_transactions', 'text') + .addColumn('disabled', 'integer', (col) => + col.notNull().defaultTo(0) + ) + .addColumn('reason', 'text') + .execute() + + await sql` + INSERT INTO wallets_new ( + party_id, network_id, "primary", hint, public_key, namespace, + user_id, signing_provider_id, status, external_tx_id, + topology_transactions, disabled, reason + ) + SELECT + w.party_id, w.network_id, w."primary", w.hint, w.public_key, w.namespace, + w.user_id, w.signing_provider_id, w.status, w.external_tx_id, + w.topology_transactions, w.disabled, w.reason + FROM wallets w + INNER JOIN ( + SELECT party_id, MIN(rowid) as min_rowid + FROM wallets + GROUP BY party_id + ) ranked ON w.party_id = ranked.party_id AND w.rowid = ranked.min_rowid + `.execute(db) + + await db.schema.dropTable('wallets').execute() + await db.schema.alterTable('wallets_new').renameTo('wallets').execute() + } +} diff --git a/core/wallet-store-sql/src/migrations/007-add-unique-primary-per-network.ts b/core/wallet-store-sql/src/migrations/007-add-unique-primary-per-network.ts new file mode 100644 index 000000000..6638f0bf2 --- /dev/null +++ b/core/wallet-store-sql/src/migrations/007-add-unique-primary-per-network.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Kysely, sql } from 'kysely' +import { DB } from '../schema.js' +import { isPostgres } from '../utils.js' + +export async function up(db: Kysely): Promise { + const isPg = await isPostgres(db) + + // Cleanup duplicate primary wallets (keep first per network/user) + if (isPg) { + // PostgreSQL + await sql` + UPDATE wallets w + SET "primary" = false + FROM ( + SELECT party_id, network_id, user_id, + ROW_NUMBER() OVER (PARTITION BY network_id, user_id ORDER BY party_id) as rn + FROM wallets + WHERE "primary" = true + ) ranked + WHERE w.party_id = ranked.party_id + AND w.network_id = ranked.network_id + AND w.user_id = ranked.user_id + AND ranked.rn > 1 + AND w."primary" = true + `.execute(db) + } else { + // SQLite + await sql` + UPDATE wallets + SET "primary" = 0 + WHERE rowid NOT IN ( + SELECT MIN(rowid) + FROM wallets + WHERE "primary" = 1 + GROUP BY network_id, user_id + ) + AND "primary" = 1 + `.execute(db) + } + + // Ensure only one primary wallet per network per user + if (isPg) { + // PostgreSQL + await sql` + CREATE UNIQUE INDEX IF NOT EXISTS wallets_one_primary_per_network_user + ON wallets(network_id, user_id) + WHERE "primary" = true + `.execute(db) + } else { + // SQLite + await sql` + CREATE UNIQUE INDEX IF NOT EXISTS wallets_one_primary_per_network_user + ON wallets(network_id, user_id) + WHERE "primary" = 1 + `.execute(db) + } +} + +export async function down(db: Kysely): Promise { + await sql` + DROP INDEX IF EXISTS wallets_one_primary_per_network_user + `.execute(db) +} diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index 9e0c932d3..895d6913c 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -101,6 +101,12 @@ implementations.forEach(([name, StoreImpl]) => { const store = new StoreImpl(db, pino(sink()), authContextMock) await store.addIdp(idp) await store.addNetwork(network) + const session: Session = { + id: 'session1', + network: 'network1', + accessToken: 'token', + } + await store.setSession(session) await store.addWallet(wallet) const wallets = await store.getWallets() expect(wallets).toHaveLength(1) @@ -157,22 +163,33 @@ implementations.forEach(([name, StoreImpl]) => { await store.addIdp(idp2) await store.addNetwork(network) await store.addNetwork(network2) + const session: Session = { + id: 'session1', + network: 'network1', + accessToken: 'token', + } + await store.setSession(session) await store.addWallet(wallet1) await store.addWallet(wallet2) await store.addWallet(wallet3) + const getAllWallets = await store.getWallets() - const getWalletsByNetworkId = await store.getWallets({ + const getAllWalletsAcrossNetworks = await store.getAllWallets({ + networkIds: ['network1', 'network2'], + }) + const getWalletsByNetworkId = await store.getAllWallets({ networkIds: ['network1'], }) - const getWalletsBySigningProviderId = await store.getWallets({ + const getWalletsBySigningProviderId = await store.getAllWallets({ signingProviderIds: ['internal'], }) const getWalletsByNetworkIdAndSigningProviderId = - await store.getWallets({ + await store.getAllWallets({ networkIds: ['network1'], signingProviderIds: ['internal'], }) - expect(getAllWallets).toHaveLength(3) + expect(getAllWallets).toHaveLength(2) + expect(getAllWalletsAcrossNetworks).toHaveLength(3) expect(getWalletsByNetworkId).toHaveLength(2) expect(getWalletsBySigningProviderId).toHaveLength(3) expect(getWalletsByNetworkIdAndSigningProviderId).toHaveLength(2) @@ -202,6 +219,13 @@ implementations.forEach(([name, StoreImpl]) => { const store = new StoreImpl(db, pino(sink()), authContextMock) await store.addIdp(idp) await store.addNetwork(network) + // Set session so getCurrentNetwork() works + const session: Session = { + id: 'sess-123', + network: 'network1', + accessToken: 'token', + } + await store.setSession(session) await store.addWallet(wallet1) await store.addWallet(wallet2) await store.setPrimaryWallet('party2') @@ -261,5 +285,307 @@ implementations.forEach(([name, StoreImpl]) => { const store = new StoreImpl(db, pino(sink()), authContextMock) await expect(store.getCurrentNetwork()).rejects.toThrow() }) + + test('should allow same party ID across different networks', async () => { + const auth2: AuthorizationCodeAuth = { + method: 'authorization_code', + clientId: 'cid', + scope: 'scope', + audience: 'aud', + } + const network2: Network = { + name: 'testnet2', + id: 'network2', + synchronizerId: 'sync1::fingerprint', + identityProviderId: 'idp1', + description: 'Test Network 2', + ledgerApi, + auth: auth2, + } + const wallet1: Wallet = { + primary: false, + partyId: 'party1::namespace', + status: 'allocated', + hint: 'party1', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network1', + } + const wallet2: Wallet = { + primary: false, + partyId: 'party1::namespace', // Same party ID + status: 'allocated', + hint: 'party1', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network2', // Different network + } + const store = new StoreImpl(db, pino(sink()), authContextMock) + await store.addIdp(idp) + await store.addNetwork(network) + await store.addNetwork(network2) + const session: Session = { + id: 'session1', + network: 'network1', + accessToken: 'token', + } + await store.setSession(session) + await store.addWallet(wallet1) + await store.addWallet(wallet2) // Should not throw + + const wallets = await store.getWallets() + expect(wallets).toHaveLength(1) + const allWallets = await store.getAllWallets({ + networkIds: ['network1', 'network2'], + }) + expect(allWallets).toHaveLength(2) + expect( + allWallets.filter((w) => w.partyId === 'party1::namespace') + ).toHaveLength(2) + }) + + test('should have separate primary wallets per network', async () => { + const auth2: AuthorizationCodeAuth = { + method: 'authorization_code', + clientId: 'cid', + scope: 'scope', + audience: 'aud', + } + const network2: Network = { + name: 'testnet2', + id: 'network2', + synchronizerId: 'sync1::fingerprint', + identityProviderId: 'idp1', + description: 'Test Network 2', + ledgerApi, + auth: auth2, + } + const wallet1: Wallet = { + primary: false, + partyId: 'party1', + status: 'allocated', + hint: 'hint1', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network1', + } + const wallet2: Wallet = { + primary: false, + partyId: 'party2', + status: 'allocated', + hint: 'hint2', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network1', + } + const wallet3: Wallet = { + primary: false, + partyId: 'party3', + status: 'allocated', + hint: 'hint3', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network2', + } + const wallet4: Wallet = { + primary: false, + partyId: 'party4', + status: 'allocated', + hint: 'hint4', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network2', + } + const store = new StoreImpl(db, pino(sink()), authContextMock) + await store.addIdp(idp) + await store.addNetwork(network) + await store.addNetwork(network2) + + // Set session for network1 + const session1: Session = { + id: 'sess-1', + network: 'network1', + accessToken: 'token', + } + await store.setSession(session1) + await store.addWallet(wallet1) + await store.addWallet(wallet2) + await store.setPrimaryWallet('party2') + const primary1 = await store.getPrimaryWallet() + expect(primary1?.partyId).toBe('party2') + expect(primary1?.networkId).toBe('network1') + + // Switch to network2 + const session2: Session = { + id: 'sess-2', + network: 'network2', + accessToken: 'token', + } + await store.setSession(session2) + await store.addWallet(wallet3) + await store.addWallet(wallet4) + await store.setPrimaryWallet('party4') + const primary2 = await store.getPrimaryWallet() + expect(primary2?.partyId).toBe('party4') + expect(primary2?.networkId).toBe('network2') + + // Verify network1 still has party2 as primary + await store.setSession(session1) + const primary1Again = await store.getPrimaryWallet() + expect(primary1Again?.partyId).toBe('party2') + expect(primary1Again?.networkId).toBe('network1') + }) + + test('addWallet should upsert when same party exists on different network', async () => { + const wallet1: Wallet = { + primary: false, + partyId: 'party1::namespace', + status: 'allocated', + hint: 'party1', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network1', + } + const wallet2: Wallet = { + primary: false, + partyId: 'party1::namespace', // Same party ID + status: 'allocated', + hint: 'party1', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network2', // Different network + } + const store = new StoreImpl(db, pino(sink()), authContextMock) + await store.addIdp(idp) + await store.addNetwork(network) + + const auth2: AuthorizationCodeAuth = { + method: 'authorization_code', + clientId: 'cid', + scope: 'scope', + audience: 'aud', + } + const network2: Network = { + name: 'testnet2', + id: 'network2', + synchronizerId: 'sync1::fingerprint', + identityProviderId: 'idp1', + description: 'Test Network 2', + ledgerApi, + auth: auth2, + } + await store.addNetwork(network2) + + // Set session for network1 + const session1: Session = { + id: 'sess-1', + network: 'network1', + accessToken: 'token', + } + await store.setSession(session1) + await store.addWallet(wallet1) + + // Switch to network2 and add same party + const session2: Session = { + id: 'sess-2', + network: 'network2', + accessToken: 'token', + } + await store.setSession(session2) + await store.addWallet(wallet2) // Should not throw, should create new entry + + const wallets = await store.getWallets() + expect(wallets).toHaveLength(1) + const allWallets = await store.getAllWallets({ + networkIds: ['network1', 'network2'], + }) + expect(allWallets).toHaveLength(2) + expect( + allWallets.filter((w) => w.partyId === 'party1::namespace') + ).toHaveLength(2) + expect( + allWallets.find((w) => w.networkId === 'network1')?.partyId + ).toBe('party1::namespace') + expect( + allWallets.find((w) => w.networkId === 'network2')?.partyId + ).toBe('party1::namespace') + }) + + test('addWallet should update userId when same party+network exists with different user', async () => { + const wallet1: Wallet = { + primary: false, + partyId: 'party1::namespace', + status: 'allocated', + hint: 'party1', + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId: 'network1', + } + const store = new StoreImpl(db, pino(sink()), authContextMock) + await store.addIdp(idp) + await store.addNetwork(network) + + const session: Session = { + id: 'sess-123', + network: 'network1', + accessToken: 'token', + } + await store.setSession(session) + await store.addWallet(wallet1) + + // Verify wallet was created with first user's ID + const walletBefore = await db + .selectFrom('wallets') + .selectAll() + .where('party_id', '=', 'party1::namespace') + .where('network_id', '=', 'network1') + .executeTakeFirst() + expect(walletBefore?.userId).toBe('test-user-id') + + // Create new store with different user + const authContext2: AuthContext = { + userId: 'test-user-id-2', + accessToken: 'test-access-token-2', + } + const store2 = new StoreImpl(db, pino(sink()), authContext2) + const session2: Session = { + id: 'sess-456', + network: 'network1', + accessToken: 'token', + } + await store2.setSession(session2) + + // Add same wallet (same party+network) - should update userId + await store2.addWallet(wallet1) + + // Verify wallet userId was updated to second user's ID + const walletAfter = await db + .selectFrom('wallets') + .selectAll() + .where('party_id', '=', 'party1::namespace') + .where('network_id', '=', 'network1') + .executeTakeFirst() + expect(walletAfter?.userId).toBe('test-user-id-2') + expect(walletAfter?.partyId).toBe('party1::namespace') + expect(walletAfter?.networkId).toBe('network1') + + // Verify there's still only one wallet (not duplicated) + const allWallets = await db + .selectFrom('wallets') + .selectAll() + .where('party_id', '=', 'party1::namespace') + .where('network_id', '=', 'network1') + .execute() + expect(allWallets).toHaveLength(1) + }) }) }) diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 0bbf0d70a..c66c0d440 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -19,6 +19,7 @@ import { Network, StoreConfig, UpdateWallet, + CurrentNetworkWalletFilter, } from '@canton-network/core-wallet-store' import { CamelCasePlugin, Kysely, PostgresDialect, SqliteDialect } from 'kysely' import Database from 'better-sqlite3' @@ -57,7 +58,7 @@ export class StoreSql implements BaseStore, AuthAware { // Wallet methods - async getWallets(filter: WalletFilter = {}): Promise> { + async getAllWallets(filter: WalletFilter = {}): Promise> { const userId = this.assertConnected() const { networkIds, signingProviderIds } = filter const networkIdSet = networkIds ? new Set(networkIds) : null @@ -102,31 +103,60 @@ export class StoreSql implements BaseStore, AuthAware { ) } + async getWallets( + filter: CurrentNetworkWalletFilter = {} + ): Promise> { + const network = await this.getCurrentNetwork() + return this.getAllWallets({ + ...filter, + networkIds: [network.id], + }) + } + async getPrimaryWallet(): Promise { const wallets = await this.getWallets() return wallets.find((w) => w.primary === true) } async setPrimaryWallet(partyId: PartyId): Promise { + const network = await this.getCurrentNetwork() + const userId = this.assertConnected() const wallets = await this.getWallets() + if (!wallets.some((w) => w.partyId === partyId)) { - throw new Error(`Wallet with partyId "${partyId}" not found`) + throw new Error( + `Wallet with partyId "${partyId}" not found in network "${network.id}"` + ) } const primary = wallets.find((w) => w.primary === true) await this.db.transaction().execute(async (trx) => { if (primary) { + // Unset primary for current network only await trx .updateTable('wallets') .set({ primary: 0 }) - .where('partyId', '=', primary.partyId) + .where((eb) => + eb.and([ + eb('partyId', '=', primary.partyId), + eb('networkId', '=', network.id), + eb('userId', '=', userId), + ]) + ) .execute() } + // Set new primary for current network await trx .updateTable('wallets') .set({ primary: 1 }) - .where('partyId', '=', partyId) + .where((eb) => + eb.and([ + eb('partyId', '=', partyId), + eb('networkId', '=', network.id), + eb('userId', '=', userId), + ]) + ) .execute() }) } @@ -135,62 +165,134 @@ export class StoreSql implements BaseStore, AuthAware { this.logger.info('Adding wallet') const userId = this.assertConnected() - const wallets = await this.getWallets() - if (wallets.some((w) => w.partyId === wallet.partyId)) { - throw new Error( - `Wallet with partyId "${wallet.partyId}" already exists` - ) - } - - if (wallets.length === 0) { - // If this is the first wallet, set it as primary automatically + // Check if this is the first wallet in this network for this user + const wallets = await this.getAllWallets({ + networkIds: [wallet.networkId], + }) + const isFirstWallet = wallets.length === 0 + if (isFirstWallet) { wallet.primary = true } await this.db.transaction().execute(async (trx) => { - if (wallet.primary) { - // If the new wallet is primary, set all others to non-primary + // Check if wallet already exists (possibly from a different user) + const existingWallet = await trx + .selectFrom('wallets') + .selectAll() + .where((eb) => + eb.and([ + eb('partyId', '=', wallet.partyId), + eb('networkId', '=', wallet.networkId), + ]) + ) + .executeTakeFirst() + + if (existingWallet) { + // Wallet exists - update it (handles case where network was edited to use different user) + this.logger.info( + { + partyId: wallet.partyId, + networkId: wallet.networkId, + oldUserId: existingWallet.userId, + newUserId: userId, + }, + 'Updating existing wallet (possibly from different user)' + ) + + if (wallet.primary) { + // If the new wallet is primary, set all others in the same network to non-primary + await trx + .updateTable('wallets') + .set({ primary: 0 }) + .where((eb) => + eb.and([ + eb('primary', '=', 1), + eb('networkId', '=', wallet.networkId), + eb('userId', '=', userId), + ]) + ) + .execute() + } + + // Update the existing wallet with new data and user await trx .updateTable('wallets') - .set({ primary: 0 }) + .set(fromWallet(wallet, userId)) .where((eb) => eb.and([ - eb('primary', '=', 1), - eb('userId', '=', userId), + eb('partyId', '=', wallet.partyId), + eb('networkId', '=', wallet.networkId), ]) ) .execute() + } else { + // Wallet doesn't exist - insert it + if (wallet.primary) { + // If the new wallet is primary, set all others in the same network to non-primary + await trx + .updateTable('wallets') + .set({ primary: 0 }) + .where((eb) => + eb.and([ + eb('primary', '=', 1), + eb('networkId', '=', wallet.networkId), + eb('userId', '=', userId), + ]) + ) + .execute() + } + await trx + .insertInto('wallets') + .values(fromWallet(wallet, userId)) + .execute() } - await trx - .insertInto('wallets') - .values(fromWallet(wallet, userId)) - .execute() }) } async updateWallet({ status, partyId, + networkId, externalTxId, }: UpdateWallet): Promise { this.logger.info('Updating wallet') + const userId = this.assertConnected() + + // Use provided networkId or get current network from session + const targetNetworkId = networkId ?? (await this.getCurrentNetwork()).id await this.db.transaction().execute(async (trx) => { await trx .updateTable('wallets') .set({ status, externalTxId }) - .where('partyId', '=', partyId) + .where((eb) => + eb.and([ + eb('partyId', '=', partyId), + eb('networkId', '=', targetNetworkId), + eb('userId', '=', userId), + ]) + ) .execute() }) } async removeWallet(partyId: PartyId): Promise { this.logger.info('Removing wallet') + const userId = this.assertConnected() + + // Remove wallet from current network only + const network = await this.getCurrentNetwork() await this.db.transaction().execute(async (trx) => { await trx .deleteFrom('wallets') - .where('partyId', '=', partyId) + .where((eb) => + eb.and([ + eb('partyId', '=', partyId), + eb('networkId', '=', network.id), + eb('userId', '=', userId), + ]) + ) .execute() }) } diff --git a/core/wallet-store-sql/src/utils.ts b/core/wallet-store-sql/src/utils.ts new file mode 100644 index 000000000..a38aa2584 --- /dev/null +++ b/core/wallet-store-sql/src/utils.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Kysely, sql } from 'kysely' +import { DB } from './schema' + +export async function isPostgres(db: Kysely): Promise { + try { + // Try to query PostgreSQL system catalog + await sql`SELECT 1 FROM pg_database LIMIT 1`.execute(db) + return true + } catch { + return false + } +} diff --git a/core/wallet-store/src/Store.ts b/core/wallet-store/src/Store.ts index a0f2e09a2..6ef08ec7c 100644 --- a/core/wallet-store/src/Store.ts +++ b/core/wallet-store/src/Store.ts @@ -26,9 +26,12 @@ export interface WalletFilter { signingProviderIds?: string[] } +export type CurrentNetworkWalletFilter = Omit + export interface UpdateWallet { status: WalletStatus partyId: PartyId + networkId?: string externalTxId: string } @@ -73,7 +76,8 @@ export interface Transaction { export interface Store { // Wallet methods - getWallets(filter?: WalletFilter): Promise> + getWallets(filter?: CurrentNetworkWalletFilter): Promise> + getAllWallets(filter?: WalletFilter): Promise> getPrimaryWallet(): Promise setPrimaryWallet(partyId: PartyId): Promise addWallet(wallet: Wallet): Promise diff --git a/core/wallet-test-utils/src/wallet-gateway.ts b/core/wallet-test-utils/src/wallet-gateway.ts index be2c7f6c8..b581b624a 100644 --- a/core/wallet-test-utils/src/wallet-gateway.ts +++ b/core/wallet-test-utils/src/wallet-gateway.ts @@ -125,7 +125,9 @@ export class WalletGateway { // Check for existing user with that party hint. const pattern = new RegExp(`${args.partyHint}::[0-9a-f]+`) - const wallets = (await this.popup()).getByText(pattern) + const wallets = (await this.popup()) + .locator('.wallet-card') + .filter({ hasText: pattern }) const walletsCount = await wallets.count() if (walletsCount > 0) { const partyId = (await wallets.first().innerText()).match( @@ -134,6 +136,14 @@ export class WalletGateway { if (partyId === undefined) { throw new Error(`did not find partyID for ${args.partyHint}`) } + + if (args.primary) { + await wallets + .first() + .getByRole('button', { name: 'Set Primary' }) + .click() + } + return partyId } diff --git a/core/wallet-ui-components/src/components/wallets-sync.ts b/core/wallet-ui-components/src/components/wallets-sync.ts index ab3ebe4cf..68603a187 100644 --- a/core/wallet-ui-components/src/components/wallets-sync.ts +++ b/core/wallet-ui-components/src/components/wallets-sync.ts @@ -143,6 +143,13 @@ export class WgWalletsSync extends BaseElement { // Re-check if sync is needed after sync await this.checkWalletSyncNeeded() + this.dispatchEvent( + new CustomEvent('sync-success', { + detail: {}, + bubbles: true, + composed: true, + }) + ) } catch (e) { handleErrorToast(e) } finally { diff --git a/wallet-gateway/remote/src/dapp-api/controller.ts b/wallet-gateway/remote/src/dapp-api/controller.ts index b42b338f7..1dadb3f66 100644 --- a/wallet-gateway/remote/src/dapp-api/controller.ts +++ b/wallet-gateway/remote/src/dapp-api/controller.ts @@ -232,8 +232,7 @@ export const dappController = ( throw new Error('Only for events.') }, listAccounts: async () => { - const wallets = await store.getWallets() - return wallets + return await store.getWallets() }, txChanged: async () => { throw new Error('Only for events.') diff --git a/wallet-gateway/remote/src/ledger/wallet-sync-service.test.ts b/wallet-gateway/remote/src/ledger/wallet-sync-service.test.ts index 8bc1e4685..df2c60d9c 100644 --- a/wallet-gateway/remote/src/ledger/wallet-sync-service.test.ts +++ b/wallet-gateway/remote/src/ledger/wallet-sync-service.test.ts @@ -25,7 +25,8 @@ import { } from '@canton-network/core-signing-store-sql' import { AuthContext } from '@canton-network/core-wallet-auth' import { LedgerClient } from '@canton-network/core-ledger-client' -import { Store } from '@canton-network/core-wallet-store' +import { Wallet, Network, Store } from '@canton-network/core-wallet-store' +import { StoreInternal } from '@canton-network/core-wallet-store-inmemory' import { WalletSyncService } from './wallet-sync-service.js' import { PartyAllocationService } from './party-allocation-service.js' @@ -242,3 +243,459 @@ describe('WalletSyncService - resolveSigningProvider', () => { }) }) }) + +describe('WalletSyncService - multi-network features', () => { + const authContext: AuthContext = { + userId: 'test-user-id', + accessToken: 'test-access-token', + } + + let mockLogger: Logger + let store: StoreInternal + let mockLedgerClient: LedgerClient + let mockAdminLedgerClient: LedgerClient + let partyAllocator: PartyAllocationService + const createNetwork = (id: string): Network => ({ + id, + name: `Network ${id}`, + synchronizerId: `${id}-sync`, + identityProviderId: 'idp1', + description: `Test Network ${id}`, + ledgerApi: { baseUrl: `http://${id}` }, + auth: { + method: 'authorization_code' as const, + clientId: 'cid', + scope: 'scope', + audience: 'aud', + }, + }) + + const createWallet = ( + partyId: string, + networkId: string, + disabled = false + ): Wallet => ({ + primary: false, + partyId, + status: 'allocated', + hint: partyId.split('::')[0], + signingProviderId: 'internal', + publicKey: 'publicKey', + namespace: 'namespace', + networkId, + disabled, + }) + + const setSession = async (networkId: string) => { + await store.setSession({ + id: `sess-${networkId}`, + network: networkId, + accessToken: 'token', + }) + } + + beforeEach(async () => { + mockLogger = pino(sink()) as Logger + store = new StoreInternal( + { + idps: [], + networks: [], + }, + mockLogger, + authContext + ) + + // Add a default IdP that tests can use (use updateIdp to avoid errors if it already exists) + try { + await store.addIdp({ + id: 'idp1', + type: 'oauth', + issuer: 'http://auth', + configUrl: 'http://auth/.well-known/openid-configuration', + }) + } catch { + // IdP might already exist from previous test, use updateIdp instead + await store.updateIdp({ + id: 'idp1', + type: 'oauth', + issuer: 'http://auth', + configUrl: 'http://auth/.well-known/openid-configuration', + }) + } + + partyAllocator = new PartyAllocationService({ + synchronizerId: 'test-sync-id', + accessTokenProvider: { + getUserAccessToken: async () => 'user.jwt', + getAdminAccessToken: async () => 'admin.jwt', + }, + httpLedgerUrl: 'http://test', + logger: mockLogger, + }) + + const ledgerModule = await import('@canton-network/core-ledger-client') + mockLedgerClient = new ledgerModule.LedgerClient({ + baseUrl: new URL('http://test'), + logger: mockLogger, + accessTokenProvider: { + getUserAccessToken: async () => 'token', + getAdminAccessToken: async () => 'token', + }, + }) + mockAdminLedgerClient = new ledgerModule.LedgerClient({ + baseUrl: new URL('http://test'), + logger: mockLogger, + isAdmin: true, + accessTokenProvider: { + getUserAccessToken: async () => 'token', + getAdminAccessToken: async () => 'token', + }, + }) + }) + + afterEach(() => { + jest.restoreAllMocks() + mockLedgerGet.mockClear() + }) + + it('isWalletSyncNeeded should filter by current network', async () => { + const network1 = createNetwork('network1') + await store.addNetwork(network1) + await setSession('network1') + await store.addWallet(createWallet('party1::namespace', 'network1')) + await store.addWallet(createWallet('party2::namespace', 'network2')) + + mockLedgerGet.mockResolvedValueOnce({ + rights: [ + { + kind: { + CanActAs: { + value: { + party: 'party1::namespace', + }, + }, + }, + }, + ], + }) + + const service = new WalletSyncService( + store, + mockLedgerClient, + mockAdminLedgerClient, + authContext, + mockLogger, + {}, + partyAllocator + ) + + const syncNeeded = await service.isWalletSyncNeeded() + + // Should return false because party1 already exists in network1 + expect(syncNeeded).toBe(false) + }) + + it('isWalletSyncNeeded should detect new parties for current network only', async () => { + const network1 = createNetwork('network1') + await store.addNetwork(network1) + await setSession('network1') + + mockLedgerGet.mockResolvedValueOnce({ + rights: [ + { + kind: { + CanActAs: { + value: { + party: 'party1::namespace', + }, + }, + }, + }, + ], + }) + + const service = new WalletSyncService( + store, + mockLedgerClient, + mockAdminLedgerClient, + authContext, + mockLogger, + {}, + partyAllocator + ) + + const syncNeeded = await service.isWalletSyncNeeded() + + // Should return true because party1 exists on ledger but not in store for network1 + expect(syncNeeded).toBe(true) + }) + + it('syncWallets should only sync wallets for current network', async () => { + const network1 = createNetwork('network1') + await store.addNetwork(network1) + await setSession('network1') + await store.addWallet(createWallet('party1::namespace', 'network1')) + const addWalletSpy = jest.spyOn(store, 'addWallet') + + mockLedgerGet + .mockResolvedValueOnce({ + rights: [ + { + kind: { + CanActAs: { + value: { + party: 'party1::namespace', + }, + }, + }, + }, + { + kind: { + CanActAs: { + value: { + party: 'party3::namespace', + }, + }, + }, + }, + ], + }) + .mockResolvedValueOnce({ + participantId: 'participant1::namespace', + }) + + const service = new WalletSyncService( + store, + mockLedgerClient, + mockAdminLedgerClient, + authContext, + mockLogger, + {}, + partyAllocator + ) + + await service.syncWallets() + + // Should only add wallet for party3 (party1 already exists) + const wallets = await store.getAllWallets({ networkIds: ['network1'] }) + expect(wallets.some((w) => w.partyId === 'party3::namespace')).toBe( + true + ) + expect(addWalletSpy).toHaveBeenCalled() + }) + + it('syncWallets should handle same party ID across different networks', async () => { + const network1 = createNetwork('network1') + await store.addNetwork(network1) + await setSession('network1') + + // Mock ledger client to return rights for party1 + mockLedgerGet + .mockResolvedValueOnce({ + rights: [ + { + kind: { + CanActAs: { + value: { + party: 'party1::namespace', + }, + }, + }, + }, + ], + }) + .mockResolvedValueOnce({ + participantId: 'participant1::namespace', + }) + + const service = new WalletSyncService( + store, + mockLedgerClient, + mockAdminLedgerClient, + authContext, + mockLogger, + {}, + partyAllocator + ) + + await service.syncWallets() + + // Should add party1 for network1 + const wallets = await store.getAllWallets({ networkIds: ['network1'] }) + expect(wallets.some((w) => w.partyId === 'party1::namespace')).toBe( + true + ) + }) + + it('isWalletSyncNeeded should detect multi-hosted party on different network', async () => { + const network1 = createNetwork('network1') + const network2 = createNetwork('network2') + await store.addNetwork(network1) + await store.addNetwork(network2) + + await store.addWallet(createWallet('party1::namespace', 'network1')) + + const service = new WalletSyncService( + store, + mockLedgerClient, + mockAdminLedgerClient, + authContext, + mockLogger, + {}, + partyAllocator + ) + + // Mock ledger client to return rights for party1 (multi-hosted party) for network1 check + mockLedgerGet.mockResolvedValueOnce({ + rights: [ + { + kind: { + CanActAs: { + value: { + party: 'party1::namespace', + }, + }, + }, + }, + ], + }) + + await setSession('network1') + // Check sync needed for network1 (party already exists) + const syncNeeded1 = await service.isWalletSyncNeeded() + expect(syncNeeded1).toBe(false) + + // Mock ledger client to return rights for party1 for network2 check + mockLedgerGet.mockResolvedValueOnce({ + rights: [ + { + kind: { + CanActAs: { + value: { + party: 'party1::namespace', + }, + }, + }, + }, + ], + }) + + await setSession('network2') + // Check sync needed for network2 (party doesn't exist yet) + const syncNeeded2 = await service.isWalletSyncNeeded() + expect(syncNeeded2).toBe(true) + }) + + it('syncWallets should handle multi-hosted party across networks', async () => { + const network1 = createNetwork('network1') + const network2 = createNetwork('network2') + await store.addNetwork(network1) + await store.addNetwork(network2) + + // Add wallet to network1 (simulating it was synced there previously) + await setSession('network1') + await store.addWallet(createWallet('party1::namespace', 'network1')) + + const service = new WalletSyncService( + store, + mockLedgerClient, + mockAdminLedgerClient, + authContext, + mockLogger, + {}, + partyAllocator + ) + + // Sync on network1 (party already exists, should not add) + await setSession('network1') + // Only need one mock since resolveSigningProvider won't be called if party already exists + mockLedgerGet.mockResolvedValueOnce({ + rights: [ + { + kind: { + CanActAs: { + value: { + party: 'party1::namespace', + }, + }, + }, + }, + ], + }) + const syncResult1 = await service.syncWallets() + expect(syncResult1.added.length).toBe(0) // Should not add, already exists + + // Sync on network2 (party doesn't exist, should add) + await setSession('network2') + + // Verify no wallets exist for network2 before sync + const walletsBeforeSync = await store.getAllWallets({ + networkIds: ['network2'], + }) + expect(walletsBeforeSync.length).toBe(0) + + mockLedgerGet.mockClear() + // First mock: getPartiesRightsMap calls ledgerClient.getWithRetry('/v2/users/{user-id}/rights') + mockLedgerGet.mockResolvedValueOnce({ + rights: [ + { + kind: { + CanActAs: { + value: { + party: 'party1::namespace', + }, + }, + }, + }, + ], + }) + // Second mock: resolveSigningProvider calls adminLedgerClient.getWithRetry('/v2/parties/participant-id') + mockLedgerGet.mockResolvedValueOnce({ + participantId: 'participant1::namespace', + }) + + expect(mockLedgerGet).toHaveBeenCalledTimes(0) + const syncResult = await service.syncWallets() + + expect(mockLedgerGet).toHaveBeenCalledTimes(2) // Once for rights, once for participantId + expect(syncResult.added.length).toBe(1) + expect(syncResult.added[0].partyId).toBe('party1::namespace') + expect(syncResult.added[0].networkId).toBe('network2') + expect(syncResult.added[0].disabled).toBe(false) + + const network2Wallets = await store.getAllWallets({ + networkIds: ['network2'], + }) + const party1Wallet = network2Wallets.find( + (w) => w.partyId === 'party1::namespace' + ) + expect(party1Wallet).toBeDefined() + expect(party1Wallet?.networkId).toBe('network2') + expect(party1Wallet?.disabled).toBe(false) + }) + + // TODO maybe now that we never block sync button don't consider disabled wallet as sync is needed? + it('isWalletSyncNeeded should return true when disabled wallets exist', async () => { + const network1 = createNetwork('network1') + await store.addNetwork(network1) + await setSession('network1') + await store.addWallet( + createWallet('party1::namespace', 'network1', true) + ) + + const service = new WalletSyncService( + store, + mockLedgerClient, + mockAdminLedgerClient, + authContext, + mockLogger, + {}, + partyAllocator + ) + + const syncNeeded = await service.isWalletSyncNeeded() + + // Should return true because there's a disabled wallet + expect(syncNeeded).toBe(true) + }) +}) diff --git a/wallet-gateway/remote/src/ledger/wallet-sync-service.ts b/wallet-gateway/remote/src/ledger/wallet-sync-service.ts index e2f18adf7..21f2af57d 100644 --- a/wallet-gateway/remote/src/ledger/wallet-sync-service.ts +++ b/wallet-gateway/remote/src/ledger/wallet-sync-service.ts @@ -223,27 +223,29 @@ export class WalletSyncService { async isWalletSyncNeeded(): Promise { try { + const network = await this.store.getCurrentNetwork() + const existingWallets = await this.store.getWallets() - // Check if there are any disabled wallets const hasDisabledWallets = existingWallets.some((w) => w.disabled) if (hasDisabledWallets) { return true } - // Check if there are parties on ledger that aren't in store const partiesWithRights = await this.getPartiesRightsMap() // Treat disabled wallets as if they don't exist, so they can be re-synced const enabledWallets = existingWallets.filter((w) => !w.disabled) - const existingPartyIds = new Set( - enabledWallets.map((w) => w.partyId) + // Track by (partyId, networkId) combination to handle multi-hosted parties + const existingPartyNetworkPairs = new Set( + enabledWallets.map((w) => `${w.partyId}:${w.networkId}`) ) - // Check if there are parties on ledger that aren't in store - return partiesWithRights - .keys() - .some((party) => !existingPartyIds.has(party)) + // Check if there are parties on ledger that aren't in store for this network + return Array.from(partiesWithRights.keys()).some( + (party) => + !existingPartyNetworkPairs.has(`${party}:${network.id}`) + ) } catch (err) { this.logger.error({ err }, 'Error checking if sync is needed') // On error, return false to avoid showing sync button unnecessarily @@ -256,25 +258,36 @@ export class WalletSyncService { const network = await this.store.getCurrentNetwork() this.logger.info(network, 'Current network') - // Get existing parties from participant const partiesWithRights = await this.getPartiesRightsMap() // Add new Wallets given the found parties + // Only check wallets in the current network const existingWallets = await this.store.getWallets() this.logger.info(existingWallets, 'Existing wallets') // Treat disabled wallets as if they don't exist, so they can be re-synced const enabledWallets = existingWallets.filter((w) => !w.disabled) - const existingPartyIdToSigningProvider = new Map( - enabledWallets.map((w) => [w.partyId, w.signingProviderId]) + // Track by (partyId, networkId) combination + const existingPartyNetworkToSigningProvider = new Map( + enabledWallets.map((w) => [ + `${w.partyId}:${w.networkId}`, + w.signingProviderId, + ]) ) - const disabledPartyIds = new Set( - existingWallets.filter((w) => w.disabled).map((w) => w.partyId) + // Track disabled wallets by (partyId, networkId) combination + const disabledPartyNetworkPairs = new Set( + existingWallets + .filter((w) => w.disabled) + .map((w) => `${w.partyId}:${w.networkId}`) ) // Resolve signing providers for all new parties + // Check if (partyId, networkId) combination already exists const newParties = Array.from(partiesWithRights.keys()).filter( - (party) => !existingPartyIdToSigningProvider.has(party) + (party) => + !existingPartyNetworkToSigningProvider.has( + `${party}:${network.id}` + ) // todo: filter on idp id ) @@ -330,12 +343,20 @@ export class WalletSyncService { ) // Remove disabled wallets that are being re-synced before adding them back + // Filter by (partyId, networkId) combination await Promise.all( newParticipantWallets - .filter((wallet) => disabledPartyIds.has(wallet.partyId)) + .filter((wallet) => + disabledPartyNetworkPairs.has( + `${wallet.partyId}:${wallet.networkId}` + ) + ) .map((wallet) => { this.logger.info( - { partyId: wallet.partyId }, + { + partyId: wallet.partyId, + networkId: wallet.networkId, + }, 'Removing disabled wallet for re-sync' ) return this.store.removeWallet(wallet.partyId) @@ -358,15 +379,20 @@ export class WalletSyncService { 'Wallet sync summary' ) - // Set primary wallet if none exists - const wallets = await this.store.getWallets() - const hasPrimary = wallets.some((w) => w.primary) - if (!hasPrimary && wallets.length > 0) { - this.store.setPrimaryWallet(wallets[0].partyId) - this.logger.info(`Set ${wallets[0].partyId} as primary wallet`) + // Set primary wallet if none exists in current network + const networkWallets = await this.store.getWallets() + const hasPrimary = networkWallets.some((w) => w.primary) + if (!hasPrimary && networkWallets.length > 0) { + this.store.setPrimaryWallet(networkWallets[0].partyId) + this.logger.info( + `Set ${networkWallets[0].partyId} as primary wallet in network ${network.id}` + ) } - this.logger.debug(wallets, 'Wallet sync completed.') + this.logger.debug( + { wallets: newParticipantWallets }, + 'Wallet sync completed.' + ) return { added: newParticipantWallets, removed: [], diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 5baef5e92..5fc76dbc2 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -441,6 +441,7 @@ export const userController = ( ) { await store.updateWallet({ partyId: wallet.partyId, + networkId: wallet.networkId, status: wallet.status, externalTxId: wallet.externalTxId!, }) @@ -458,7 +459,9 @@ export const userController = ( const notifier = authContext?.userId ? notificationService.getNotifier(authContext.userId) : undefined - notifier?.emit('accountsChanged', await store.getWallets()) + + const wallets = await store.getWallets() + notifier?.emit('accountsChanged', wallets) return null }, removeWallet: async (params: { partyId: string }) => @@ -466,8 +469,7 @@ export const userController = ( listWallets: async (params: { filter?: { networkIds?: string[]; signingProviderIds?: string[] } }) => { - // TODO: support filters - return await store.getWallets() + return await store.getAllWallets(params.filter) }, sign: async ({ preparedTransaction, @@ -475,21 +477,20 @@ export const userController = ( partyId, commandId, }: SignParams) => { + const network = await store.getCurrentNetwork() + if (network === undefined) { + throw new Error('No network session found') + } + const wallets = await store.getWallets() const wallet = wallets.find((w) => w.partyId === partyId) - const network = await store.getCurrentNetwork() - if (wallet === undefined) { throw new Error('No primary wallet found') } const userId = assertConnected(authContext).userId - if (network === undefined) { - throw new Error('No network session found') - } - const notifier = notificationService.getNotifier(userId) const signingProvider = wallet.signingProviderId as SigningProvider const driver = drivers[signingProvider]?.controller(userId) diff --git a/wallet-gateway/remote/src/web/frontend/approve/index.ts b/wallet-gateway/remote/src/web/frontend/approve/index.ts index bf1a7e195..ea70d73a2 100644 --- a/wallet-gateway/remote/src/web/frontend/approve/index.ts +++ b/wallet-gateway/remote/src/web/frontend/approve/index.ts @@ -214,7 +214,8 @@ export class ApproveUi extends LitElement { this.txParsed = null } }) - userClient.request('listWallets', []).then((wallets) => { + + userClient.request('listWallets', {}).then((wallets) => { this.partyId = wallets.find((w) => w.primary === true)?.partyId || '' }) diff --git a/wallet-gateway/remote/src/web/frontend/wallets/index.ts b/wallet-gateway/remote/src/web/frontend/wallets/index.ts index e9592d483..8ba562b07 100644 --- a/wallet-gateway/remote/src/web/frontend/wallets/index.ts +++ b/wallet-gateway/remote/src/web/frontend/wallets/index.ts @@ -170,6 +170,12 @@ export class UserUiWallets extends LitElement { ` protected render() { + // This prevents race condition between render and this.client being set in connectedCallback asynchronously, + // resulting in keeping client as null + if (!this.client) { + return html`` + } + const shownWallets = { verifiedWallets: [] as Wallet[], unverifiedWallets: [] as Wallet[], @@ -188,6 +194,7 @@ export class UserUiWallets extends LitElement { @@ -378,9 +385,20 @@ export class UserUiWallets extends LitElement { const userClient = await createUserClient( stateManager.accessToken.get() ) - userClient.request('listWallets', []).then((wallets) => { - this.wallets = wallets || [] - }) + + const sessions = await userClient + .request('listSessions') + .catch(() => ({ sessions: [] })) + const currentSession = sessions?.sessions?.[0] + const networkId = + currentSession?.network?.id || stateManager.networkId.get() + + const filter = networkId ? { networkIds: [networkId] } : undefined + userClient + .request('listWallets', filter ? { filter } : {}) + .then((wallets) => { + this.wallets = wallets || [] + }) } private async _setPrimary(wallet: Wallet) { From f38586fd6bc68efe3659b281331c97c0dbcaf14f Mon Sep 17 00:00:00 2001 From: Fayi <112705750+fayi-da@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:00:02 +0000 Subject: [PATCH 15/16] chore: setup example portfolio for release (#1210) Signed-off-by: fayi-da --- examples/portfolio/package.json | 8 +++++++- nx.json | 4 ++++ scripts/src/release.ts | 7 ++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index 72db98960..294ff2879 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/example-portfolio", - "private": true, + "private": false, "version": "0.0.0", "type": "module", "scripts": { @@ -59,6 +59,12 @@ "typescript-eslint": "^8.53.1", "vite": "^7.3.1" }, + "files": [ + "dist/**" + ], + "publishConfig": { + "access": "public" + }, "nx": { "targets": { "playwright:e2e": { diff --git a/nx.json b/nx.json index 341a7054e..96cb58958 100644 --- a/nx.json +++ b/nx.json @@ -41,6 +41,10 @@ "wallet-gateway": { "projects": ["wallet-gateway/remote"], "projectsRelationship": "independent" + }, + "example-portfolio": { + "projects": ["examples/portfolio"], + "projectsRelationship": "independent" } } }, diff --git a/scripts/src/release.ts b/scripts/src/release.ts index bd831b38a..e6b4d0716 100644 --- a/scripts/src/release.ts +++ b/scripts/src/release.ts @@ -25,7 +25,12 @@ async function cmd(command: string): Promise { }) } -const options = ['wallet-sdk', 'dapp-sdk', 'wallet-gateway'] +const options = [ + 'wallet-sdk', + 'dapp-sdk', + 'wallet-gateway', + 'example-portfolio', +] program .option('--dry-run', 'Perform a dry run (default: true)') From 70e6e589e4e0242479741c242a5d2ecf06afc958 Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Fri, 30 Jan 2026 11:34:35 -0500 Subject: [PATCH 16/16] chore(release): publish (#1211) - project: @canton-network/core-wallet-dapp-remote-rpc-client 0.15.0 - project: @canton-network/core-wallet-dapp-rpc-client 0.21.0 - project: @canton-network/core-wallet-user-rpc-client 0.22.0 - project: @canton-network/core-wallet-store-inmemory 0.22.0 - project: @canton-network/core-wallet-ui-components 0.22.0 - project: @canton-network/core-signing-blockdaemon 0.19.0 - project: @canton-network/core-signing-participant 0.21.0 - project: @canton-network/core-signing-fireblocks 0.19.0 - project: @canton-network/core-signing-store-sql 0.22.0 - project: @canton-network/core-wallet-test-utils 0.2.0 - project: @canton-network/core-signing-internal 0.21.0 - project: @canton-network/core-wallet-store-sql 0.21.0 - project: @canton-network/core-splice-provider 0.22.0 - project: @canton-network/core-token-standard 0.18.0 - project: @canton-network/core-ledger-client 0.26.0 - project: @canton-network/core-rpc-transport 0.3.0 - project: @canton-network/core-splice-client 0.20.0 - project: @canton-network/core-tx-visualizer 0.18.0 - project: @canton-network/core-ledger-proto 0.18.0 - project: @canton-network/core-wallet-store 0.21.0 - project: @canton-network/core-signing-lib 0.21.0 - project: @canton-network/core-wallet-auth 0.18.0 - project: @canton-network/core-rpc-errors 0.14.0 - project: @canton-network/core-types 0.17.0 - project: @canton-network/example-portfolio 0.1.0 Signed-off-by: Alex Matson --- core/ledger-client/package.json | 2 +- core/ledger-proto/package.json | 2 +- core/rpc-errors/package.json | 2 +- core/rpc-transport/package.json | 2 +- core/signing-blockdaemon/package.json | 2 +- core/signing-fireblocks/package.json | 2 +- core/signing-internal/package.json | 2 +- core/signing-lib/package.json | 2 +- core/signing-participant/package.json | 2 +- core/signing-store-sql/package.json | 2 +- core/splice-client/package.json | 2 +- core/splice-provider/package.json | 2 +- core/token-standard/package.json | 2 +- core/tx-visualizer/package.json | 2 +- core/types/package.json | 2 +- core/wallet-auth/package.json | 2 +- core/wallet-dapp-remote-rpc-client/package.json | 2 +- core/wallet-dapp-rpc-client/package.json | 2 +- core/wallet-store-inmemory/package.json | 2 +- core/wallet-store-sql/package.json | 2 +- core/wallet-store/package.json | 2 +- core/wallet-test-utils/package.json | 2 +- core/wallet-ui-components/package.json | 2 +- core/wallet-user-rpc-client/package.json | 2 +- examples/portfolio/package.json | 3 +-- 25 files changed, 25 insertions(+), 26 deletions(-) diff --git a/core/ledger-client/package.json b/core/ledger-client/package.json index 47c007324..a65938e17 100644 --- a/core/ledger-client/package.json +++ b/core/ledger-client/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-ledger-client", - "version": "0.25.2", + "version": "0.26.0", "type": "module", "description": "Provides a TypeScript Canton Network ledger client, generated by the OpenAPI spec", "license": "Apache-2.0", diff --git a/core/ledger-proto/package.json b/core/ledger-proto/package.json index 2238bb313..5f21589d6 100644 --- a/core/ledger-proto/package.json +++ b/core/ledger-proto/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-ledger-proto", - "version": "0.17.3", + "version": "0.18.0", "type": "module", "description": "Provides TypeScript protobuf bindings for Canton", "license": "Apache-2.0", diff --git a/core/rpc-errors/package.json b/core/rpc-errors/package.json index 98086534a..d5a0a4cf9 100644 --- a/core/rpc-errors/package.json +++ b/core/rpc-errors/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-rpc-errors", - "version": "0.13.3", + "version": "0.14.0", "type": "module", "description": "Wrapper for JSON-RPC error objects", "author": "Alex Matson ", diff --git a/core/rpc-transport/package.json b/core/rpc-transport/package.json index 03fa7a889..39fbc5e20 100644 --- a/core/rpc-transport/package.json +++ b/core/rpc-transport/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-rpc-transport", - "version": "0.2.4", + "version": "0.3.0", "type": "module", "description": "RPC transport implementations", "license": "Apache-2.0", diff --git a/core/signing-blockdaemon/package.json b/core/signing-blockdaemon/package.json index 768304bc4..dc9bdd040 100644 --- a/core/signing-blockdaemon/package.json +++ b/core/signing-blockdaemon/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-signing-blockdaemon", - "version": "0.18.0", + "version": "0.19.0", "type": "module", "description": "Wallet Gateway signing driver for Blockdaemon", "license": "Apache-2.0", diff --git a/core/signing-fireblocks/package.json b/core/signing-fireblocks/package.json index b674ce3a0..346b8b47a 100644 --- a/core/signing-fireblocks/package.json +++ b/core/signing-fireblocks/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-signing-fireblocks", - "version": "0.18.3", + "version": "0.19.0", "type": "module", "description": "Wallet Gateway signing driver for Fireblocks", "license": "Apache-2.0", diff --git a/core/signing-internal/package.json b/core/signing-internal/package.json index 72a5bf6fd..d0706851d 100644 --- a/core/signing-internal/package.json +++ b/core/signing-internal/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-signing-internal", - "version": "0.20.2", + "version": "0.21.0", "type": "module", "description": "Wallet Gateway driver for offline Ed25519 keypairs", "license": "Apache-2.0", diff --git a/core/signing-lib/package.json b/core/signing-lib/package.json index 519e37fee..18e521121 100644 --- a/core/signing-lib/package.json +++ b/core/signing-lib/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-signing-lib", - "version": "0.20.3", + "version": "0.21.0", "type": "module", "description": "Core library for signing driver implementations", "license": "Apache-2.0", diff --git a/core/signing-participant/package.json b/core/signing-participant/package.json index ff890c435..3a895421c 100644 --- a/core/signing-participant/package.json +++ b/core/signing-participant/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-signing-participant", - "version": "0.20.2", + "version": "0.21.0", "packageManager": "yarn@4.9.4", "type": "module", "description": "Wallet Gateway driver for Canton participant internal parties", diff --git a/core/signing-store-sql/package.json b/core/signing-store-sql/package.json index 84a84904f..93383391f 100644 --- a/core/signing-store-sql/package.json +++ b/core/signing-store-sql/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-signing-store-sql", - "version": "0.21.2", + "version": "0.22.0", "type": "module", "description": "SQL implementation of the Store API", "license": "Apache-2.0", diff --git a/core/splice-client/package.json b/core/splice-client/package.json index 05a4b5597..daeb0dcab 100644 --- a/core/splice-client/package.json +++ b/core/splice-client/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-splice-client", - "version": "0.19.3", + "version": "0.20.0", "type": "module", "description": "Typescript Client for the multiple Canton Network APIs", "license": "Apache-2.0", diff --git a/core/splice-provider/package.json b/core/splice-provider/package.json index 04495fb76..ea68d55a3 100644 --- a/core/splice-provider/package.json +++ b/core/splice-provider/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-splice-provider", - "version": "0.21.0", + "version": "0.22.0", "type": "module", "description": "A JavaScript Splice Provider API (EIP-1193 compliant).", "license": "Apache-2.0", diff --git a/core/token-standard/package.json b/core/token-standard/package.json index c43017672..3f30766d2 100644 --- a/core/token-standard/package.json +++ b/core/token-standard/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-token-standard", - "version": "0.17.3", + "version": "0.18.0", "type": "module", "description": "daml codegen js for core token standard", "license": "Apache-2.0", diff --git a/core/tx-visualizer/package.json b/core/tx-visualizer/package.json index ebc7f45bc..c0c350934 100644 --- a/core/tx-visualizer/package.json +++ b/core/tx-visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-tx-visualizer", - "version": "0.17.3", + "version": "0.18.0", "type": "module", "description": "Decode and visualize prepared transactions from Canton", "license": "Apache-2.0", diff --git a/core/types/package.json b/core/types/package.json index d2ada881b..30e51cdea 100644 --- a/core/types/package.json +++ b/core/types/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-types", - "version": "0.16.3", + "version": "0.17.0", "type": "module", "description": "Types and transport-agnostic parsers for data sent across Wallet components.", "license": "Apache-2.0", diff --git a/core/wallet-auth/package.json b/core/wallet-auth/package.json index 19beb7f95..a52b67031 100644 --- a/core/wallet-auth/package.json +++ b/core/wallet-auth/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-wallet-auth", - "version": "0.17.3", + "version": "0.18.0", "type": "module", "description": "Provides authentication middleware and user management for the Wallet Gateway", "license": "Apache-2.0", diff --git a/core/wallet-dapp-remote-rpc-client/package.json b/core/wallet-dapp-remote-rpc-client/package.json index a87a29b47..a11589df2 100644 --- a/core/wallet-dapp-remote-rpc-client/package.json +++ b/core/wallet-dapp-remote-rpc-client/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-wallet-dapp-remote-rpc-client", - "version": "0.14.2", + "version": "0.15.0", "type": "module", "description": "TypeScript client generated by OpenRPC", "license": "Apache-2.0", diff --git a/core/wallet-dapp-rpc-client/package.json b/core/wallet-dapp-rpc-client/package.json index c1d048465..8ab0cf96a 100644 --- a/core/wallet-dapp-rpc-client/package.json +++ b/core/wallet-dapp-rpc-client/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-wallet-dapp-rpc-client", - "version": "0.20.2", + "version": "0.21.0", "type": "module", "description": "TypeScript client generated by OpenRPC", "license": "Apache-2.0", diff --git a/core/wallet-store-inmemory/package.json b/core/wallet-store-inmemory/package.json index e466f9273..bd66fdba1 100644 --- a/core/wallet-store-inmemory/package.json +++ b/core/wallet-store-inmemory/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-wallet-store-inmemory", - "version": "0.21.2", + "version": "0.22.0", "type": "module", "description": "In-memory implementation of the Store API", "author": "Marc Juchli ", diff --git a/core/wallet-store-sql/package.json b/core/wallet-store-sql/package.json index bc2a53ff0..db4a711a5 100644 --- a/core/wallet-store-sql/package.json +++ b/core/wallet-store-sql/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-wallet-store-sql", - "version": "0.20.2", + "version": "0.21.0", "type": "module", "description": "SQL implementation of the Store API", "license": "Apache-2.0", diff --git a/core/wallet-store/package.json b/core/wallet-store/package.json index 3fb822ffb..b27a8655d 100644 --- a/core/wallet-store/package.json +++ b/core/wallet-store/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-wallet-store", - "version": "0.20.2", + "version": "0.21.0", "type": "module", "description": "The Store API provides persistency for the Wallet Gateway", "license": "Apache-2.0", diff --git a/core/wallet-test-utils/package.json b/core/wallet-test-utils/package.json index b4abbc9c6..c8b340a3e 100644 --- a/core/wallet-test-utils/package.json +++ b/core/wallet-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@canton-network/core-wallet-test-utils", "private": true, - "version": "0.1.0", + "version": "0.2.0", "type": "module", "description": "Utilities for writing (E2E) tests for the wallet gateway", "main": "dist/index.cjs", diff --git a/core/wallet-ui-components/package.json b/core/wallet-ui-components/package.json index 18e85ef73..720bd14d7 100644 --- a/core/wallet-ui-components/package.json +++ b/core/wallet-ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-wallet-ui-components", - "version": "0.21.0", + "version": "0.22.0", "type": "module", "description": "Reusable UI components for the Splice wallet", "license": "Apache-2.0", diff --git a/core/wallet-user-rpc-client/package.json b/core/wallet-user-rpc-client/package.json index 127509923..1d35b964f 100644 --- a/core/wallet-user-rpc-client/package.json +++ b/core/wallet-user-rpc-client/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-wallet-user-rpc-client", - "version": "0.21.0", + "version": "0.22.0", "type": "module", "description": "TypeScript client generated by OpenRPC", "license": "Apache-2.0", diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index 294ff2879..2da605b1f 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -1,7 +1,6 @@ { "name": "@canton-network/example-portfolio", - "private": false, - "version": "0.0.0", + "version": "0.1.0", "type": "module", "scripts": { "dev": "vite",