From 4e56fe2ca830ab5e0420daaa60ae0f2b8af8790a Mon Sep 17 00:00:00 2001 From: Marc Juchli Date: Mon, 8 Jun 2026 11:36:27 +0200 Subject: [PATCH 1/5] fix: get signing key by pub key user check Signed-off-by: Marc Juchli --- core/signing-store-sql/src/store-sql.test.ts | 107 +++++++++++++++++++ core/signing-store-sql/src/store-sql.ts | 2 + 2 files changed, 109 insertions(+) create mode 100644 core/signing-store-sql/src/store-sql.test.ts diff --git a/core/signing-store-sql/src/store-sql.test.ts b/core/signing-store-sql/src/store-sql.test.ts new file mode 100644 index 000000000..7b87f43ea --- /dev/null +++ b/core/signing-store-sql/src/store-sql.test.ts @@ -0,0 +1,107 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, test, beforeEach } from 'vitest' +import { AuthContext } from '@canton-network/core-wallet-auth' +import { Kysely } from 'kysely' +import { pino } from 'pino' +import { migrator } from './migrator.js' +import { DB } from './schema.js' +import { connection, StoreSql } from './store-sql.js' + +const userA: AuthContext = { + userId: 'user-a', + accessToken: 'token-a', +} + +const userB: AuthContext = { + userId: 'user-b', + accessToken: 'token-b', +} + +describe('StoreSql auth scoping', () => { + let db: Kysely + + beforeEach(async () => { + db = connection({ connection: { type: 'memory' } }) + const umzug = migrator(db) + await umzug.up() + }) + + test('returns empty for getSigningKeyByPublicKey owned by another user', async () => { + const storeWithoutAuth = new StoreSql(db, pino({ level: 'silent' })) + await storeWithoutAuth.setSigningKey(userB.userId, { + id: 'key-b', + name: 'key-b', + publicKey: 'user-b-public-key', + privateKey: 'private-key-b', + createdAt: new Date(), + updatedAt: new Date(), + }) + + const scopedStore = storeWithoutAuth.withAuthContext(userA) + const key = + await scopedStore.getSigningKeyByPublicKey('user-b-public-key') + + expect(key).toBeUndefined() + }) + + test('scopes getSigningKeyByPublicKey to authContext user', async () => { + const storeWithoutAuth = new StoreSql(db, pino({ level: 'silent' })) + await storeWithoutAuth.setSigningKey(userA.userId, { + id: 'key-a', + name: 'key-a', + publicKey: 'shared-public-key', + privateKey: 'private-key-a', + createdAt: new Date(), + updatedAt: new Date(), + }) + await storeWithoutAuth.setSigningKey(userB.userId, { + id: 'key-b', + name: 'key-b', + publicKey: 'shared-public-key', + privateKey: 'private-key-b', + createdAt: new Date(), + updatedAt: new Date(), + }) + + const scopedStore = storeWithoutAuth.withAuthContext(userA) + const key = + await scopedStore.getSigningKeyByPublicKey('shared-public-key') + + expect(key?.id).toBe('key-a') + expect(key?.privateKey).toBe('private-key-a') + }) + + test('scopes listSigningTransactionsByTxIdsAndPublicKeys to authContext user', async () => { + const storeWithoutAuth = new StoreSql(db, pino({ level: 'silent' })) + const now = new Date() + + await storeWithoutAuth.setSigningTransaction(userA.userId, { + id: 'tx-a', + hash: 'hash-a', + publicKey: 'public-key-a', + status: 'signed', + createdAt: now, + updatedAt: now, + }) + await storeWithoutAuth.setSigningTransaction(userB.userId, { + id: 'tx-b', + hash: 'hash-b', + publicKey: 'public-key-b', + status: 'signed', + createdAt: now, + updatedAt: now, + }) + + const scopedStore = storeWithoutAuth.withAuthContext(userA) + const transactions = + await scopedStore.listSigningTransactionsByTxIdsAndPublicKeys( + ['tx-a', 'tx-b'], + [] + ) + + expect(transactions).toHaveLength(1) + expect(transactions[0]?.id).toBe('tx-a') + }) +}) diff --git a/core/signing-store-sql/src/store-sql.ts b/core/signing-store-sql/src/store-sql.ts index 18141865b..101b851a6 100644 --- a/core/signing-store-sql/src/store-sql.ts +++ b/core/signing-store-sql/src/store-sql.ts @@ -78,6 +78,7 @@ export class StoreSql implements SigningDriverStore, AuthAware { .selectFrom('signingKeys') .selectAll() .where('publicKey', '=', publicKey) + .where('userId', '=', this.assertConnected()) .executeTakeFirst() return result ? toSigningKey(result) : undefined } @@ -240,6 +241,7 @@ export class StoreSql implements SigningDriverStore, AuthAware { eb('id', 'in', txIds), ]) ) + .where('userId', '=', this.assertConnected()) .execute() return results.map(toSigningTransaction) From 56102a82e37e01a98399c779a1c087a189d51fe9 Mon Sep 17 00:00:00 2001 From: Marc Juchli Date: Mon, 8 Jun 2026 15:24:44 +0200 Subject: [PATCH 2/5] make internal driver auth aware Signed-off-by: Marc Juchli --- core/signing-internal/src/controller.ts | 29 ++--- core/wallet-auth/src/auth-utils.test.ts | 138 ++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 13 deletions(-) diff --git a/core/signing-internal/src/controller.ts b/core/signing-internal/src/controller.ts index cc4648149..89b9af557 100644 --- a/core/signing-internal/src/controller.ts +++ b/core/signing-internal/src/controller.ts @@ -36,7 +36,7 @@ import { SignMessageResult, } from '@canton-network/core-signing-lib' import { randomUUID } from 'node:crypto' -import { AuthContext } from '@canton-network/core-wallet-auth' +import { AuthAware, AuthContext } from '@canton-network/core-wallet-auth' interface InternalKey { id: string @@ -62,17 +62,19 @@ const convertInternalTransaction = (tx: InternalTransaction): Transaction => { } export class InternalSigningDriver implements SigningDriverInterface { - private store: SigningDriverStore - + private store: SigningDriverStore & AuthAware public partyMode = PartyMode.EXTERNAL public signingProvider = SigningProvider.WALLET_KERNEL - constructor(store: SigningDriverStore) { + constructor(store: SigningDriverStore & AuthAware) { this.store = store } - public controller = (_userId: AuthContext['userId'] | undefined) => - buildController({ + public controller = (_userId: AuthContext['userId'] | undefined) => { + const scopedStore = this.store.withAuthContext( + _userId ? { userId: _userId, accessToken: '' } : undefined + ) + return buildController({ signTransaction: async ( params: SignTransactionParams ): Promise => { @@ -86,7 +88,7 @@ export class InternalSigningDriver implements SigningDriverInterface { }) } - const key = await this.store.getSigningKeyByPublicKey( + const key = await scopedStore.getSigningKeyByPublicKey( params.keyIdentifier.publicKey ) @@ -109,7 +111,7 @@ export class InternalSigningDriver implements SigningDriverInterface { signedAt: now, } - this.store.setSigningTransaction( + scopedStore.setSigningTransaction( _userId, internalTransaction ) @@ -146,7 +148,7 @@ export class InternalSigningDriver implements SigningDriverInterface { }) } - const key = await this.store.getSigningKeyByPublicKey( + const key = await scopedStore.getSigningKeyByPublicKey( params.keyIdentifier.publicKey ) if (!key?.privateKey) { @@ -172,7 +174,7 @@ export class InternalSigningDriver implements SigningDriverInterface { }) } - const storedTx = await this.store.getSigningTransaction( + const storedTx = await scopedStore.getSigningTransaction( _userId, params.txId ) @@ -204,7 +206,7 @@ export class InternalSigningDriver implements SigningDriverInterface { if (params.publicKeys || params.txIds) { const transactions = - await this.store.listSigningTransactionsByTxIdsAndPublicKeys( + await scopedStore.listSigningTransactionsByTxIdsAndPublicKeys( params.txIds || [], params.publicKeys || [] ) @@ -237,7 +239,7 @@ export class InternalSigningDriver implements SigningDriverInterface { }) } - const keys = await this.store.listSigningKeys(_userId) + const keys = await scopedStore.listSigningKeys(_userId) if (keys.length > 0) { return Promise.resolve({ keys: Array.from(keys).map((key) => ({ @@ -276,7 +278,7 @@ export class InternalSigningDriver implements SigningDriverInterface { updatedAt: now, } - await this.store.setSigningKey(_userId, internalKey) + await scopedStore.setSigningKey(_userId, internalKey) return { id, @@ -299,4 +301,5 @@ export class InternalSigningDriver implements SigningDriverInterface { ): Promise => Promise.resolve({} as SubscribeTransactionsResult), }) + } } diff --git a/core/wallet-auth/src/auth-utils.test.ts b/core/wallet-auth/src/auth-utils.test.ts index 36f3bf0a8..3885fb856 100644 --- a/core/wallet-auth/src/auth-utils.test.ts +++ b/core/wallet-auth/src/auth-utils.test.ts @@ -16,7 +16,9 @@ import { assertConnected, fetchOidcUserInfo, jwtExpired, + resolveUserEmail, } from './auth-utils.js' +import { Idp } from './config/schema.js' import { TokenProviderConfig } from './auth-token-provider.js' import { Logger } from '@canton-network/core-types' import { SelfSignedTokenService } from './self-signed-token-service.js' @@ -162,4 +164,140 @@ describe('Auth Utils', () => { `Failed to fetch OIDC userinfo: 400 Bad request` ) }) + + describe('resolveUserEmail', () => { + const oauthIdp: Idp = { + id: 'oauth-idp', + type: 'oauth', + issuer: 'https://idp.example.com', + configUrl, + } + + const selfSignedIdp: Idp = { + id: 'self-signed-idp', + type: 'self_signed', + issuer: 'unsafe-auth', + } + + it('returns email from authContext when already set', async () => { + const email = await resolveUserEmail( + { + userId: 'user', + accessToken: 'token', + email: 'user@example.com', + }, + oauthIdp, + mockLogger + ) + + expect(email).toBe('user@example.com') + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('returns undefined for non-oauth idp', async () => { + const email = await resolveUserEmail( + { + userId: 'user', + accessToken: 'token', + }, + selfSignedIdp, + mockLogger + ) + + expect(email).toBeUndefined() + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('fetches email from OIDC userinfo for oauth idp', async () => { + fetchMock.mockImplementation((url) => { + if (url.includes('openid-configuration')) { + return Promise.resolve({ + ok: true, + json: async () => ({ + userinfo_endpoint: 'https://userinfo', + }), + }) + } + if (url.includes('userinfo')) { + return Promise.resolve({ + ok: true, + json: async () => ({ + sub: 'user', + email: 'fetched@example.com', + }), + }) + } + + return Promise.reject('') + }) + + const email = await resolveUserEmail( + { + userId: 'user', + accessToken: 'access-token', + }, + oauthIdp, + mockLogger + ) + + expect(email).toBe('fetched@example.com') + }) + + it('returns undefined when userinfo has no email', async () => { + fetchMock.mockImplementation((url) => { + if (url.includes('openid-configuration')) { + return Promise.resolve({ + ok: true, + json: async () => ({ + userinfo_endpoint: 'https://userinfo', + }), + }) + } + if (url.includes('userinfo')) { + return Promise.resolve({ + ok: true, + json: async () => ({ sub: 'user' }), + }) + } + + return Promise.reject('') + }) + + const email = await resolveUserEmail( + { + userId: 'user', + accessToken: 'access-token', + }, + oauthIdp, + mockLogger + ) + + expect(email).toBeUndefined() + }) + + it('returns undefined and logs when userinfo fetch fails', async () => { + fetchMock.mockImplementation(() => + Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + }) + ) + + const email = await resolveUserEmail( + { + userId: 'user', + accessToken: 'access-token', + }, + oauthIdp, + mockLogger + ) + + expect(email).toBeUndefined() + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.any(Error), + 'Failed to resolve user email from OIDC userinfo' + ) + }) + }) }) From 6a525711a3ae719da70b22e491f565421107074a Mon Sep 17 00:00:00 2001 From: Marc Juchli Date: Mon, 8 Jun 2026 15:45:10 +0200 Subject: [PATCH 3/5] revert Signed-off-by: Marc Juchli --- core/wallet-auth/src/auth-utils.test.ts | 138 ------------------------ 1 file changed, 138 deletions(-) diff --git a/core/wallet-auth/src/auth-utils.test.ts b/core/wallet-auth/src/auth-utils.test.ts index 3885fb856..36f3bf0a8 100644 --- a/core/wallet-auth/src/auth-utils.test.ts +++ b/core/wallet-auth/src/auth-utils.test.ts @@ -16,9 +16,7 @@ import { assertConnected, fetchOidcUserInfo, jwtExpired, - resolveUserEmail, } from './auth-utils.js' -import { Idp } from './config/schema.js' import { TokenProviderConfig } from './auth-token-provider.js' import { Logger } from '@canton-network/core-types' import { SelfSignedTokenService } from './self-signed-token-service.js' @@ -164,140 +162,4 @@ describe('Auth Utils', () => { `Failed to fetch OIDC userinfo: 400 Bad request` ) }) - - describe('resolveUserEmail', () => { - const oauthIdp: Idp = { - id: 'oauth-idp', - type: 'oauth', - issuer: 'https://idp.example.com', - configUrl, - } - - const selfSignedIdp: Idp = { - id: 'self-signed-idp', - type: 'self_signed', - issuer: 'unsafe-auth', - } - - it('returns email from authContext when already set', async () => { - const email = await resolveUserEmail( - { - userId: 'user', - accessToken: 'token', - email: 'user@example.com', - }, - oauthIdp, - mockLogger - ) - - expect(email).toBe('user@example.com') - expect(fetchMock).not.toHaveBeenCalled() - }) - - it('returns undefined for non-oauth idp', async () => { - const email = await resolveUserEmail( - { - userId: 'user', - accessToken: 'token', - }, - selfSignedIdp, - mockLogger - ) - - expect(email).toBeUndefined() - expect(fetchMock).not.toHaveBeenCalled() - }) - - it('fetches email from OIDC userinfo for oauth idp', async () => { - fetchMock.mockImplementation((url) => { - if (url.includes('openid-configuration')) { - return Promise.resolve({ - ok: true, - json: async () => ({ - userinfo_endpoint: 'https://userinfo', - }), - }) - } - if (url.includes('userinfo')) { - return Promise.resolve({ - ok: true, - json: async () => ({ - sub: 'user', - email: 'fetched@example.com', - }), - }) - } - - return Promise.reject('') - }) - - const email = await resolveUserEmail( - { - userId: 'user', - accessToken: 'access-token', - }, - oauthIdp, - mockLogger - ) - - expect(email).toBe('fetched@example.com') - }) - - it('returns undefined when userinfo has no email', async () => { - fetchMock.mockImplementation((url) => { - if (url.includes('openid-configuration')) { - return Promise.resolve({ - ok: true, - json: async () => ({ - userinfo_endpoint: 'https://userinfo', - }), - }) - } - if (url.includes('userinfo')) { - return Promise.resolve({ - ok: true, - json: async () => ({ sub: 'user' }), - }) - } - - return Promise.reject('') - }) - - const email = await resolveUserEmail( - { - userId: 'user', - accessToken: 'access-token', - }, - oauthIdp, - mockLogger - ) - - expect(email).toBeUndefined() - }) - - it('returns undefined and logs when userinfo fetch fails', async () => { - fetchMock.mockImplementation(() => - Promise.resolve({ - ok: false, - status: 500, - statusText: 'Internal Server Error', - }) - ) - - const email = await resolveUserEmail( - { - userId: 'user', - accessToken: 'access-token', - }, - oauthIdp, - mockLogger - ) - - expect(email).toBeUndefined() - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.any(Error), - 'Failed to resolve user email from OIDC userinfo' - ) - }) - }) }) From d804e8f9b7afc8996c2dc3efed574fa79cdc71a6 Mon Sep 17 00:00:00 2001 From: Marc Juchli Date: Mon, 8 Jun 2026 16:29:16 +0200 Subject: [PATCH 4/5] fix tests Signed-off-by: Marc Juchli --- core/signing-blockdaemon/src/index.ts | 10 +- core/signing-dfns/src/index.ts | 2 +- core/signing-fireblocks/src/index.ts | 12 +- core/signing-internal/src/controller.ts | 7 +- core/signing-lib/src/index.ts | 2 +- core/signing-participant/src/controller.ts | 2 +- .../src/ledger/party-allocation-service.ts | 6 +- .../src/ledger/transaction-service.test.ts | 26 ++-- .../remote/src/ledger/transaction-service.ts | 25 +-- .../blockdaemon-wallet-allocator.ts | 18 +-- .../dfns-wallet-allocator.ts | 16 +- .../fireblocks-wallet-allocator.ts | 20 ++- .../kernel-wallet-allocator.ts | 16 +- .../participant-wallet-allocator.ts | 15 +- .../wallet-allocation-service.test.ts | 142 +++++++----------- .../wallet-allocation-service.ts | 48 +++--- .../src/ledger/wallet-sync-service.test.ts | 2 +- .../remote/src/ledger/wallet-sync-service.ts | 3 +- .../remote/src/user-api/controller.ts | 22 +-- 19 files changed, 173 insertions(+), 221 deletions(-) diff --git a/core/signing-blockdaemon/src/index.ts b/core/signing-blockdaemon/src/index.ts index 346a35bf7..37f9590cb 100644 --- a/core/signing-blockdaemon/src/index.ts +++ b/core/signing-blockdaemon/src/index.ts @@ -47,7 +47,7 @@ export default class BlockdaemonSigningDriver implements SigningDriverInterface public partyMode = PartyMode.EXTERNAL public signingProvider = SigningProvider.BLOCKDAEMON - public controller = (userId: AuthContext['userId'] | undefined) => + public controller = (authContext: AuthContext | undefined) => buildController({ signTransaction: async ( params: SignTransactionParams @@ -67,7 +67,7 @@ export default class BlockdaemonSigningDriver implements SigningDriverInterface ...(params.internalTxId !== undefined && { internalTxId: params.internalTxId, }), - userIdentifier: userId, + userIdentifier: authContext?.email, }) return { txId: tx.txId, @@ -135,7 +135,7 @@ export default class BlockdaemonSigningDriver implements SigningDriverInterface const transactions = await this.client.getTransactions({ txIds: params.txIds!, publicKeys: params.publicKeys!, - userIdentifier: userId, + userIdentifier: authContext?.email, }) return { transactions: transactions.map((tx) => ({ @@ -175,7 +175,7 @@ export default class BlockdaemonSigningDriver implements SigningDriverInterface id: k.id, name: k.name, publicKey: k.publicKey, - userIdentifier: userId, + userIdentifier: authContext?.email, })), } } catch (error) { @@ -192,7 +192,7 @@ export default class BlockdaemonSigningDriver implements SigningDriverInterface try { const key = await this.client.createKey({ name: params.name, - userIdentifier: userId, + userIdentifier: authContext?.email, }) return { id: key.id, diff --git a/core/signing-dfns/src/index.ts b/core/signing-dfns/src/index.ts index 934291e2e..afc40b108 100644 --- a/core/signing-dfns/src/index.ts +++ b/core/signing-dfns/src/index.ts @@ -90,7 +90,7 @@ export default class DfnsSigningDriver implements SigningDriverInterface { public controller = ( // eslint-disable-next-line @typescript-eslint/no-unused-vars - _userId: AuthContext['userId'] | undefined + authContext: AuthContext | undefined ) => buildController({ signTransaction: async ( diff --git a/core/signing-fireblocks/src/index.ts b/core/signing-fireblocks/src/index.ts index 666775776..fe4ea30d3 100644 --- a/core/signing-fireblocks/src/index.ts +++ b/core/signing-fireblocks/src/index.ts @@ -75,7 +75,7 @@ export default class FireblocksSigningDriver implements SigningDriverInterface { } public partyMode = PartyMode.EXTERNAL public signingProvider = SigningProvider.FIREBLOCKS - public controller = (userId: AuthContext['userId'] | undefined) => + public controller = (authContext: AuthContext | undefined) => buildController({ signTransaction: async ( params: SignTransactionParams @@ -84,7 +84,7 @@ export default class FireblocksSigningDriver implements SigningDriverInterface { try { const tx = await this.fireblocks.signTransaction( - userId, + authContext?.userId, params.txHash, params.keyIdentifier, params.internalTxId @@ -117,7 +117,7 @@ export default class FireblocksSigningDriver implements SigningDriverInterface { params: GetTransactionParams ): Promise => { const tx = await this.fireblocks.getTransaction( - userId, + authContext?.userId, params.txId ) if (tx) { @@ -144,7 +144,7 @@ export default class FireblocksSigningDriver implements SigningDriverInterface { const txIds = new Set(params.txIds) const publicKeys = new Set(params.publicKeys) for await (const tx of this.fireblocks.getTransactions( - userId + authContext?.userId )) { if ( txIds.has(tx.txId) || @@ -180,7 +180,9 @@ export default class FireblocksSigningDriver implements SigningDriverInterface { getKeys: async (): Promise => { try { - const keys = await this.fireblocks.getPublicKeys(userId) + const keys = await this.fireblocks.getPublicKeys( + authContext?.userId + ) return { keys: keys.map((k) => ({ id: k.derivationPath.join('-'), diff --git a/core/signing-internal/src/controller.ts b/core/signing-internal/src/controller.ts index 89b9af557..aa90a99d0 100644 --- a/core/signing-internal/src/controller.ts +++ b/core/signing-internal/src/controller.ts @@ -70,10 +70,9 @@ export class InternalSigningDriver implements SigningDriverInterface { this.store = store } - public controller = (_userId: AuthContext['userId'] | undefined) => { - const scopedStore = this.store.withAuthContext( - _userId ? { userId: _userId, accessToken: '' } : undefined - ) + public controller = (authContext: AuthContext | undefined) => { + const scopedStore = this.store.withAuthContext(authContext) + const _userId = authContext?.userId return buildController({ signTransaction: async ( params: SignTransactionParams diff --git a/core/signing-lib/src/index.ts b/core/signing-lib/src/index.ts index c8ee9a6df..9d6a61c15 100644 --- a/core/signing-lib/src/index.ts +++ b/core/signing-lib/src/index.ts @@ -38,7 +38,7 @@ export interface KeyPair { export interface SigningDriverInterface { partyMode: PartyMode signingProvider: SigningProvider - controller: (userId: AuthContext['userId'] | undefined) => Methods + controller: (authContext: AuthContext | undefined) => Methods } export const verifySignedTxHash = ( diff --git a/core/signing-participant/src/controller.ts b/core/signing-participant/src/controller.ts index 07eeebe27..8f1bf0a21 100644 --- a/core/signing-participant/src/controller.ts +++ b/core/signing-participant/src/controller.ts @@ -25,7 +25,7 @@ export class ParticipantSigningDriver implements SigningDriverInterface { public signingProvider = SigningProvider.PARTICIPANT public controller = ( - _userId: AuthContext['userId'] | undefined // eslint-disable-line @typescript-eslint/no-unused-vars + authContext: AuthContext | undefined // eslint-disable-line @typescript-eslint/no-unused-vars ) => buildController({ signTransaction: async ( diff --git a/wallet-gateway/remote/src/ledger/party-allocation-service.ts b/wallet-gateway/remote/src/ledger/party-allocation-service.ts index 61487232d..4e549e115 100644 --- a/wallet-gateway/remote/src/ledger/party-allocation-service.ts +++ b/wallet-gateway/remote/src/ledger/party-allocation-service.ts @@ -6,7 +6,7 @@ import { LedgerClient, } from '@canton-network/core-ledger-client' import { createHash } from 'node:crypto' -import { AccessTokenProvider } from '@canton-network/core-wallet-auth' +import { AccessTokenProvider, UserId } from '@canton-network/core-wallet-auth' import { Logger } from 'pino' export type AllocatedParty = { @@ -165,14 +165,14 @@ export class PartyAllocationService { namespace: string, transactions: string[], signature: string, - userId: string + userId: UserId ): Promise async allocatePartyWithExistingWallet( namespace: string, transactions: string[], signature: string, - userId: string + userId: UserId ): Promise { const synchronizerId = this.synchronizerId ?? (await this.ledgerClient.getSynchronizerId()) diff --git a/wallet-gateway/remote/src/ledger/transaction-service.test.ts b/wallet-gateway/remote/src/ledger/transaction-service.test.ts index 008b1634a..a90c3735a 100644 --- a/wallet-gateway/remote/src/ledger/transaction-service.test.ts +++ b/wallet-gateway/remote/src/ledger/transaction-service.test.ts @@ -18,8 +18,14 @@ import { } from '@canton-network/core-signing-lib' import type { Notifier } from '../notification/NotificationService.js' import { TransactionService } from './transaction-service.js' +import { AuthContext } from '@canton-network/core-wallet-auth' const userId = 'user-1' +const authContext: AuthContext = { + userId, + accessToken: 'access-token', + email: 'user@example.com', +} const wallet: Wallet = { primary: true, @@ -162,7 +168,7 @@ describe('TransactionService', () => { ) const result = await service.signWithWalletKernel( - userId, + authContext, wallet, signParams ) @@ -195,7 +201,7 @@ describe('TransactionService', () => { const service = createService(createStore(), {}, notifier, logger) await expect( - service.signWithWalletKernel(userId, wallet, signParams) + service.signWithWalletKernel(authContext, wallet, signParams) ).rejects.toThrow('Wallet Gateway signing driver not available') }) @@ -212,7 +218,7 @@ describe('TransactionService', () => { ) await expect( - service.signWithWalletKernel(userId, wallet, signParams) + service.signWithWalletKernel(authContext, wallet, signParams) ).rejects.toThrow('Transaction not found with id: tx-1') }) @@ -233,7 +239,7 @@ describe('TransactionService', () => { ) await expect( - service.signWithWalletKernel(userId, wallet, signParams) + service.signWithWalletKernel(authContext, wallet, signParams) ).rejects.toThrow('Error from signing driver: Signing rejected') }) }) @@ -257,7 +263,7 @@ describe('TransactionService', () => { ) const result = await service.signWithBlockdaemon( - userId, + authContext, wallet, signParams ) @@ -302,13 +308,13 @@ describe('TransactionService', () => { ) const result = await service.signWithBlockdaemon( - userId, + authContext, wallet, signParams ) expect(getTransaction).toHaveBeenCalledWith({ - userId, + userId: authContext.userId, txId: 'external-tx-1', }) expect(store.setTransactionSigned).toHaveBeenCalledWith( @@ -347,14 +353,14 @@ describe('TransactionService', () => { ) const result = await service.signWithFireblocks( - userId, + authContext, wallet, signParams ) expect(signTransaction).toHaveBeenCalledWith( expect.objectContaining({ - userId, + userId: authContext.userId, txHash: Buffer.from( pendingTransaction.preparedTransactionHash, 'base64' @@ -389,7 +395,7 @@ describe('TransactionService', () => { ) const result = await service.signWithDfns( - userId, + authContext, wallet, signParams ) diff --git a/wallet-gateway/remote/src/ledger/transaction-service.ts b/wallet-gateway/remote/src/ledger/transaction-service.ts index cf3c45c4b..968551d23 100644 --- a/wallet-gateway/remote/src/ledger/transaction-service.ts +++ b/wallet-gateway/remote/src/ledger/transaction-service.ts @@ -26,6 +26,7 @@ import { import { UserId } from '../dapp-api/rpc-gen/typings.js' import { Notifier } from '../notification/NotificationService.js' import { ledgerPrepareParams, type PrepareParams } from '../utils.js' +import { AuthContext } from '@canton-network/core-wallet-auth' function handleSigningError(result: SigningError | T): T { if ('error' in result) { @@ -67,7 +68,7 @@ export class TransactionService { } public async signWithWalletKernel( - userId: UserId, + authContext: AuthContext, wallet: Wallet, signParams: SignParams ): Promise { @@ -76,7 +77,7 @@ export class TransactionService { if (!signingProvider) { throw new Error('Wallet Gateway signing driver not available') } - const driver = signingProvider.controller(userId) + const driver = signingProvider.controller(authContext) const tx = await this.loadPreparedTransactionForSigning( signParams.transactionId @@ -124,7 +125,7 @@ export class TransactionService { } public async signWithBlockdaemon( - userId: UserId, + authContext: AuthContext, wallet: Wallet, signParams: SignParams ): Promise { @@ -132,7 +133,7 @@ export class TransactionService { if (!signingProvider) { throw new Error('Blockdaemon signing driver not available') } - const driver = signingProvider.controller(userId) + const driver = signingProvider.controller(authContext) const tx = await this.loadPreparedTransactionForSigning( signParams.transactionId @@ -145,7 +146,7 @@ export class TransactionService { if (tx && tx.externalTxId) { signingResult = await driver .getTransaction({ - userId, + userId: authContext.userId, txId: tx.externalTxId, }) .then(handleSigningError) @@ -232,7 +233,7 @@ export class TransactionService { } public async signWithFireblocks( - userId: UserId, + authContext: AuthContext, wallet: Wallet, signParams: SignParams ): Promise { @@ -240,7 +241,7 @@ export class TransactionService { if (!signingProvider) { throw new Error('Fireblocks signing driver not available') } - const driver = signingProvider.controller(userId) + const driver = signingProvider.controller(authContext) const tx = await this.loadPreparedTransactionForSigning( signParams.transactionId @@ -253,14 +254,14 @@ export class TransactionService { if (tx && tx.externalTxId) { signingResult = await driver .getTransaction({ - userId, + userId: authContext.userId, txId: tx.externalTxId, }) .then(handleSigningError) } else { signingResult = await driver .signTransaction({ - userId, + userId: authContext.userId, tx: tx.preparedTransaction, txHash: Buffer.from( tx.preparedTransactionHash, @@ -350,7 +351,7 @@ export class TransactionService { * the same SignResult shape the other external providers use. */ public async signWithDfns( - userId: UserId, + authContext: AuthContext, wallet: Wallet, signParams: SignParams ): Promise { @@ -358,7 +359,7 @@ export class TransactionService { if (!signingProvider) { throw new Error('Dfns signing driver not available') } - const driver = signingProvider.controller(userId) + const driver = signingProvider.controller(authContext) const tx = await this.loadPreparedTransactionForSigning( signParams.transactionId @@ -371,7 +372,7 @@ export class TransactionService { if (tx.externalTxId) { signingResult = await driver .getTransaction({ - userId, + userId: authContext.userId, txId: tx.externalTxId, }) .then(handleSigningError) diff --git a/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/blockdaemon-wallet-allocator.ts b/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/blockdaemon-wallet-allocator.ts index a8939da37..bac8f7f2e 100644 --- a/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/blockdaemon-wallet-allocator.ts +++ b/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/blockdaemon-wallet-allocator.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { UserId } from '@canton-network/core-wallet-auth' +import { AuthContext } from '@canton-network/core-wallet-auth' import { Store, UpdateWallet, Wallet } from '@canton-network/core-wallet-store' import { Error as SigningError, @@ -32,12 +32,11 @@ export class BlockdaemonWalletAllocator implements WalletAllocator { ) {} async createWallet( - userId: UserId, - email: string | undefined, + authContext: AuthContext, partyHint: PartyHint, primary: Primary = false ): Promise { - const driver = this.signingDriver.controller(email) + const driver = this.signingDriver.controller(authContext) const key = await driver.createKey({ name: partyHint, @@ -96,7 +95,7 @@ export class BlockdaemonWalletAllocator implements WalletAllocator { if (status === 'signed') { const { signature } = await driver .getTransaction({ - userId, + userId: authContext.userId, txId, }) .then(handleSigningError) @@ -110,7 +109,7 @@ export class BlockdaemonWalletAllocator implements WalletAllocator { namespace, topologyTransactions, signature, - userId + authContext.userId ) wallet = { ...walletBase, @@ -141,8 +140,7 @@ export class BlockdaemonWalletAllocator implements WalletAllocator { } async allocateParty( - userId: UserId, - email: string | undefined, + authContext: AuthContext, existingWallet: Wallet ): Promise { if ( @@ -153,7 +151,7 @@ export class BlockdaemonWalletAllocator implements WalletAllocator { 'Existing wallet is missing field externalTxId or topologyTransactions' ) } - const driver = this.signingDriver.controller(email) + const driver = this.signingDriver.controller(authContext) const { signature, status, metadata } = await driver .getTransaction({ @@ -176,7 +174,7 @@ export class BlockdaemonWalletAllocator implements WalletAllocator { existingWallet.namespace, existingWallet.topologyTransactions.split(', '), signature, - userId + authContext.userId ) walletUpdate = { ...walletUpdate, diff --git a/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/dfns-wallet-allocator.ts b/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/dfns-wallet-allocator.ts index 06d89c5cc..154b7b4aa 100644 --- a/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/dfns-wallet-allocator.ts +++ b/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/dfns-wallet-allocator.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { UserId } from '@canton-network/core-wallet-auth' +import { AuthContext, UserId } from '@canton-network/core-wallet-auth' import { Store, UpdateWallet, Wallet } from '@canton-network/core-wallet-store' import { Error as SigningError, @@ -37,12 +37,11 @@ export class DfnsWalletAllocator implements WalletAllocator { ) {} async createWallet( - userId: UserId, - _email: string | undefined, + authContext: AuthContext, partyHint: PartyHint, primary: Primary = false ): Promise { - const driver = this.signingDriver.controller(userId) + const driver = this.signingDriver.controller(authContext) const key = await driver .createKey({ name: partyHint }) @@ -89,7 +88,7 @@ export class DfnsWalletAllocator implements WalletAllocator { const wallet = await this.finalizeWallet( walletBase, - userId, + authContext.userId, status, txId, topologyTransactions, @@ -102,8 +101,7 @@ export class DfnsWalletAllocator implements WalletAllocator { } async allocateParty( - userId: UserId, - _email: string | undefined, + authContext: AuthContext, existingWallet: Wallet ): Promise { if ( @@ -114,7 +112,7 @@ export class DfnsWalletAllocator implements WalletAllocator { 'Existing wallet is missing field externalTxId or topologyTransactions' ) } - const driver = this.signingDriver.controller(userId) + const driver = this.signingDriver.controller(authContext) const { signature, status, metadata } = await driver .getTransaction({ txId: existingWallet.externalTxId }) @@ -135,7 +133,7 @@ export class DfnsWalletAllocator implements WalletAllocator { existingWallet.namespace, existingWallet.topologyTransactions.split(', '), signature, - userId + authContext.userId ) walletUpdate = { ...walletUpdate, diff --git a/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.ts b/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.ts index 14f671181..ec6071a0e 100644 --- a/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.ts +++ b/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { UserId } from '@canton-network/core-wallet-auth' +import { AuthContext } from '@canton-network/core-wallet-auth' import { Store, UpdateWallet, Wallet } from '@canton-network/core-wallet-store' import { Error as SigningError, @@ -32,12 +32,11 @@ export class FireblocksWalletAllocator implements WalletAllocator { ) {} async createWallet( - userId: UserId, - email: string | undefined, + authContext: AuthContext, partyHint: PartyHint, primary: Primary = false ): Promise { - const driver = this.signingDriver.controller(userId) + const driver = this.signingDriver.controller(authContext) const keys = await driver.getKeys().then(handleSigningError) const key = keys?.keys?.find((k) => k.name === 'Canton Party') @@ -85,7 +84,7 @@ export class FireblocksWalletAllocator implements WalletAllocator { if (status === 'signed') { const { signature } = await driver .getTransaction({ - userId, + userId: authContext.userId, txId, }) .then(handleSigningError) @@ -99,7 +98,7 @@ export class FireblocksWalletAllocator implements WalletAllocator { namespace, topologyTransactions, Buffer.from(signature, 'hex').toString('base64'), - userId + authContext.userId ) wallet = { ...walletBase, @@ -130,8 +129,7 @@ export class FireblocksWalletAllocator implements WalletAllocator { } async allocateParty( - userId: UserId, - email: string | undefined, + authContext: AuthContext, existingWallet: Wallet ): Promise { if ( @@ -143,14 +141,14 @@ export class FireblocksWalletAllocator implements WalletAllocator { ) } - const driver = this.signingDriver.controller(userId) + const driver = this.signingDriver.controller(authContext) const keys = await driver.getKeys().then(handleSigningError) const key = keys?.keys?.find((k) => k.name === 'Canton Party') if (!key) throw new Error('Fireblocks key not found') const { signature, status } = await driver .getTransaction({ - userId, + userId: authContext.userId, txId: existingWallet.externalTxId, }) .then(handleSigningError) @@ -170,7 +168,7 @@ export class FireblocksWalletAllocator implements WalletAllocator { existingWallet.namespace, existingWallet.topologyTransactions.split(', '), Buffer.from(signature, 'hex').toString('base64'), - userId + authContext.userId ) walletUpdate = { ...walletUpdate, diff --git a/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/kernel-wallet-allocator.ts b/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/kernel-wallet-allocator.ts index 88ac44b5d..9ab2203b0 100644 --- a/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/kernel-wallet-allocator.ts +++ b/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/kernel-wallet-allocator.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { UserId } from '@canton-network/core-wallet-auth' +import { AuthContext } from '@canton-network/core-wallet-auth' import { Store, UpdateWallet, Wallet } from '@canton-network/core-wallet-store' import { Error as SigningError, @@ -30,12 +30,11 @@ export class KernelWalletAllocator implements WalletAllocator { ) {} async createWallet( - userId: UserId, - email: string | undefined, + authContext: AuthContext, partyHint: PartyHint, primary: Primary = false ): Promise { - const driver = this.signingDriver.controller(userId) + const driver = this.signingDriver.controller(authContext) const key = await driver .createKey({ name: partyHint, @@ -43,7 +42,7 @@ export class KernelWalletAllocator implements WalletAllocator { .then(handleSigningError) const party = await this.partyAllocator.allocateParty( - userId, + authContext.userId, partyHint, key.publicKey, async (hash) => { @@ -84,11 +83,10 @@ export class KernelWalletAllocator implements WalletAllocator { } async allocateParty( - userId: UserId, - email: string | undefined, + authContext: AuthContext, existingWallet: Wallet ): Promise { - const driver = this.signingDriver.controller(userId) + const driver = this.signingDriver.controller(authContext) const signingCallback = async (hash: string) => { const result = await driver .signTransaction({ @@ -105,7 +103,7 @@ export class KernelWalletAllocator implements WalletAllocator { } const party = await this.partyAllocator.allocateParty( - userId, + authContext.userId, existingWallet.hint, existingWallet.publicKey, signingCallback diff --git a/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/participant-wallet-allocator.ts b/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/participant-wallet-allocator.ts index a623e7378..4518ea48f 100644 --- a/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/participant-wallet-allocator.ts +++ b/wallet-gateway/remote/src/ledger/wallet-allocation/signing-providers/participant-wallet-allocator.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { UserId } from '@canton-network/core-wallet-auth' +import { AuthContext } from '@canton-network/core-wallet-auth' import { Store, Wallet } from '@canton-network/core-wallet-store' import { SigningProvider } from '@canton-network/core-signing-lib' import { Logger } from 'pino' @@ -17,12 +17,14 @@ export class ParticipantWalletAllocator implements WalletAllocator { ) {} async createWallet( - userId: UserId, - email: string | undefined, + authContext: AuthContext, partyHint: PartyHint, primary: Primary = false ): Promise { - const party = await this.partyAllocator.allocateParty(userId, partyHint) + const party = await this.partyAllocator.allocateParty( + authContext.userId, + partyHint + ) const network = await this.store.getCurrentNetwork() const wallet: Wallet = { partyId: party.partyId, @@ -42,12 +44,11 @@ export class ParticipantWalletAllocator implements WalletAllocator { } async allocateParty( - userId: UserId, - email: string | undefined, + authContext: AuthContext, existingWallet: Wallet ): Promise { const party = await this.partyAllocator.allocateParty( - userId, + authContext.userId, existingWallet.hint ) const network = await this.store.getCurrentNetwork() diff --git a/wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.test.ts b/wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.test.ts index abd368089..b23e43ad2 100644 --- a/wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.test.ts +++ b/wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.test.ts @@ -20,6 +20,13 @@ import { SigningProvider } from '@canton-network/core-signing-lib' import type { SigningDriverInterface } from '@canton-network/core-signing-lib' import type { AllocatedParty } from '../party-allocation-service.js' import { WALLET_DISABLED_REASON } from '@canton-network/core-types' +import { AuthContext } from '@canton-network/core-wallet-auth' + +const authContext: AuthContext = { + userId: 'user-1', + accessToken: 'access-token', + email: 'user-1@example.com', +} const createWallet = ( partyId: string, @@ -315,8 +322,7 @@ describe('WalletAllocationService', () => { mockPartyAllocator.allocateParty.mockResolvedValue(expectedParty) const result = await service.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.PARTICIPANT @@ -351,14 +357,13 @@ describe('WalletAllocationService', () => { }) await service.allocateParty( - 'user-1', - undefined, + authContext, existingWallet, SigningProvider.PARTICIPANT ) expect(mockPartyAllocator.allocateParty).toHaveBeenCalledWith( - 'user-1', + authContext.userId, 'alice' ) expect(mockStore.updateWallet).toHaveBeenCalledWith({ @@ -382,8 +387,7 @@ describe('WalletAllocationService', () => { mockPartyAllocator.allocateParty.mockResolvedValue(expectedParty) const result = await service.createWallet( - 'user-1', - undefined, + authContext, 'bob', false, SigningProvider.WALLET_KERNEL @@ -404,7 +408,7 @@ describe('WalletAllocationService', () => { controller: Mock } ).controller - ).toHaveBeenCalledWith('user-1') + ).toHaveBeenCalledWith(authContext) expect(mockController.createKey).toHaveBeenCalledWith({ name: 'bob', }) @@ -431,8 +435,7 @@ describe('WalletAllocationService', () => { }) await service.allocateParty( - 'user-1', - undefined, + authContext, existingWallet, SigningProvider.WALLET_KERNEL ) @@ -455,8 +458,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithoutDriver.createWallet( - 'user-1', - undefined, + authContext, 'bob', false, SigningProvider.WALLET_KERNEL @@ -472,8 +474,7 @@ describe('WalletAllocationService', () => { await expect( service.createWallet( - 'user-1', - undefined, + authContext, 'bob', false, SigningProvider.WALLET_KERNEL @@ -504,8 +505,7 @@ describe('WalletAllocationService', () => { await expect( service.createWallet( - 'user-1', - undefined, + authContext, 'bob', false, SigningProvider.WALLET_KERNEL @@ -535,8 +535,7 @@ describe('WalletAllocationService', () => { await expect( service.createWallet( - 'user-1', - undefined, + authContext, 'bob', false, SigningProvider.WALLET_KERNEL @@ -571,8 +570,7 @@ describe('WalletAllocationService', () => { await expect( service.allocateParty( - 'user-1', - undefined, + authContext, existingWallet, SigningProvider.WALLET_KERNEL ) @@ -586,8 +584,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithoutFireblocks.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.FIREBLOCKS @@ -611,8 +608,7 @@ describe('WalletAllocationService', () => { ) const result = await serviceWithFireblocks.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.FIREBLOCKS @@ -638,8 +634,7 @@ describe('WalletAllocationService', () => { }) const result = await serviceWithFireblocks.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.FIREBLOCKS @@ -669,8 +664,7 @@ describe('WalletAllocationService', () => { }) const result = await serviceWithFireblocks.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.FIREBLOCKS @@ -697,8 +691,7 @@ describe('WalletAllocationService', () => { }) const result = await serviceWithFireblocks.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.FIREBLOCKS @@ -719,8 +712,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithFireblocks.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.FIREBLOCKS @@ -737,8 +729,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithFireblocks.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.FIREBLOCKS @@ -759,8 +750,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithFireblocks.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.FIREBLOCKS @@ -777,8 +767,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithFireblocks.allocateParty( - 'user-1', - undefined, + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.FIREBLOCKS, topologyTransactions: undefined, @@ -799,8 +788,7 @@ describe('WalletAllocationService', () => { }) await serviceWithFireblocks.allocateParty( - 'user-1', - undefined, + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.FIREBLOCKS, topologyTransactions: 'tx1', @@ -833,8 +821,7 @@ describe('WalletAllocationService', () => { ) await serviceWithFireblocks.allocateParty( - 'user-1', - undefined, + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.FIREBLOCKS, namespace: 'fingerprint', @@ -868,8 +855,7 @@ describe('WalletAllocationService', () => { }) await serviceWithFireblocks.allocateParty( - 'user-1', - undefined, + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.FIREBLOCKS, topologyTransactions: 'tx1', @@ -894,8 +880,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithoutBlockdaemon.createWallet( - 'user-1', - 'user-1@example.com', + authContext, 'alice', false, SigningProvider.BLOCKDAEMON @@ -911,8 +896,7 @@ describe('WalletAllocationService', () => { }) const result = await serviceWithBlockdaemon.createWallet( - 'user-1', - 'user-1@example.com', + authContext, 'alice', false, SigningProvider.BLOCKDAEMON @@ -946,8 +930,7 @@ describe('WalletAllocationService', () => { ) const result = await serviceWithBlockdaemon.createWallet( - 'user-1', - 'user-1@example.com', + authContext, 'alice', false, SigningProvider.BLOCKDAEMON @@ -973,8 +956,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithBlockdaemon.createWallet( - 'user-1', - 'user-1@example.com', + authContext, 'alice', false, SigningProvider.BLOCKDAEMON @@ -995,8 +977,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithBlockdaemon.createWallet( - 'user-1', - 'user-1@example.com', + authContext, 'alice', false, SigningProvider.BLOCKDAEMON @@ -1019,8 +1000,7 @@ describe('WalletAllocationService', () => { }) const result = await serviceWithBlockdaemon.createWallet( - 'user-1', - 'user-1@example.com', + authContext, 'alice', false, SigningProvider.BLOCKDAEMON @@ -1039,8 +1019,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithBlockdaemon.allocateParty( - 'user-1', - 'user-1@example.com', + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.BLOCKDAEMON, topologyTransactions: 'tx1', @@ -1068,8 +1047,7 @@ describe('WalletAllocationService', () => { ) await serviceWithBlockdaemon.allocateParty( - 'user-1', - 'user-1@example.com', + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.BLOCKDAEMON, namespace: 'fingerprint', @@ -1095,8 +1073,7 @@ describe('WalletAllocationService', () => { }) await serviceWithBlockdaemon.allocateParty( - 'user-1', - 'user-1@example.com', + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.BLOCKDAEMON, topologyTransactions: 'tx1', @@ -1121,8 +1098,7 @@ describe('WalletAllocationService', () => { }) await serviceWithBlockdaemon.allocateParty( - 'user-1', - 'user-1@example.com', + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.BLOCKDAEMON, topologyTransactions: 'tx1', @@ -1153,8 +1129,7 @@ describe('WalletAllocationService', () => { }) await serviceWithBlockdaemon.allocateParty( - 'user-1', - 'user-1@example.com', + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.BLOCKDAEMON, topologyTransactions: 'tx1', @@ -1180,8 +1155,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithoutDfns.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.DFNS @@ -1198,8 +1172,7 @@ describe('WalletAllocationService', () => { }) const result = await serviceWithDfns.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.DFNS @@ -1231,8 +1204,7 @@ describe('WalletAllocationService', () => { ) const result = await serviceWithDfns.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.DFNS @@ -1260,8 +1232,7 @@ describe('WalletAllocationService', () => { }) const result = await serviceWithDfns.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.DFNS @@ -1286,8 +1257,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithDfns.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.DFNS @@ -1308,8 +1278,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithDfns.createWallet( - 'user-1', - undefined, + authContext, 'alice', false, SigningProvider.DFNS @@ -1326,8 +1295,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithDfns.allocateParty( - 'user-1', - undefined, + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.DFNS, topologyTransactions: 'tx1', @@ -1352,8 +1320,7 @@ describe('WalletAllocationService', () => { await expect( serviceWithDfns.allocateParty( - 'user-1', - undefined, + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.DFNS, topologyTransactions: 'tx1', @@ -1382,8 +1349,7 @@ describe('WalletAllocationService', () => { ) await serviceWithDfns.allocateParty( - 'user-1', - undefined, + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.DFNS, namespace: 'fingerprint', @@ -1399,7 +1365,7 @@ describe('WalletAllocationService', () => { 'fingerprint', ['tx1'], 'sig-base64', - 'user-1' + authContext.userId ) expect(mockStore.updateWallet).toHaveBeenCalledWith({ networkId: 'network1', @@ -1417,8 +1383,7 @@ describe('WalletAllocationService', () => { }) await serviceWithDfns.allocateParty( - 'user-1', - undefined, + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.DFNS, topologyTransactions: 'tx1', @@ -1452,8 +1417,7 @@ describe('WalletAllocationService', () => { }) await serviceWithDfns.allocateParty( - 'user-1', - undefined, + authContext, createWallet('alice::fingerprint', { signingProviderId: SigningProvider.DFNS, topologyTransactions: 'tx1', diff --git a/wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.ts b/wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.ts index 11c3f4eea..657d093ce 100644 --- a/wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.ts +++ b/wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { UserId } from '@canton-network/core-wallet-auth' +import { AuthContext } from '@canton-network/core-wallet-auth' import { Store, Wallet } from '@canton-network/core-wallet-store' import { SigningDriverInterface, @@ -18,14 +18,12 @@ import { DfnsWalletAllocator } from './signing-providers/dfns-wallet-allocator.j export interface WalletAllocator { createWallet( - userId: UserId, - email: string | undefined, + authContext: AuthContext, partyHint: PartyHint, primary: Primary ): Promise allocateParty( - userId: UserId, - email: string | undefined, + authContext: AuthContext, existingWallet: Wallet ): Promise } @@ -93,8 +91,7 @@ export class WalletAllocationService { } public async createWallet( - userId: UserId, - email: string | undefined, + authContext: AuthContext, partyHint: PartyHint, primary: Primary, signingProviderId: SigningProvider @@ -102,8 +99,7 @@ export class WalletAllocationService { switch (signingProviderId) { case SigningProvider.PARTICIPANT: return this.participantAllocator.createWallet( - userId, - email, + authContext, partyHint, primary ) @@ -114,8 +110,7 @@ export class WalletAllocationService { ) } return this.kernelAllocator.createWallet( - userId, - email, + authContext, partyHint, primary ) @@ -124,8 +119,7 @@ export class WalletAllocationService { throw new Error('Fireblocks signing driver not available') } return this.fireblocksAllocator.createWallet( - userId, - email, + authContext, partyHint, primary ) @@ -133,14 +127,13 @@ export class WalletAllocationService { if (!this.blockdaemonAllocator) { throw new Error('Blockdaemon signing driver not available') } - if (!email) { + if (!authContext.email) { throw new Error( 'Email is required for Blockdaemon wallet allocation' ) } return this.blockdaemonAllocator.createWallet( - userId, - email, + authContext, partyHint, primary ) @@ -149,8 +142,7 @@ export class WalletAllocationService { throw new Error('Dfns signing driver not available') } return this.dfnsAllocator.createWallet( - userId, - email, + authContext, partyHint, primary ) @@ -162,16 +154,14 @@ export class WalletAllocationService { } public async allocateParty( - userId: UserId, - email: string | undefined, + authContext: AuthContext, existingWallet: Wallet, signingProviderId: SigningProvider ): Promise { switch (signingProviderId) { case SigningProvider.PARTICIPANT: return this.participantAllocator.allocateParty( - userId, - email, + authContext, existingWallet ) case SigningProvider.WALLET_KERNEL: @@ -181,8 +171,7 @@ export class WalletAllocationService { ) } return this.kernelAllocator.allocateParty( - userId, - email, + authContext, existingWallet ) case SigningProvider.FIREBLOCKS: @@ -190,22 +179,20 @@ export class WalletAllocationService { throw new Error('Fireblocks signing driver not available') } return this.fireblocksAllocator.allocateParty( - userId, - email, + authContext, existingWallet ) case SigningProvider.BLOCKDAEMON: if (!this.blockdaemonAllocator) { throw new Error('Blockdaemon signing driver not available') } - if (!email) { + if (!authContext.email) { throw new Error( 'Email is required for Blockdaemon wallet allocation' ) } return this.blockdaemonAllocator.allocateParty( - userId, - email, + authContext, existingWallet ) case SigningProvider.DFNS: @@ -213,8 +200,7 @@ export class WalletAllocationService { throw new Error('Dfns signing driver not available') } return this.dfnsAllocator.allocateParty( - userId, - email, + authContext, existingWallet ) default: 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 f4de6658e..b069e72da 100644 --- a/wallet-gateway/remote/src/ledger/wallet-sync-service.test.ts +++ b/wallet-gateway/remote/src/ledger/wallet-sync-service.test.ts @@ -161,7 +161,7 @@ describe('WalletSyncService - resolveSigningProvider', () => { const internalDriver = service['signingDrivers'][ SigningProvider.WALLET_KERNEL ] as InternalSigningDriver - const controller = internalDriver.controller(authContext.userId) + const controller = internalDriver.controller(authContext) const key = await controller.createKey({ name: 'test-key' }) if ('error' in key) { diff --git a/wallet-gateway/remote/src/ledger/wallet-sync-service.ts b/wallet-gateway/remote/src/ledger/wallet-sync-service.ts index 988b57732..b3eec1d46 100644 --- a/wallet-gateway/remote/src/ledger/wallet-sync-service.ts +++ b/wallet-gateway/remote/src/ledger/wallet-sync-service.ts @@ -102,7 +102,6 @@ export class WalletSyncService { } // Get keys from signing providers try to match - const userId = this.authContext?.userId for (const [providerId, driver] of Object.entries( this.signingDrivers )) { @@ -114,7 +113,7 @@ export class WalletSyncService { } try { - const controller = driver.controller(userId) + const controller = driver.controller(this.authContext) const result = await controller.getKeys() // In case of error getKeys resolve Promise but with error object diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 974e58c02..4c09d75d5 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -311,8 +311,7 @@ export const userController = ( } const wallet = await walletAllocationService.createWallet( - userId, - email, + connectedContext, partyHint, primary ?? false, signingProviderId as SigningProvider @@ -402,8 +401,7 @@ export const userController = ( } await walletAllocationService.allocateParty( - userId, - email, + connectedContext, existingWallet, signingProviderId ) @@ -474,7 +472,8 @@ export const userController = ( const notifier = notificationService.getNotifier(userId) const signingProvider = wallet.signingProviderId as SigningProvider - const driver = drivers[signingProvider]?.controller(userId) + const driver = + drivers[signingProvider]?.controller(connectedContext) if (!driver) { throw new Error( @@ -495,7 +494,7 @@ export const userController = ( } case SigningProvider.WALLET_KERNEL: { return transactionService.signWithWalletKernel( - userId, + connectedContext, wallet, signParams ) @@ -507,21 +506,21 @@ export const userController = ( ) } return transactionService.signWithBlockdaemon( - email, + connectedContext, wallet, signParams ) } case SigningProvider.FIREBLOCKS: { return transactionService.signWithFireblocks( - userId, + connectedContext, wallet, signParams ) } case SigningProvider.DFNS: { return transactionService.signWithDfns( - userId, + connectedContext, wallet, signParams ) @@ -594,8 +593,11 @@ export const userController = ( ) } + const connectedContext = assertConnected(authContext) const driver = - drivers[SigningProvider.WALLET_KERNEL]?.controller(userId) + drivers[SigningProvider.WALLET_KERNEL]?.controller( + connectedContext + ) if (!driver) { return await emitFailedAndPersist( 'Wallet Kernel signing driver not available' From 2c142aabd0e562e18bee081f66da2b4fc9bc851c Mon Sep 17 00:00:00 2001 From: Marc Juchli Date: Mon, 8 Jun 2026 16:41:06 +0200 Subject: [PATCH 5/5] fix Signed-off-by: Marc Juchli --- .../src/ledger/transaction-service.test.ts | 792 ++++++++++-------- 1 file changed, 429 insertions(+), 363 deletions(-) diff --git a/wallet-gateway/remote/src/ledger/transaction-service.test.ts b/wallet-gateway/remote/src/ledger/transaction-service.test.ts index a90c3735a..e5bb777ca 100644 --- a/wallet-gateway/remote/src/ledger/transaction-service.test.ts +++ b/wallet-gateway/remote/src/ledger/transaction-service.test.ts @@ -6,6 +6,7 @@ import { pino } from 'pino' import { sink } from 'pino-test' import type { Logger } from 'pino' import type { LedgerClient } from '@canton-network/core-ledger-client' +import type { AuthContext } from '@canton-network/core-wallet-auth' import type { Network, Store, @@ -18,12 +19,14 @@ import { } from '@canton-network/core-signing-lib' import type { Notifier } from '../notification/NotificationService.js' import { TransactionService } from './transaction-service.js' -import { AuthContext } from '@canton-network/core-wallet-auth' -const userId = 'user-1' const authContext: AuthContext = { - userId, - accessToken: 'access-token', + userId: 'user-1', + accessToken: 'access-token-1', +} + +const authContextWithEmail: AuthContext = { + ...authContext, email: 'user@example.com', } @@ -76,6 +79,10 @@ const network: Network = { }, } +function walletWithProvider(signingProviderId: SigningProvider): Wallet { + return { ...wallet, signingProviderId } +} + function createDriver(options: { signTransaction?: ReturnType getTransaction?: ReturnType @@ -133,412 +140,471 @@ describe('TransactionService', () => { vi.clearAllMocks() }) - describe('signWithParticipant', () => { - it('returns a signed result without calling external drivers', () => { - const store = createStore() - const service = createService(store, {}, notifier, logger) - - const result = service.signWithParticipant(wallet) - - expect(result).toEqual({ - status: 'signed', - signature: 'none', - signedBy: wallet.namespace, - partyId: wallet.partyId, + describe('sign', () => { + describe('participant', () => { + it('returns a signed result without calling external drivers', async () => { + const store = createStore() + const service = createService( + store, + { + [SigningProvider.PARTICIPANT]: createDriver({}), + }, + notifier, + logger + ) + const participantWallet = walletWithProvider( + SigningProvider.PARTICIPANT + ) + + const result = await service.sign( + authContext, + participantWallet, + signParams + ) + + expect(result).toEqual({ + status: 'signed', + signature: 'none', + signedBy: wallet.namespace, + partyId: wallet.partyId, + }) + expect(store.getTransaction).not.toHaveBeenCalled() }) - expect(store.getTransaction).not.toHaveBeenCalled() }) - }) - describe('signWithWalletKernel', () => { - it('signs the transaction and persists the signed state', async () => { - const signTransaction = vi - .fn() - .mockResolvedValue({ signature: 'kernel-signature' }) - const store = createStore() - const service = createService( - store, - { - [SigningProvider.WALLET_KERNEL]: createDriver({ - signTransaction, - }), - }, - notifier, - logger - ) + describe('wallet-kernel', () => { + it('signs the transaction and persists the signed state', async () => { + const signTransaction = vi + .fn() + .mockResolvedValue({ signature: 'kernel-signature' }) + const store = createStore() + const service = createService( + store, + { + [SigningProvider.WALLET_KERNEL]: createDriver({ + signTransaction, + }), + }, + notifier, + logger + ) - const result = await service.signWithWalletKernel( - authContext, - wallet, - signParams - ) + const result = await service.sign( + authContext, + wallet, + signParams + ) - expect(signTransaction).toHaveBeenCalledWith({ - tx: pendingTransaction.preparedTransaction, - txHash: pendingTransaction.preparedTransactionHash, - keyIdentifier: { publicKey: wallet.publicKey }, - }) - expect(store.setTransactionSigned).toHaveBeenCalledWith( - pendingTransaction.id, - expect.any(Date) - ) - expect(emit).toHaveBeenCalledWith( - 'txChanged', - expect.objectContaining({ - id: pendingTransaction.id, + expect(signTransaction).toHaveBeenCalledWith({ + tx: pendingTransaction.preparedTransaction, + txHash: pendingTransaction.preparedTransactionHash, + keyIdentifier: { publicKey: wallet.publicKey }, + }) + expect(store.setTransactionSigned).toHaveBeenCalledWith( + pendingTransaction.id, + expect.any(Date) + ) + expect(emit).toHaveBeenCalledWith( + 'txChanged', + expect.objectContaining({ + id: pendingTransaction.id, + status: 'signed', + }) + ) + expect(result).toEqual({ status: 'signed', + signature: 'kernel-signature', + signedBy: wallet.namespace, + partyId: wallet.partyId, }) - ) - expect(result).toEqual({ - status: 'signed', - signature: 'kernel-signature', - signedBy: wallet.namespace, - partyId: wallet.partyId, }) - }) - - it('throws when the wallet-kernel driver is missing', async () => { - const service = createService(createStore(), {}, notifier, logger) - - await expect( - service.signWithWalletKernel(authContext, wallet, signParams) - ).rejects.toThrow('Wallet Gateway signing driver not available') - }) - - it('throws when the transaction does not exist', async () => { - const store = createStore() - store.getTransaction.mockResolvedValue(undefined) - const service = createService( - store, - { - [SigningProvider.WALLET_KERNEL]: createDriver({}), - }, - notifier, - logger - ) - - await expect( - service.signWithWalletKernel(authContext, wallet, signParams) - ).rejects.toThrow('Transaction not found with id: tx-1') - }) - it('throws when the driver returns an RPC error', async () => { - const signTransaction = vi.fn().mockResolvedValue({ - error: 'access_denied', - error_description: 'Signing rejected', + it('throws when the wallet-kernel driver is missing', async () => { + const service = createService( + createStore(), + {}, + notifier, + logger + ) + + await expect( + service.sign(authContext, wallet, signParams) + ).rejects.toThrow('No driver found for wallet-kernel') }) - const service = createService( - createStore(), - { - [SigningProvider.WALLET_KERNEL]: createDriver({ - signTransaction, - }), - }, - notifier, - logger - ) - await expect( - service.signWithWalletKernel(authContext, wallet, signParams) - ).rejects.toThrow('Error from signing driver: Signing rejected') - }) - }) + it('throws when the transaction does not exist', async () => { + const store = createStore() + store.getTransaction.mockResolvedValue(undefined) + const service = createService( + store, + { + [SigningProvider.WALLET_KERNEL]: createDriver({}), + }, + notifier, + logger + ) - describe('signWithBlockdaemon', () => { - it('starts signing when there is no external transaction id yet', async () => { - const signTransaction = vi.fn().mockResolvedValue({ - status: 'pending', - txId: 'external-tx-1', + await expect( + service.sign(authContext, wallet, signParams) + ).rejects.toThrow('Transaction not found with id: tx-1') }) - const store = createStore() - const service = createService( - store, - { - [SigningProvider.BLOCKDAEMON]: createDriver({ - signTransaction, - }), - }, - notifier, - logger - ) - - const result = await service.signWithBlockdaemon( - authContext, - wallet, - signParams - ) - expect(signTransaction).toHaveBeenCalledWith( - expect.objectContaining({ - tx: pendingTransaction.preparedTransaction, - internalTxId: expect.any(String), + it('throws when the driver returns an RPC error', async () => { + const signTransaction = vi.fn().mockResolvedValue({ + error: 'access_denied', + error_description: 'Signing rejected', }) - ) - expect(store.setTransactionStatus).toHaveBeenCalledWith( - pendingTransaction.id, - 'pending', - { externalTxId: 'external-tx-1' } - ) - expect(result).toEqual({ - status: 'pending', - externalTxId: 'external-tx-1', - partyId: wallet.partyId, - }) - }) + const service = createService( + createStore(), + { + [SigningProvider.WALLET_KERNEL]: createDriver({ + signTransaction, + }), + }, + notifier, + logger + ) - it('polls the driver when an external transaction id already exists', async () => { - const getTransaction = vi.fn().mockResolvedValue({ - status: 'signed', - txId: 'external-tx-1', - signature: 'bd-signature', + await expect( + service.sign(authContext, wallet, signParams) + ).rejects.toThrow('Error from signing driver: Signing rejected') }) - const store = createStore({ - ...pendingTransaction, - externalTxId: 'external-tx-1', - }) - const service = createService( - store, - { - [SigningProvider.BLOCKDAEMON]: createDriver({ - getTransaction, - }), - }, - notifier, - logger - ) + }) - const result = await service.signWithBlockdaemon( - authContext, - wallet, - signParams + describe('blockdaemon', () => { + const blockdaemonWallet = walletWithProvider( + SigningProvider.BLOCKDAEMON ) - expect(getTransaction).toHaveBeenCalledWith({ - userId: authContext.userId, - txId: 'external-tx-1', - }) - expect(store.setTransactionSigned).toHaveBeenCalledWith( - pendingTransaction.id, - expect.any(Date), - 'external-tx-1' - ) - expect(result).toMatchObject({ - status: 'signed', - signature: 'bd-signature', - externalTxId: 'external-tx-1', + it('throws when email is missing from auth context', async () => { + const service = createService( + createStore(), + { + [SigningProvider.BLOCKDAEMON]: createDriver({}), + }, + notifier, + logger + ) + + await expect( + service.sign(authContext, blockdaemonWallet, signParams) + ).rejects.toThrow( + 'Email is required for Blockdaemon wallet allocation' + ) }) - }) - }) - describe('signWithFireblocks', () => { - it('returns a base64 signature when signing completes', async () => { - const hexSignature = Buffer.from('fireblocks-signature').toString( - 'hex' - ) - const signTransaction = vi.fn().mockResolvedValue({ - status: 'signed', - txId: 'fb-tx-1', - signature: hexSignature, + it('starts signing when there is no external transaction id yet', async () => { + const signTransaction = vi.fn().mockResolvedValue({ + status: 'pending', + txId: 'external-tx-1', + }) + const store = createStore() + const service = createService( + store, + { + [SigningProvider.BLOCKDAEMON]: createDriver({ + signTransaction, + }), + }, + notifier, + logger + ) + + const result = await service.sign( + authContextWithEmail, + blockdaemonWallet, + signParams + ) + + expect(signTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + tx: pendingTransaction.preparedTransaction, + internalTxId: expect.any(String), + }) + ) + expect(store.setTransactionStatus).toHaveBeenCalledWith( + pendingTransaction.id, + 'pending', + { externalTxId: 'external-tx-1' } + ) + expect(result).toEqual({ + status: 'pending', + externalTxId: 'external-tx-1', + partyId: wallet.partyId, + }) }) - const store = createStore() - const service = createService( - store, - { - [SigningProvider.FIREBLOCKS]: createDriver({ - signTransaction, - }), - }, - notifier, - logger - ) - const result = await service.signWithFireblocks( - authContext, - wallet, - signParams - ) + it('fetches transaction when an external transaction id already exists', async () => { + const getTransaction = vi.fn().mockResolvedValue({ + status: 'signed', + txId: 'external-tx-1', + signature: 'bd-signature', + }) + const store = createStore({ + ...pendingTransaction, + externalTxId: 'external-tx-1', + }) + const service = createService( + store, + { + [SigningProvider.BLOCKDAEMON]: createDriver({ + getTransaction, + }), + }, + notifier, + logger + ) + + const result = await service.sign( + authContextWithEmail, + blockdaemonWallet, + signParams + ) + + expect(getTransaction).toHaveBeenCalledWith({ + userId: authContextWithEmail.email, + txId: 'external-tx-1', + }) + expect(store.setTransactionSigned).toHaveBeenCalledWith( + pendingTransaction.id, + expect.any(Date), + 'external-tx-1' + ) + expect(result).toMatchObject({ + status: 'signed', + signature: 'bd-signature', + externalTxId: 'external-tx-1', + }) + }) + }) - expect(signTransaction).toHaveBeenCalledWith( - expect.objectContaining({ - userId: authContext.userId, - txHash: Buffer.from( - pendingTransaction.preparedTransactionHash, + describe('fireblocks', () => { + it('returns a base64 signature when signing completes', async () => { + const hexSignature = Buffer.from( + 'fireblocks-signature' + ).toString('hex') + const signTransaction = vi.fn().mockResolvedValue({ + status: 'signed', + txId: 'fb-tx-1', + signature: hexSignature, + }) + const store = createStore() + const service = createService( + store, + { + [SigningProvider.FIREBLOCKS]: createDriver({ + signTransaction, + }), + }, + notifier, + logger + ) + + const result = await service.sign( + authContext, + walletWithProvider(SigningProvider.FIREBLOCKS), + signParams + ) + + expect(signTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + userId: authContext.userId, + txHash: Buffer.from( + pendingTransaction.preparedTransactionHash, + 'base64' + ).toString('hex'), + }) + ) + expect(result).toMatchObject({ + status: 'signed', + signature: Buffer.from(hexSignature, 'hex').toString( 'base64' - ).toString('hex'), + ), + externalTxId: 'fb-tx-1', }) - ) - expect(result).toMatchObject({ - status: 'signed', - signature: Buffer.from(hexSignature, 'hex').toString('base64'), - externalTxId: 'fb-tx-1', }) }) - }) - describe('signWithDfns', () => { - it('persists the update id as the signature when signing completes', async () => { - const signTransaction = vi.fn().mockResolvedValue({ - status: 'signed', - txId: 'dfns-tx-1', - signature: 'update-id-123', - }) - const store = createStore() - const service = createService( - store, - { - [SigningProvider.DFNS]: createDriver({ - signTransaction, - }), - }, - notifier, - logger - ) + describe('dfns', () => { + it('persists the update id as the signature when signing completes', async () => { + const signTransaction = vi.fn().mockResolvedValue({ + status: 'signed', + txId: 'dfns-tx-1', + signature: 'update-id-123', + }) + const store = createStore() + const service = createService( + store, + { + [SigningProvider.DFNS]: createDriver({ + signTransaction, + }), + }, + notifier, + logger + ) - const result = await service.signWithDfns( - authContext, - wallet, - signParams - ) + const result = await service.sign( + authContext, + walletWithProvider(SigningProvider.DFNS), + signParams + ) - expect(result).toEqual({ - status: 'signed', - signature: 'update-id-123', - signedBy: wallet.namespace, - partyId: wallet.partyId, - externalTxId: 'dfns-tx-1', + expect(result).toEqual({ + status: 'signed', + signature: 'update-id-123', + signedBy: wallet.namespace, + partyId: wallet.partyId, + externalTxId: 'dfns-tx-1', + }) }) }) }) - describe('executeWithDfns', () => { - it('marks the transaction executed using the external tx id', async () => { - const signedTransaction: Transaction = { - ...pendingTransaction, - status: 'signed', - externalTxId: 'dfns-update-id', - } - const store = createStore(signedTransaction) - const service = createService(store, {}, notifier, logger) - - const result = await service.executeWithDfns(signedTransaction) - - expect(store.setTransactionStatus).toHaveBeenCalledWith( - signedTransaction.id, - 'executed', - { externalTxId: 'dfns-update-id' } - ) - expect(emit).toHaveBeenCalledWith( - 'txChanged', - expect.objectContaining({ status: 'executed' }) - ) - expect(result).toEqual({ updateId: 'dfns-update-id' }) - }) - - it('throws when the transaction has no external tx id', async () => { - const service = createService(createStore(), {}, notifier, logger) + describe('execute', () => { + describe('dfns', () => { + it('marks the transaction executed using the external tx id', async () => { + const signedTransaction: Transaction = { + ...pendingTransaction, + status: 'signed', + externalTxId: 'dfns-update-id', + } + const store = createStore(signedTransaction) + const service = createService(store, {}, notifier, logger) + + const result = await service.execute( + authContext, + walletWithProvider(SigningProvider.DFNS), + signedTransaction + ) + + expect(store.setTransactionStatus).toHaveBeenCalledWith( + signedTransaction.id, + 'executed', + { externalTxId: 'dfns-update-id' } + ) + expect(emit).toHaveBeenCalledWith( + 'txChanged', + expect.objectContaining({ status: 'executed' }) + ) + expect(result).toEqual({ updateId: 'dfns-update-id' }) + }) - await expect( - service.executeWithDfns(pendingTransaction) - ).rejects.toThrow( - 'Cannot execute Dfns transaction without externalTxId from Dfns' - ) + it('throws when the transaction has no external tx id', async () => { + const service = createService( + createStore(), + {}, + notifier, + logger + ) + + await expect( + service.execute( + authContext, + walletWithProvider(SigningProvider.DFNS), + pendingTransaction + ) + ).rejects.toThrow( + 'Cannot execute Dfns transaction without externalTxId from Dfns' + ) + }) }) - }) - describe('executeWithParticipant', () => { - it('submits the prepared transaction to the ledger', async () => { - const store = createStore({ - ...pendingTransaction, - payload: { - commandId: pendingTransaction.commandId, - commands: [], - }, - }) - const postWithRetry = vi - .fn() - .mockResolvedValue({ updateId: 'ledger-update-1' }) - const ledgerClient = { - postWithRetry, - getSynchronizerId: vi.fn(), - } as unknown as LedgerClient - const service = createService(store, {}, notifier, logger) - - const result = await service.executeWithParticipant( - userId, - executeParams, - { + describe('participant', () => { + it('submits the prepared transaction to the ledger', async () => { + const participantWallet = walletWithProvider( + SigningProvider.PARTICIPANT + ) + const transaction = { ...pendingTransaction, payload: { commandId: pendingTransaction.commandId, commands: [], }, - }, - ledgerClient, - network - ) - - expect(postWithRetry).toHaveBeenCalledWith( - '/v2/commands/submit-and-wait', - expect.objectContaining({ - commandId: pendingTransaction.commandId, - userId, - synchronizerId: network.synchronizerId, - }) - ) - expect(store.setTransactionStatus).toHaveBeenCalledWith( - pendingTransaction.id, - 'executed', - { payload: { updateId: 'ledger-update-1' } } - ) - expect(result).toEqual({ updateId: 'ledger-update-1' }) + } + const store = createStore(transaction) + const postWithRetry = vi + .fn() + .mockResolvedValue({ updateId: 'ledger-update-1' }) + const ledgerClient = { + postWithRetry, + getSynchronizerId: vi.fn(), + } as unknown as LedgerClient + const service = createService(store, {}, notifier, logger) + + const result = await service.execute( + authContext, + participantWallet, + transaction, + executeParams, + ledgerClient, + network + ) + + expect(postWithRetry).toHaveBeenCalledWith( + '/v2/commands/submit-and-wait', + expect.objectContaining({ + commandId: pendingTransaction.commandId, + userId: authContext.userId, + synchronizerId: network.synchronizerId, + }) + ) + expect(store.setTransactionStatus).toHaveBeenCalledWith( + pendingTransaction.id, + 'executed', + { payload: { updateId: 'ledger-update-1' } } + ) + expect(result).toEqual({ updateId: 'ledger-update-1' }) + }) }) - }) - describe('executeWithExternal', () => { - it('executes the prepared transaction with the provided signature', async () => { - const store = createStore({ - ...pendingTransaction, - status: 'signed', - }) - const postWithRetry = vi - .fn() - .mockResolvedValue({ updateId: 'external-update-1' }) - const ledgerClient = { - postWithRetry, - } as unknown as LedgerClient - const service = createService(store, {}, notifier, logger) - - const result = await service.executeWithExternal( - userId, - executeParams, - { + describe('external signing providers', () => { + it('executes the prepared transaction with the provided signature', async () => { + const signedTransaction = { ...pendingTransaction, - status: 'signed', - }, - ledgerClient - ) - - expect(postWithRetry).toHaveBeenCalledWith( - '/v2/interactive-submission/executeAndWait', - expect.objectContaining({ - userId, - preparedTransaction: pendingTransaction.preparedTransaction, - submissionId: pendingTransaction.commandId, - partySignatures: expect.objectContaining({ - signatures: [ - expect.objectContaining({ - party: wallet.partyId, - }), - ], - }), - }) - ) - expect(store.setTransactionStatus).toHaveBeenCalledWith( - pendingTransaction.id, - 'executed', - { payload: { updateId: 'external-update-1' } } - ) - expect(result).toEqual({ updateId: 'external-update-1' }) + status: 'signed' as const, + } + const store = createStore(signedTransaction) + const postWithRetry = vi + .fn() + .mockResolvedValue({ updateId: 'external-update-1' }) + const ledgerClient = { + postWithRetry, + } as unknown as LedgerClient + const service = createService(store, {}, notifier, logger) + + const result = await service.execute( + authContext, + wallet, + signedTransaction, + executeParams, + ledgerClient, + network + ) + + expect(postWithRetry).toHaveBeenCalledWith( + '/v2/interactive-submission/executeAndWait', + expect.objectContaining({ + userId: authContext.userId, + preparedTransaction: + pendingTransaction.preparedTransaction, + submissionId: pendingTransaction.commandId, + partySignatures: expect.objectContaining({ + signatures: [ + expect.objectContaining({ + party: wallet.partyId, + }), + ], + }), + }) + ) + expect(store.setTransactionStatus).toHaveBeenCalledWith( + pendingTransaction.id, + 'executed', + { payload: { updateId: 'external-update-1' } } + ) + expect(result).toEqual({ updateId: 'external-update-1' }) + }) }) }) })