Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -83,6 +83,24 @@ export class SigningCoordinator<WalletMetadata extends {}, AccountMetadata exten
this.#logger = contextLogger(logger, 'SigningCoordinator');
}

/**
* Gets the appropriate TrezorConfig for the given wallet.
*
* This allows wallets to specify only the properties they want to override
* (e.g., derivationType) while inheriting global settings (e.g., communicationType, manifest)
*/
#getTrezorConfig(wallet: AnyBip32Wallet<WalletMetadata, AccountMetadata>): TrezorConfig {
const trezorConfig =
wallet.type === WalletType.Trezor && 'trezorConfig' in wallet.metadata
? (wallet.metadata as { trezorConfig?: Partial<TrezorConfig> }).trezorConfig
: undefined;

return {
...this.#hwOptions, // Global defaults (communicationType, manifest, etc.)
...(trezorConfig || {}) // Wallet-specific overrides (derivationType, etc.)
};
}

async signTransaction(
{ tx, signContext, options }: SignTransactionProps,
requestContext: RequestContext<WalletMetadata, AccountMetadata>
Expand Down Expand Up @@ -127,11 +145,8 @@ export class SigningCoordinator<WalletMetadata extends {}, AccountMetadata exten
if (!emitter$.observed) {
return reject(new WrongTargetError('Not expecting sign requests at this time'));
}
const account = request.requestContext.wallet.accounts.find(
({ accountIndex, purpose = KeyPurpose.STANDARD }) =>
accountIndex === request.requestContext.accountIndex && request.requestContext.purpose === purpose
);

const account = this.#findAccount(request);
if (!account) {
return reject(
new errors.ProofGenerationError(
Expand All @@ -144,67 +159,107 @@ export class SigningCoordinator<WalletMetadata extends {}, AccountMetadata exten
...request,
reject: async (reason: string) => 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<WalletMetadata, AccountMetadata>;
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<R> =>
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<WalletMetadata, AccountMetadata> }) {
return request.requestContext.wallet.accounts.find(
({ accountIndex, purpose = KeyPurpose.STANDARD }) =>
accountIndex === request.requestContext.accountIndex && request.requestContext.purpose === purpose
);
}

#createInMemorySignRequest<R, Req extends RequestBase<WalletMetadata, AccountMetadata> & SignRequest<R>>(
commonRequestProps: Omit<Req, 'reject' | 'sign'>,
account: Bip32WalletAccount<AccountMetadata>,
sign: (keyAgent: KeyAgent) => Promise<R>,
resolve: (result: R | Promise<R>) => void,
reject: (error: unknown) => void
): Req {
return {
...commonRequestProps,
sign: async (passphrase: Uint8Array, options?: SignOptions) =>
bubbleResolveReject(
async () => {
const wallet = commonRequestProps.requestContext.wallet as InMemoryWallet<WalletMetadata, AccountMetadata>;
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<R, Req extends RequestBase<WalletMetadata, AccountMetadata> & SignRequest<R>>(
commonRequestProps: Omit<Req, 'reject' | 'sign'>,
account: Bip32WalletAccount<AccountMetadata>,
sign: (keyAgent: KeyAgent) => Promise<R>,
resolve: (result: R | Promise<R>) => void,
reject: (error: unknown) => void
): Req {
return {
...commonRequestProps,
sign: async (): Promise<R> =>
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<WalletMetadata, AccountMetadata>; walletType: WalletType },
account: Bip32WalletAccount<AccountMetadata>
): Promise<KeyAgent> {
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)
});
}
}
17 changes: 14 additions & 3 deletions packages/web-extension/src/walletManager/types.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,13 +29,24 @@ export type Bip32Wallet<WalletMetadata extends {}, AccountMetadata extends {}> =
blockchainName?: Blockchain;
};

export type HardwareWallet<WalletMetadata extends {}, AccountMetadata extends {}> = Bip32Wallet<
export type LedgerHardwareWallet<WalletMetadata extends {}, AccountMetadata extends {}> = Bip32Wallet<
WalletMetadata,
AccountMetadata
> & {
type: WalletType.Ledger | WalletType.Trezor;
type: WalletType.Ledger;
};

export type TrezorHardwareWallet<
WalletMetadata extends { trezorConfig?: Partial<TrezorConfig> },
AccountMetadata extends {}
> = Bip32Wallet<WalletMetadata, AccountMetadata> & {
type: WalletType.Trezor;
};

export type HardwareWallet<WalletMetadata extends {}, AccountMetadata extends {}> =
| LedgerHardwareWallet<WalletMetadata, AccountMetadata>
| TrezorHardwareWallet<WalletMetadata, AccountMetadata>;

export type InMemoryWallet<WalletMetadata extends {}, AccountMetadata extends {}> = Bip32Wallet<
WalletMetadata,
AccountMetadata
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -73,7 +77,9 @@ describe('SigningCoordinator', () => {
signCip8Data: jest.fn(),
signTransaction: jest.fn()
} as unknown as jest.Mocked<InMemoryKeyAgent>;

keyAgentFactory.InMemory.mockResolvedValue(keyAgent);
keyAgentFactory.Trezor.mockResolvedValue(keyAgent as unknown as Awaited<ReturnType<KeyAgentFactory['Trezor']>>);
});

describe('signTransaction', () => {
Expand Down Expand Up @@ -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<TrezorConfig> }, {}> = {
accounts: [createAccount(0, 0)],
metadata: {
trezorConfig: {
communicationType: CommunicationType.Web,
derivationType: 'ICARUS_TREZOR' as MasterKeyGeneration,
manifest: {
appUrl: 'https://custom.app',
email: '[email protected]'
}
}
},
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<Cardano.Signatures> };
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: '[email protected]' // Wallet override
}
}
});
});
});

describe('signData', () => {
Expand Down
Loading