From 5e062c735df36e615722cf559e7f6a2534687093 Mon Sep 17 00:00:00 2001 From: Pawel Stepien Date: Fri, 19 Dec 2025 17:02:23 +0100 Subject: [PATCH 1/7] sign with either public key or identifier Signed-off-by: Pawel Stepien --- api-specs/openrpc-signing-api.json | 27 ++++- .../signing-fireblocks/src/fireblocks.test.ts | 5 +- core/signing-fireblocks/src/fireblocks.ts | 49 +++++++-- core/signing-fireblocks/src/index.test.ts | 4 +- core/signing-fireblocks/src/index.ts | 4 +- core/signing-internal/src/controller.test.ts | 4 +- core/signing-internal/src/controller.ts | 104 +++++++++++------- core/signing-lib/src/SigningDriverStore.ts | 4 + core/signing-lib/src/rpc-gen/typings.ts | 24 ++-- .../src/controller.test.ts | 2 +- core/signing-store-sql/src/store-sql.ts | 13 +++ .../remote/src/user-api/controller.ts | 12 +- 12 files changed, 182 insertions(+), 70 deletions(-) diff --git a/api-specs/openrpc-signing-api.json b/api-specs/openrpc-signing-api.json index 5795ff153..e5729b99d 100644 --- a/api-specs/openrpc-signing-api.json +++ b/api-specs/openrpc-signing-api.json @@ -25,10 +25,9 @@ "type": "string", "description": "Hash of the prepared transaction that will be signed." }, - "publicKey": { - "title": "publicKey", - "type": "string", - "description": "Public key to use to sign the transaction." + "keyIdentifier": { + "$ref": "#/components/schemas/KeyIdentifier", + "description": "Identifier for the key to use for signing. At least one of publicKey or id must be provided." }, "internalTxId": { "title": "internalTxId", @@ -36,7 +35,7 @@ "description": "Internal txId used by the Wallet Gateway to store the transaction." } }, - "required": ["tx", "txHash", "publicKey"] + "required": ["tx", "txHash", "keyIdentifier"] } } ], @@ -279,6 +278,24 @@ "type": "null", "description": "Represents a null value, used in responses where no data is returned." }, + "KeyIdentifier": { + "title": "KeyIdentifier", + "type": "object", + "properties": { + "publicKey": { + "title": "publicKey", + "type": "string", + "description": "Public key to use to sign the transaction." + }, + "id": { + "title": "id", + "type": "string", + "description": "Unique identifier for the key" + } + }, + "minProperties": 1, + "description": "Identifier for a key. At least one of publicKey or id must be provided (both can also be provided)." + }, "Key": { "title": "Key", "type": "object", diff --git a/core/signing-fireblocks/src/fireblocks.test.ts b/core/signing-fireblocks/src/fireblocks.test.ts index 1da036891..6ef507bd4 100644 --- a/core/signing-fireblocks/src/fireblocks.test.ts +++ b/core/signing-fireblocks/src/fireblocks.test.ts @@ -38,7 +38,10 @@ describe('fireblocks handler', () => { const transaction = await handler.signTransaction( userId, TEST_TRANSACTION_HASH, - '02fefbcc9aebc8a479f211167a9f564df53aefd603a8662d9449a98c1ead2eba' + { + publicKey: + '02fefbcc9aebc8a479f211167a9f564df53aefd603a8662d9449a98c1ead2eba', + } ) expect(transaction).toBeDefined() const foundTransaction = await handler.getTransaction( diff --git a/core/signing-fireblocks/src/fireblocks.ts b/core/signing-fireblocks/src/fireblocks.ts index 5ae28fffa..38639848b 100644 --- a/core/signing-fireblocks/src/fireblocks.ts +++ b/core/signing-fireblocks/src/fireblocks.ts @@ -54,6 +54,7 @@ export class FireblocksHandler { private keyInfoByPublicKey: Map = new Map() private publicKeyByDerivationPath: Map = new Map() + private keyInfoById: Map = new Map() private getClient = (userId: string | undefined): Fireblocks => { if (userId !== undefined && this.clients.has(userId)) { @@ -132,6 +133,8 @@ export class FireblocksHandler { } keys.push(storedKey) this.keyInfoByPublicKey.set(storedKey.publicKey, storedKey) + const keyId = storedKey.derivationPath.join('-') + this.keyInfoById.set(keyId, storedKey) } } } catch (error) { @@ -306,33 +309,59 @@ export class FireblocksHandler { } } /** - * Sign a transaction using a public key + * Resolve a key identifier to a FireblocksKey + * @param userId - The user ID + * @param keyIdentifier - The key identifier (publicKey or id) + * @return The FireblocksKey if found + */ + private async resolveKey( + userId: string | undefined, + keyIdentifier: { publicKey?: string; id?: string } + ): Promise { + // Refresh cache if needed + if ( + (keyIdentifier.publicKey && + !this.keyInfoByPublicKey.has(keyIdentifier.publicKey)) || + (keyIdentifier.id && !this.keyInfoById.has(keyIdentifier.id)) + ) { + await this.getPublicKeys(userId) + } + + if (keyIdentifier.publicKey) { + return this.keyInfoByPublicKey.get(keyIdentifier.publicKey) + } + if (keyIdentifier.id) { + return this.keyInfoById.get(keyIdentifier.id) + } + return undefined + } + + /** + * Sign a transaction using a key identifier * @param tx - The transaction to sign, as a string - * @param publicKey - The public key to use for signing + * @param keyIdentifier - The key identifier (publicKey or id) to use for signing * @param externalTxId - The transaction ID assigned by the Wallet Gateway * @return The transaction object from Fireblocks */ public async signTransaction( userId: string | undefined, tx: string, - publicKey: string, + keyIdentifier: { publicKey?: string; id?: string }, externalTxId?: string ): Promise { try { const client = this.getClient(userId) - if (!this.keyInfoByPublicKey.has(publicKey)) { - // refresh the keycache - await this.getPublicKeys(userId) - } - const key = this.keyInfoByPublicKey.get(publicKey) + const key = await this.resolveKey(userId, keyIdentifier) if (!key) { - throw new Error(`Public key ${publicKey} not found in vaults`) + throw new Error( + `Key identifier not found in vaults: ${JSON.stringify(keyIdentifier)}` + ) } const transaction = await client.transactions.createTransaction({ transactionRequest: { operation: 'RAW', - note: `Signing transaction with public key ${publicKey}`, + note: `Signing transaction with key ${key.name || key.publicKey}`, externalTxId, extraParameters: { rawMessageData: { diff --git a/core/signing-fireblocks/src/index.test.ts b/core/signing-fireblocks/src/index.test.ts index 4ed3a7b42..b0a665338 100644 --- a/core/signing-fireblocks/src/index.test.ts +++ b/core/signing-fireblocks/src/index.test.ts @@ -197,7 +197,9 @@ test('transaction signature', async () => { const tx = await controller.signTransaction({ tx: TEST_TRANSACTION, txHash: TEST_TRANSACTION_HASH, - publicKey: key.publicKey, + keyIdentifier: { + publicKey: key.publicKey, + }, }) throwWhenRpcError(tx) diff --git a/core/signing-fireblocks/src/index.ts b/core/signing-fireblocks/src/index.ts index e43338dc1..f89e136c3 100644 --- a/core/signing-fireblocks/src/index.ts +++ b/core/signing-fireblocks/src/index.ts @@ -84,8 +84,8 @@ export default class FireblocksSigningDriver implements SigningDriverInterface { try { const tx = await this.fireblocks.signTransaction( userId, - params.txHash, - params.publicKey, + params.tx, + params.keyIdentifier, params.internalTxId ) return { diff --git a/core/signing-internal/src/controller.test.ts b/core/signing-internal/src/controller.test.ts index 1be40ddb6..c91ac9f46 100644 --- a/core/signing-internal/src/controller.test.ts +++ b/core/signing-internal/src/controller.test.ts @@ -78,7 +78,9 @@ test('transaction signature', async () => { const tx = await controller.signTransaction({ tx: TEST_TRANSACTION, txHash: TEST_TRANSACTION_HASH, - publicKey: key.publicKey, + keyIdentifier: { + publicKey: key.publicKey, + }, }) if (isRpcError(tx)) { diff --git a/core/signing-internal/src/controller.ts b/core/signing-internal/src/controller.ts index b2cc3105d..df01b6574 100644 --- a/core/signing-internal/src/controller.ts +++ b/core/signing-internal/src/controller.ts @@ -68,6 +68,24 @@ export class InternalSigningDriver implements SigningDriverInterface { this.store = store } + private async resolveKeyFromIdentifier( + userId: string | undefined, + keyIdentifier: SignTransactionParams['keyIdentifier'] + ): Promise { + if (keyIdentifier.publicKey) { + return await this.store.getSigningKeyByPublicKey( + keyIdentifier.publicKey + ) + } + if (!userId) { + return undefined + } + if (keyIdentifier.id) { + return await this.store.getSigningKey(userId, keyIdentifier.id) + } + return undefined + } + public controller = (_userId: AuthContext['userId'] | undefined) => buildController({ signTransaction: async ( @@ -75,52 +93,60 @@ export class InternalSigningDriver implements SigningDriverInterface { ): Promise => { // TODO: validate transaction here - const key = await this.store.getSigningKeyByPublicKey( - params.publicKey - ) - if (key?.privateKey && _userId) { - const txId = randomUUID() - const signature = signTransactionHash( - params.txHash, - key.privateKey - ) - - const now = new Date() - const internalTransaction: SigningTransaction = { - id: txId, - hash: params.txHash, - signature, - publicKey: params.publicKey, - createdAt: now, - status: 'signed', - updatedAt: now, - signedAt: now, - } + if (!_userId) { + return Promise.resolve({ + error: 'userId_not_found', + error_description: + 'User ID is required for all signing operations.', + }) + } - this.store.setSigningTransaction( - _userId, - internalTransaction - ) + const key = await this.resolveKeyFromIdentifier( + _userId, + params.keyIdentifier + ) - return Promise.resolve({ - txId, - status: 'signed', - signature, - } as SignTransactionResult) - } else { - if (!_userId) { - return Promise.resolve({ - error: 'userId_not_found', - error_description: - 'User ID is required for all signing operations.', - }) - } + if (!key) { return Promise.resolve({ error: 'key_not_found', error_description: - 'The provided public key does not exist in the signing.', + 'The provided key identifier does not exist in the signing store.', + }) + } + + if (!key.privateKey) { + return Promise.resolve({ + error: 'key_not_suitable', + error_description: + 'The provided key does not have a private key available for signing.', }) } + + const txId = randomUUID() + const signature = signTransactionHash( + params.txHash, + key.privateKey + ) + + const now = new Date() + const internalTransaction: SigningTransaction = { + id: txId, + hash: params.txHash, + signature, + publicKey: key.publicKey, + createdAt: now, + status: 'signed', + updatedAt: now, + signedAt: now, + } + + this.store.setSigningTransaction(_userId, internalTransaction) + + return Promise.resolve({ + txId, + status: 'signed', + signature, + } as SignTransactionResult) }, getTransaction: async ( diff --git a/core/signing-lib/src/SigningDriverStore.ts b/core/signing-lib/src/SigningDriverStore.ts index 847be312a..df5607136 100644 --- a/core/signing-lib/src/SigningDriverStore.ts +++ b/core/signing-lib/src/SigningDriverStore.ts @@ -13,6 +13,10 @@ export interface SigningDriverStore { keyId: string ): Promise getSigningKeyByPublicKey(publicKey: string): Promise + getSigningKeyByName( + userId: string, + name: string + ): Promise listSigningTransactionsByTxIdsAndPublicKeys( txIds: string[], publicKeys: string[] diff --git a/core/signing-lib/src/rpc-gen/typings.ts b/core/signing-lib/src/rpc-gen/typings.ts index 8447425cd..27ab8f312 100644 --- a/core/signing-lib/src/rpc-gen/typings.ts +++ b/core/signing-lib/src/rpc-gen/typings.ts @@ -21,6 +21,22 @@ export type TxHash = string * */ export type PublicKey = string +/** + * + * Unique identifier for the key + * + */ +export type Id = string +/** + * + * Identifier for the key to use for signing. At least one of publicKey or id must be provided. + * + */ +export interface KeyIdentifier { + publicKey?: PublicKey + id?: Id + [k: string]: any +} /** * * Internal txId used by the Wallet Gateway to store the transaction. @@ -106,12 +122,6 @@ export interface ObjectOfTransactionsUOtaZpXE { transactions?: Transactions [k: string]: any } -/** - * - * Unique identifier for the key - * - */ -export type Id = string export interface Key { id: Id name: Name @@ -127,7 +137,7 @@ export type Keys = Key[] export interface SignTransactionParams { tx: Tx txHash: TxHash - publicKey: PublicKey + keyIdentifier: KeyIdentifier internalTxId?: InternalTxId [k: string]: any } diff --git a/core/signing-participant/src/controller.test.ts b/core/signing-participant/src/controller.test.ts index b18d8da43..ee09689eb 100644 --- a/core/signing-participant/src/controller.test.ts +++ b/core/signing-participant/src/controller.test.ts @@ -28,7 +28,7 @@ test('transaction signature', async () => { .signTransaction({ tx: TEST_TRANSACTION, txHash: TEST_TRANSACTION_HASH, - publicKey: '', + keyIdentifier: {}, }) expect(tx.status).toBe('signed') }) diff --git a/core/signing-store-sql/src/store-sql.ts b/core/signing-store-sql/src/store-sql.ts index fb7b637de..bb202d8bd 100644 --- a/core/signing-store-sql/src/store-sql.ts +++ b/core/signing-store-sql/src/store-sql.ts @@ -75,6 +75,19 @@ export class StoreSql implements SigningDriverStore, AuthAware { return result ? toSigningKey(result) : undefined } + async getSigningKeyByName( + userId: string, + name: string + ): Promise { + const result = await this.db + .selectFrom('signingKeys') + .selectAll() + .where('userId', '=', userId) + .where('name', '=', name) + .executeTakeFirst() + return result ? toSigningKey(result) : undefined + } + async setSigningKey(userId: string, key: SigningKey): Promise { const serialized = fromSigningKey(key, userId) diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 30390f8a8..d1ecd0ce6 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -203,7 +203,9 @@ export const userController = ( const { signature } = await driver.signTransaction({ tx: '', txHash: hash, - publicKey: key.publicKey, + keyIdentifier: { + publicKey: key.publicKey, + }, }) return signature @@ -276,7 +278,9 @@ export const userController = ( transactions.multiHash, 'base64' ).toString('hex'), - publicKey: key.publicKey, + keyIdentifier: { + publicKey: key.publicKey, + }, }) if (status === 'signed') { const { signature } = await driver.getTransaction({ @@ -400,7 +404,9 @@ export const userController = ( const signature = await driver.signTransaction({ tx: preparedTransaction, txHash: preparedTransactionHash, - publicKey: wallet.publicKey, + keyIdentifier: { + publicKey: wallet.publicKey, + }, }) if (!signature.signature) { From 62810487287b4a60347c2056e7ef671a7159a6d5 Mon Sep 17 00:00:00 2001 From: Pawel Stepien Date: Mon, 22 Dec 2025 12:09:53 +0100 Subject: [PATCH 2/7] reduce unnecessary changes Signed-off-by: Pawel Stepien --- core/signing-fireblocks/src/fireblocks.ts | 49 ++++++---------------- core/signing-internal/src/controller.ts | 18 ++++---- core/signing-lib/src/SigningDriverStore.ts | 4 -- core/signing-store-sql/src/store-sql.ts | 13 ------ 4 files changed, 22 insertions(+), 62 deletions(-) diff --git a/core/signing-fireblocks/src/fireblocks.ts b/core/signing-fireblocks/src/fireblocks.ts index 38639848b..ae63f9483 100644 --- a/core/signing-fireblocks/src/fireblocks.ts +++ b/core/signing-fireblocks/src/fireblocks.ts @@ -54,7 +54,6 @@ export class FireblocksHandler { private keyInfoByPublicKey: Map = new Map() private publicKeyByDerivationPath: Map = new Map() - private keyInfoById: Map = new Map() private getClient = (userId: string | undefined): Fireblocks => { if (userId !== undefined && this.clients.has(userId)) { @@ -133,8 +132,6 @@ export class FireblocksHandler { } keys.push(storedKey) this.keyInfoByPublicKey.set(storedKey.publicKey, storedKey) - const keyId = storedKey.derivationPath.join('-') - this.keyInfoById.set(keyId, storedKey) } } } catch (error) { @@ -309,37 +306,9 @@ export class FireblocksHandler { } } /** - * Resolve a key identifier to a FireblocksKey - * @param userId - The user ID - * @param keyIdentifier - The key identifier (publicKey or id) - * @return The FireblocksKey if found - */ - private async resolveKey( - userId: string | undefined, - keyIdentifier: { publicKey?: string; id?: string } - ): Promise { - // Refresh cache if needed - if ( - (keyIdentifier.publicKey && - !this.keyInfoByPublicKey.has(keyIdentifier.publicKey)) || - (keyIdentifier.id && !this.keyInfoById.has(keyIdentifier.id)) - ) { - await this.getPublicKeys(userId) - } - - if (keyIdentifier.publicKey) { - return this.keyInfoByPublicKey.get(keyIdentifier.publicKey) - } - if (keyIdentifier.id) { - return this.keyInfoById.get(keyIdentifier.id) - } - return undefined - } - - /** - * Sign a transaction using a key identifier + * Sign a transaction using a public key * @param tx - The transaction to sign, as a string - * @param keyIdentifier - The key identifier (publicKey or id) to use for signing + * @param keyIdentifier - The key identifier (must include publicKey) * @param externalTxId - The transaction ID assigned by the Wallet Gateway * @return The transaction object from Fireblocks */ @@ -351,12 +320,20 @@ export class FireblocksHandler { ): Promise { try { const client = this.getClient(userId) - const key = await this.resolveKey(userId, keyIdentifier) - if (!key) { + if (!keyIdentifier.publicKey) { throw new Error( - `Key identifier not found in vaults: ${JSON.stringify(keyIdentifier)}` + 'Public key is required for Fireblocks signing provider' ) } + const publicKey = keyIdentifier.publicKey + if (!this.keyInfoByPublicKey.has(publicKey)) { + // refresh the keycache + await this.getPublicKeys(userId) + } + const key = this.keyInfoByPublicKey.get(publicKey) + if (!key) { + throw new Error(`Public key ${publicKey} not found in vaults`) + } const transaction = await client.transactions.createTransaction({ transactionRequest: { diff --git a/core/signing-internal/src/controller.ts b/core/signing-internal/src/controller.ts index df01b6574..5610dff0c 100644 --- a/core/signing-internal/src/controller.ts +++ b/core/signing-internal/src/controller.ts @@ -69,7 +69,6 @@ export class InternalSigningDriver implements SigningDriverInterface { } private async resolveKeyFromIdentifier( - userId: string | undefined, keyIdentifier: SignTransactionParams['keyIdentifier'] ): Promise { if (keyIdentifier.publicKey) { @@ -77,12 +76,6 @@ export class InternalSigningDriver implements SigningDriverInterface { keyIdentifier.publicKey ) } - if (!userId) { - return undefined - } - if (keyIdentifier.id) { - return await this.store.getSigningKey(userId, keyIdentifier.id) - } return undefined } @@ -101,8 +94,15 @@ export class InternalSigningDriver implements SigningDriverInterface { }) } + if (!params.keyIdentifier.publicKey) { + return Promise.resolve({ + error: 'key_not_found', + error_description: + 'The provided key identifier must include a publicKey.', + }) + } + const key = await this.resolveKeyFromIdentifier( - _userId, params.keyIdentifier ) @@ -110,7 +110,7 @@ export class InternalSigningDriver implements SigningDriverInterface { return Promise.resolve({ error: 'key_not_found', error_description: - 'The provided key identifier does not exist in the signing store.', + 'The provided public key does not exist in the signing store.', }) } diff --git a/core/signing-lib/src/SigningDriverStore.ts b/core/signing-lib/src/SigningDriverStore.ts index df5607136..847be312a 100644 --- a/core/signing-lib/src/SigningDriverStore.ts +++ b/core/signing-lib/src/SigningDriverStore.ts @@ -13,10 +13,6 @@ export interface SigningDriverStore { keyId: string ): Promise getSigningKeyByPublicKey(publicKey: string): Promise - getSigningKeyByName( - userId: string, - name: string - ): Promise listSigningTransactionsByTxIdsAndPublicKeys( txIds: string[], publicKeys: string[] diff --git a/core/signing-store-sql/src/store-sql.ts b/core/signing-store-sql/src/store-sql.ts index bb202d8bd..fb7b637de 100644 --- a/core/signing-store-sql/src/store-sql.ts +++ b/core/signing-store-sql/src/store-sql.ts @@ -75,19 +75,6 @@ export class StoreSql implements SigningDriverStore, AuthAware { return result ? toSigningKey(result) : undefined } - async getSigningKeyByName( - userId: string, - name: string - ): Promise { - const result = await this.db - .selectFrom('signingKeys') - .selectAll() - .where('userId', '=', userId) - .where('name', '=', name) - .executeTakeFirst() - return result ? toSigningKey(result) : undefined - } - async setSigningKey(userId: string, key: SigningKey): Promise { const serialized = fromSigningKey(key, userId) From 5ad659df35f032464f394f300bb84930883930dc Mon Sep 17 00:00:00 2001 From: Pawel Stepien Date: Mon, 22 Dec 2025 12:45:07 +0100 Subject: [PATCH 3/7] adjust api spec so that generated TS enforces at least one of public key or identifier Signed-off-by: Pawel Stepien --- api-specs/openrpc-signing-api.json | 36 +++++++++++++++++++++-- core/signing-fireblocks/src/fireblocks.ts | 8 +++-- core/signing-lib/src/rpc-gen/typings.ts | 22 ++++++++++++-- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/api-specs/openrpc-signing-api.json b/api-specs/openrpc-signing-api.json index e5729b99d..eefb3d50a 100644 --- a/api-specs/openrpc-signing-api.json +++ b/api-specs/openrpc-signing-api.json @@ -278,8 +278,8 @@ "type": "null", "description": "Represents a null value, used in responses where no data is returned." }, - "KeyIdentifier": { - "title": "KeyIdentifier", + "KeyIdentifierWithPublicKey": { + "title": "KeyIdentifierWithPublicKey", "type": "object", "properties": { "publicKey": { @@ -293,7 +293,37 @@ "description": "Unique identifier for the key" } }, - "minProperties": 1, + "required": ["publicKey"], + "description": "Key identifier with publicKey (id is optional)." + }, + "KeyIdentifierWithId": { + "title": "KeyIdentifierWithId", + "type": "object", + "properties": { + "publicKey": { + "title": "publicKey", + "type": "string", + "description": "Public key to use to sign the transaction." + }, + "id": { + "title": "id", + "type": "string", + "description": "Unique identifier for the key" + } + }, + "required": ["id"], + "description": "Key identifier with id (publicKey is optional)." + }, + "KeyIdentifier": { + "title": "KeyIdentifier", + "oneOf": [ + { + "$ref": "#/components/schemas/KeyIdentifierWithPublicKey" + }, + { + "$ref": "#/components/schemas/KeyIdentifierWithId" + } + ], "description": "Identifier for a key. At least one of publicKey or id must be provided (both can also be provided)." }, "Key": { diff --git a/core/signing-fireblocks/src/fireblocks.ts b/core/signing-fireblocks/src/fireblocks.ts index ae63f9483..d732e8fd7 100644 --- a/core/signing-fireblocks/src/fireblocks.ts +++ b/core/signing-fireblocks/src/fireblocks.ts @@ -8,7 +8,11 @@ import { VaultAccount, } from '@fireblocks/ts-sdk' import { pino } from 'pino' -import { SigningStatus, CC_COIN_TYPE } from '@canton-network/core-signing-lib' +import { + SigningStatus, + CC_COIN_TYPE, + KeyIdentifier, +} from '@canton-network/core-signing-lib' import { z } from 'zod' const RawMessageSchema = z.object({ @@ -315,7 +319,7 @@ export class FireblocksHandler { public async signTransaction( userId: string | undefined, tx: string, - keyIdentifier: { publicKey?: string; id?: string }, + keyIdentifier: KeyIdentifier, externalTxId?: string ): Promise { try { diff --git a/core/signing-lib/src/rpc-gen/typings.ts b/core/signing-lib/src/rpc-gen/typings.ts index 27ab8f312..c761c71c5 100644 --- a/core/signing-lib/src/rpc-gen/typings.ts +++ b/core/signing-lib/src/rpc-gen/typings.ts @@ -29,14 +29,30 @@ export type PublicKey = string export type Id = string /** * - * Identifier for the key to use for signing. At least one of publicKey or id must be provided. + * Key identifier with publicKey (id is optional). * */ -export interface KeyIdentifier { - publicKey?: PublicKey +export interface KeyIdentifierWithPublicKey { + publicKey: PublicKey id?: Id [k: string]: any } +/** + * + * Key identifier with id (publicKey is optional). + * + */ +export interface KeyIdentifierWithId { + publicKey?: PublicKey + id: Id + [k: string]: any +} +/** + * + * Identifier for the key to use for signing. At least one of publicKey or id must be provided. + * + */ +export type KeyIdentifier = KeyIdentifierWithPublicKey | KeyIdentifierWithId /** * * Internal txId used by the Wallet Gateway to store the transaction. From 693faa6afe3f4590d5aa884643c139a5405df63f Mon Sep 17 00:00:00 2001 From: Pawel Stepien Date: Mon, 22 Dec 2025 13:02:07 +0100 Subject: [PATCH 4/7] more clear fireblocks signTransaction args Signed-off-by: Pawel Stepien --- core/signing-fireblocks/src/fireblocks.ts | 9 +++++---- core/signing-fireblocks/src/index.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/signing-fireblocks/src/fireblocks.ts b/core/signing-fireblocks/src/fireblocks.ts index d732e8fd7..5a9a3e3c6 100644 --- a/core/signing-fireblocks/src/fireblocks.ts +++ b/core/signing-fireblocks/src/fireblocks.ts @@ -311,14 +311,15 @@ export class FireblocksHandler { } /** * Sign a transaction using a public key - * @param tx - The transaction to sign, as a string + * @param userId - id of a user to get respective client and keys + * @param txHash - Hash of the transaction to sign * @param keyIdentifier - The key identifier (must include publicKey) * @param externalTxId - The transaction ID assigned by the Wallet Gateway * @return The transaction object from Fireblocks */ public async signTransaction( userId: string | undefined, - tx: string, + txHash: string, keyIdentifier: KeyIdentifier, externalTxId?: string ): Promise { @@ -342,13 +343,13 @@ export class FireblocksHandler { const transaction = await client.transactions.createTransaction({ transactionRequest: { operation: 'RAW', - note: `Signing transaction with key ${key.name || key.publicKey}`, + note: `Signing transaction with public key ${publicKey}`, externalTxId, extraParameters: { rawMessageData: { messages: [ { - content: tx, + content: txHash, derivationPath: key.derivationPath, }, ], diff --git a/core/signing-fireblocks/src/index.ts b/core/signing-fireblocks/src/index.ts index f89e136c3..92f356143 100644 --- a/core/signing-fireblocks/src/index.ts +++ b/core/signing-fireblocks/src/index.ts @@ -84,7 +84,7 @@ export default class FireblocksSigningDriver implements SigningDriverInterface { try { const tx = await this.fireblocks.signTransaction( userId, - params.tx, + params.txHash, params.keyIdentifier, params.internalTxId ) From b0b751e0f56b71c1b127a5eabaffc768b1d14c32 Mon Sep 17 00:00:00 2001 From: Pawel Stepien Date: Mon, 22 Dec 2025 13:41:29 +0100 Subject: [PATCH 5/7] simplify signing internal changes Signed-off-by: Pawel Stepien --- core/signing-internal/src/controller.ts | 97 ++++++++----------- .../src/controller.test.ts | 2 +- 2 files changed, 41 insertions(+), 58 deletions(-) diff --git a/core/signing-internal/src/controller.ts b/core/signing-internal/src/controller.ts index 5610dff0c..80d31bb31 100644 --- a/core/signing-internal/src/controller.ts +++ b/core/signing-internal/src/controller.ts @@ -68,17 +68,6 @@ export class InternalSigningDriver implements SigningDriverInterface { this.store = store } - private async resolveKeyFromIdentifier( - keyIdentifier: SignTransactionParams['keyIdentifier'] - ): Promise { - if (keyIdentifier.publicKey) { - return await this.store.getSigningKeyByPublicKey( - keyIdentifier.publicKey - ) - } - return undefined - } - public controller = (_userId: AuthContext['userId'] | undefined) => buildController({ signTransaction: async ( @@ -86,14 +75,6 @@ export class InternalSigningDriver implements SigningDriverInterface { ): Promise => { // TODO: validate transaction here - if (!_userId) { - return Promise.resolve({ - error: 'userId_not_found', - error_description: - 'User ID is required for all signing operations.', - }) - } - if (!params.keyIdentifier.publicKey) { return Promise.resolve({ error: 'key_not_found', @@ -102,51 +83,53 @@ export class InternalSigningDriver implements SigningDriverInterface { }) } - const key = await this.resolveKeyFromIdentifier( - params.keyIdentifier + const key = await this.store.getSigningKeyByPublicKey( + params.keyIdentifier.publicKey ) - if (!key) { - return Promise.resolve({ - error: 'key_not_found', - error_description: - 'The provided public key does not exist in the signing store.', - }) - } + if (key?.privateKey && _userId) { + const txId = randomUUID() + const signature = signTransactionHash( + params.txHash, + key.privateKey + ) + + const now = new Date() + const internalTransaction: SigningTransaction = { + id: txId, + hash: params.txHash, + signature, + publicKey: params.keyIdentifier.publicKey, + createdAt: now, + status: 'signed', + updatedAt: now, + signedAt: now, + } + + this.store.setSigningTransaction( + _userId, + internalTransaction + ) - if (!key.privateKey) { return Promise.resolve({ - error: 'key_not_suitable', + txId, + status: 'signed', + signature, + } as SignTransactionResult) + } else { + if (!_userId) { + return Promise.resolve({ + error: 'userId_not_found', + error_description: + 'User ID is required for all signing operations.', + }) + } + return Promise.resolve({ + error: 'key_not_found', error_description: - 'The provided key does not have a private key available for signing.', + 'The provided public key does not exist in the signing.', }) } - - const txId = randomUUID() - const signature = signTransactionHash( - params.txHash, - key.privateKey - ) - - const now = new Date() - const internalTransaction: SigningTransaction = { - id: txId, - hash: params.txHash, - signature, - publicKey: key.publicKey, - createdAt: now, - status: 'signed', - updatedAt: now, - signedAt: now, - } - - this.store.setSigningTransaction(_userId, internalTransaction) - - return Promise.resolve({ - txId, - status: 'signed', - signature, - } as SignTransactionResult) }, getTransaction: async ( diff --git a/core/signing-participant/src/controller.test.ts b/core/signing-participant/src/controller.test.ts index ee09689eb..f5f960976 100644 --- a/core/signing-participant/src/controller.test.ts +++ b/core/signing-participant/src/controller.test.ts @@ -28,7 +28,7 @@ test('transaction signature', async () => { .signTransaction({ tx: TEST_TRANSACTION, txHash: TEST_TRANSACTION_HASH, - keyIdentifier: {}, + keyIdentifier: { publicKey: '' }, }) expect(tx.status).toBe('signed') }) From b1dbe36d44201cf3d73d1b893f8b41f066989475 Mon Sep 17 00:00:00 2001 From: Pawel Stepien Date: Tue, 23 Dec 2025 11:56:31 +0100 Subject: [PATCH 6/7] getSigningKeyByName Signed-off-by: Pawel Stepien --- core/signing-lib/src/SigningDriverStore.ts | 4 ++++ core/signing-store-sql/src/store-sql.ts | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/core/signing-lib/src/SigningDriverStore.ts b/core/signing-lib/src/SigningDriverStore.ts index 847be312a..df5607136 100644 --- a/core/signing-lib/src/SigningDriverStore.ts +++ b/core/signing-lib/src/SigningDriverStore.ts @@ -13,6 +13,10 @@ export interface SigningDriverStore { keyId: string ): Promise getSigningKeyByPublicKey(publicKey: string): Promise + getSigningKeyByName( + userId: string, + name: string + ): Promise listSigningTransactionsByTxIdsAndPublicKeys( txIds: string[], publicKeys: string[] diff --git a/core/signing-store-sql/src/store-sql.ts b/core/signing-store-sql/src/store-sql.ts index fb7b637de..bb202d8bd 100644 --- a/core/signing-store-sql/src/store-sql.ts +++ b/core/signing-store-sql/src/store-sql.ts @@ -75,6 +75,19 @@ export class StoreSql implements SigningDriverStore, AuthAware { return result ? toSigningKey(result) : undefined } + async getSigningKeyByName( + userId: string, + name: string + ): Promise { + const result = await this.db + .selectFrom('signingKeys') + .selectAll() + .where('userId', '=', userId) + .where('name', '=', name) + .executeTakeFirst() + return result ? toSigningKey(result) : undefined + } + async setSigningKey(userId: string, key: SigningKey): Promise { const serialized = fromSigningKey(key, userId) From c68bb5c128010c4796a6286dcdf083713124e9c0 Mon Sep 17 00:00:00 2001 From: Pawel Stepien Date: Tue, 23 Dec 2025 12:54:53 +0100 Subject: [PATCH 7/7] adjust blockdaemon driver signTransaction to new keyIdentifier arg Signed-off-by: Pawel Stepien --- core/signing-blockdaemon/src/index.ts | 11 ++++++++++- wallet-gateway/remote/src/user-api/controller.ts | 8 ++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/core/signing-blockdaemon/src/index.ts b/core/signing-blockdaemon/src/index.ts index b1077a428..9d9291025 100644 --- a/core/signing-blockdaemon/src/index.ts +++ b/core/signing-blockdaemon/src/index.ts @@ -49,10 +49,19 @@ export default class BlockdaemonSigningDriver implements SigningDriverInterface params: SignTransactionParams ): Promise => { try { + if (!params.keyIdentifier.publicKey) { + return { + error: 'key_not_found', + error_description: + 'The provided key identifier must include a publicKey.', + } + } const tx = await this.client.signTransaction({ tx: params.tx, txHash: params.txHash, - publicKey: params.publicKey, + keyIdentifier: { + publicKey: params.keyIdentifier.publicKey, + }, internalTxId: params.internalTxId, }) return { diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 0202b1053..973ca8c9d 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -284,7 +284,9 @@ export const userController = ( await driver.signTransaction({ tx: Buffer.from(txPayload).toString('base64'), txHash: transactions.multiHash, - publicKey: key.publicKey, + keyIdentifier: { + publicKey: key.publicKey, + }, internalTxId, }) @@ -549,7 +551,9 @@ export const userController = ( let result = await driver.signTransaction({ tx: preparedTransaction, txHash: preparedTransactionHash, - publicKey: wallet.publicKey, + keyIdentifier: { + publicKey: wallet.publicKey, + }, internalTxId, })