diff --git a/packages/web-extension/src/walletManager/SigningCoordinator/SigningCoordinator.ts b/packages/web-extension/src/walletManager/SigningCoordinator/SigningCoordinator.ts index 4760d338548..b78b7a860e5 100644 --- a/packages/web-extension/src/walletManager/SigningCoordinator/SigningCoordinator.ts +++ b/packages/web-extension/src/walletManager/SigningCoordinator/SigningCoordinator.ts @@ -1,7 +1,7 @@ +import { AnyBip32Wallet, Bip32WalletAccount, InMemoryWallet, WalletType } from '../types'; import { Cardano, Serialization } from '@cardano-sdk/core'; import { Cip30DataSignature } from '@cardano-sdk/dapp-connector'; import { CustomError } from 'ts-custom-error'; -import { InMemoryWallet, WalletType } from '../types'; import { KeyAgent, KeyPurpose, SignDataContext, TrezorConfig, errors } from '@cardano-sdk/key-management'; import { KeyAgentFactory } from './KeyAgentFactory'; import { Logger } from 'ts-log'; @@ -83,6 +83,24 @@ export class SigningCoordinator): TrezorConfig { + const trezorConfig = + wallet.type === WalletType.Trezor && 'trezorConfig' in wallet.metadata + ? (wallet.metadata as { trezorConfig?: Partial }).trezorConfig + : undefined; + + return { + ...this.#hwOptions, // Global defaults (communicationType, manifest, etc.) + ...(trezorConfig || {}) // Wallet-specific overrides (derivationType, etc.) + }; + } + async signTransaction( { tx, signContext, options }: SignTransactionProps, requestContext: RequestContext @@ -127,11 +145,8 @@ export class SigningCoordinator - accountIndex === request.requestContext.accountIndex && request.requestContext.purpose === purpose - ); + const account = this.#findAccount(request); if (!account) { return reject( new errors.ProofGenerationError( @@ -144,67 +159,107 @@ export class SigningCoordinator reject(new errors.AuthenticationError(reason)) }; - emitter$.next( + + const signRequest = request.walletType === WalletType.InMemory - ? ({ - ...commonRequestProps, - sign: async (passphrase: Uint8Array, options?: SignOptions) => - bubbleResolveReject( - async () => { - const wallet = request.requestContext.wallet as InMemoryWallet; - try { - const result = await sign( - await this.#keyAgentFactory.InMemory({ - accountIndex: account.accountIndex, - chainId: request.requestContext.chainId, - encryptedRootPrivateKeyBytes: [ - ...Buffer.from(wallet.encryptedSecrets.rootPrivateKeyBytes, 'hex') - ], - extendedAccountPublicKey: account.extendedAccountPublicKey, - getPassphrase: async () => passphrase, - purpose: account.purpose || KeyPurpose.STANDARD - }) - ); - clearPassphrase(passphrase); - return result; - } catch (error) { - clearPassphrase(passphrase); - return throwMaybeWrappedWithNoRejectError(error, options); - } - }, - resolve, - reject - ), - walletType: request.walletType - } as Req) - : ({ - ...commonRequestProps, - sign: async (): Promise => - bubbleResolveReject( - async (options?: SignOptions) => - sign( - request.walletType === WalletType.Ledger - ? await this.#keyAgentFactory.Ledger({ - accountIndex: request.requestContext.accountIndex, - chainId: request.requestContext.chainId, - communicationType: this.#hwOptions.communicationType, - extendedAccountPublicKey: account.extendedAccountPublicKey, - purpose: account.purpose || KeyPurpose.STANDARD - }) - : await this.#keyAgentFactory.Trezor({ - accountIndex: request.requestContext.accountIndex, - chainId: request.requestContext.chainId, - extendedAccountPublicKey: account.extendedAccountPublicKey, - purpose: account.purpose || KeyPurpose.STANDARD, - trezorConfig: this.#hwOptions - }) - ).catch((error) => throwMaybeWrappedWithNoRejectError(error, options)), - resolve, - reject - ), - walletType: request.walletType - } as Req) - ); + ? this.#createInMemorySignRequest(commonRequestProps, account, sign, resolve, reject) + : this.#createHardwareSignRequest(commonRequestProps, account, sign, resolve, reject); + + emitter$.next(signRequest); + }); + } + + #findAccount(request: { requestContext: RequestContext }) { + return request.requestContext.wallet.accounts.find( + ({ accountIndex, purpose = KeyPurpose.STANDARD }) => + accountIndex === request.requestContext.accountIndex && request.requestContext.purpose === purpose + ); + } + + #createInMemorySignRequest & SignRequest>( + commonRequestProps: Omit, + account: Bip32WalletAccount, + sign: (keyAgent: KeyAgent) => Promise, + resolve: (result: R | Promise) => void, + reject: (error: unknown) => void + ): Req { + return { + ...commonRequestProps, + sign: async (passphrase: Uint8Array, options?: SignOptions) => + bubbleResolveReject( + async () => { + const wallet = commonRequestProps.requestContext.wallet as InMemoryWallet; + try { + const result = await sign( + await this.#keyAgentFactory.InMemory({ + accountIndex: account.accountIndex, + chainId: commonRequestProps.requestContext.chainId, + encryptedRootPrivateKeyBytes: [...Buffer.from(wallet.encryptedSecrets.rootPrivateKeyBytes, 'hex')], + extendedAccountPublicKey: account.extendedAccountPublicKey, + getPassphrase: async () => passphrase, + purpose: account.purpose || KeyPurpose.STANDARD + }) + ); + clearPassphrase(passphrase); + return result; + } catch (error) { + clearPassphrase(passphrase); + return throwMaybeWrappedWithNoRejectError(error, options); + } + }, + resolve, + reject + ), + walletType: commonRequestProps.walletType + } as Req; + } + + #createHardwareSignRequest & SignRequest>( + commonRequestProps: Omit, + account: Bip32WalletAccount, + sign: (keyAgent: KeyAgent) => Promise, + resolve: (result: R | Promise) => void, + reject: (error: unknown) => void + ): Req { + return { + ...commonRequestProps, + sign: async (): Promise => + bubbleResolveReject( + async (options?: SignOptions) => { + try { + const keyAgent = await this.#createHardwareKeyAgent(commonRequestProps, account); + return await sign(keyAgent); + } catch (error) { + return throwMaybeWrappedWithNoRejectError(error, options); + } + }, + resolve, + reject + ), + walletType: commonRequestProps.walletType + } as Req; + } + + async #createHardwareKeyAgent( + request: { requestContext: RequestContext; walletType: WalletType }, + account: Bip32WalletAccount + ): Promise { + if (request.walletType === WalletType.Ledger) { + return await this.#keyAgentFactory.Ledger({ + accountIndex: request.requestContext.accountIndex, + chainId: request.requestContext.chainId, + communicationType: this.#hwOptions.communicationType, + extendedAccountPublicKey: account.extendedAccountPublicKey, + purpose: account.purpose || KeyPurpose.STANDARD + }); + } + + return await this.#keyAgentFactory.Trezor({ + accountIndex: request.requestContext.accountIndex, + chainId: request.requestContext.chainId, + extendedAccountPublicKey: account.extendedAccountPublicKey, + purpose: account.purpose || KeyPurpose.STANDARD, + trezorConfig: this.#getTrezorConfig(request.requestContext.wallet) }); } } diff --git a/packages/web-extension/src/walletManager/types.ts b/packages/web-extension/src/walletManager/types.ts index cadd6dab8ee..59a0ad7fc00 100644 --- a/packages/web-extension/src/walletManager/types.ts +++ b/packages/web-extension/src/walletManager/types.ts @@ -1,4 +1,4 @@ -import { AccountKeyDerivationPath, KeyPurpose } from '@cardano-sdk/key-management'; +import { AccountKeyDerivationPath, KeyPurpose, TrezorConfig } from '@cardano-sdk/key-management'; import { Bip32PublicKeyHex } from '@cardano-sdk/crypto'; import { Cardano } from '@cardano-sdk/core'; import { HexBlob } from '@cardano-sdk/util'; @@ -29,13 +29,24 @@ export type Bip32Wallet = blockchainName?: Blockchain; }; -export type HardwareWallet = Bip32Wallet< +export type LedgerHardwareWallet = Bip32Wallet< WalletMetadata, AccountMetadata > & { - type: WalletType.Ledger | WalletType.Trezor; + type: WalletType.Ledger; }; +export type TrezorHardwareWallet< + WalletMetadata extends { trezorConfig?: Partial }, + AccountMetadata extends {} +> = Bip32Wallet & { + type: WalletType.Trezor; +}; + +export type HardwareWallet = + | LedgerHardwareWallet + | TrezorHardwareWallet; + export type InMemoryWallet = Bip32Wallet< WalletMetadata, AccountMetadata diff --git a/packages/web-extension/test/walletManager/SigningCoordinator.test.ts b/packages/web-extension/test/walletManager/SigningCoordinator.test.ts index 1b86a6ff373..c070ba41c77 100644 --- a/packages/web-extension/test/walletManager/SigningCoordinator.test.ts +++ b/packages/web-extension/test/walletManager/SigningCoordinator.test.ts @@ -4,21 +4,25 @@ import { CommunicationType, InMemoryKeyAgent, KeyPurpose, + MasterKeyGeneration, SignDataContext, SignTransactionContext, + TrezorConfig, cip8, errors } from '@cardano-sdk/key-management'; import { Ed25519PublicKeyHex, Ed25519SignatureHex, Hash28ByteBase16 } from '@cardano-sdk/crypto'; -import { HexBlob } from '@cardano-sdk/util'; import { + HardwareWallet, InMemoryWallet, KeyAgentFactory, RequestContext, + SignOptions, SigningCoordinator, WalletType, WrongTargetError } from '../../src'; +import { HexBlob } from '@cardano-sdk/util'; import { createAccount } from './util'; import { dummyLogger } from 'ts-log'; import { firstValueFrom } from 'rxjs'; @@ -73,7 +77,9 @@ describe('SigningCoordinator', () => { signCip8Data: jest.fn(), signTransaction: jest.fn() } as unknown as jest.Mocked; + keyAgentFactory.InMemory.mockResolvedValue(keyAgent); + keyAgentFactory.Trezor.mockResolvedValue(keyAgent as unknown as Awaited>); }); describe('signTransaction', () => { @@ -171,6 +177,62 @@ describe('SigningCoordinator', () => { expect(passphrase).toEqual(new Uint8Array([0, 0, 0])); }); }); + + it('should pass wallet trezorConfig to Trezor key agent factory', async () => { + const trezorWallet: HardwareWallet<{ trezorConfig?: Partial }, {}> = { + accounts: [createAccount(0, 0)], + metadata: { + trezorConfig: { + communicationType: CommunicationType.Web, + derivationType: 'ICARUS_TREZOR' as MasterKeyGeneration, + manifest: { + appUrl: 'https://custom.app', + email: 'custom@custom.app' + } + } + }, + type: WalletType.Trezor, + walletId: Hash28ByteBase16('ad63f855e831d937457afc52a21a7f351137e4a9fff26c217817335a') + }; + + const trezorRequestContext: RequestContext<{}, {}> = { + accountIndex: 0, + chainId: Cardano.ChainIds.Preprod, + purpose: KeyPurpose.STANDARD, + wallet: trezorWallet + }; + + // Test that the Trezor factory is called with merged config + const reqEmitted = firstValueFrom(signingCoordinator.transactionWitnessRequest$); + void signingCoordinator.signTransaction({ signContext, tx }, trezorRequestContext); + const req = await reqEmitted; + + // Verify the request was created + expect(req.walletType).toBe(WalletType.Trezor); + expect(req.requestContext.wallet).toBe(trezorWallet); + + // Now call sign() to trigger the key agent factory call + // Cast to hardware wallet request type to access the correct sign method + const hardwareReq = req as { sign(options?: SignOptions): Promise }; + await hardwareReq.sign({}); + + // Verify keyAgentFactory.Trezor was called with the correct merged configuration + expect(keyAgentFactory.Trezor).toHaveBeenCalledWith({ + accountIndex: 0, + chainId: Cardano.ChainIds.Preprod, + extendedAccountPublicKey: expect.any(String), + purpose: KeyPurpose.STANDARD, + trezorConfig: { + // Wallet-specific overrides take precedence over global defaults + communicationType: CommunicationType.Web, // Wallet override + derivationType: 'ICARUS_TREZOR', // Wallet override + manifest: { + appUrl: 'https://custom.app', // Wallet override + email: 'custom@custom.app' // Wallet override + } + } + }); + }); }); describe('signData', () => {