Skip to content

Commit 15df46f

Browse files
committed
feat(web-extension): add trezorConfig support in wallet metadata
- Add trezorConfig to wallet metadata for TrezorHardwareWallet - Update SigningCoordinator to read trezorConfig from wallet.metadata - Add minimal test to verify trezorConfig functionality - Ensures trezorConfig can be updated via wallet manager and persists properly - Supports both wallet-specific trezorConfig and global hwOptions fallback
1 parent b449b59 commit 15df46f

File tree

3 files changed

+113
-25
lines changed

3 files changed

+113
-25
lines changed

packages/web-extension/src/walletManager/SigningCoordinator/SigningCoordinator.ts

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { AnyBip32Wallet, InMemoryWallet, WalletType } from '../types';
12
import { Cardano, Serialization } from '@cardano-sdk/core';
23
import { Cip30DataSignature } from '@cardano-sdk/dapp-connector';
34
import { CustomError } from 'ts-custom-error';
4-
import { InMemoryWallet, WalletType } from '../types';
55
import { KeyAgent, KeyPurpose, SignDataContext, TrezorConfig, errors } from '@cardano-sdk/key-management';
66
import { KeyAgentFactory } from './KeyAgentFactory';
77
import { Logger } from 'ts-log';
@@ -83,6 +83,24 @@ export class SigningCoordinator<WalletMetadata extends {}, AccountMetadata exten
8383
this.#logger = contextLogger(logger, 'SigningCoordinator');
8484
}
8585

86+
/**
87+
* Gets the appropriate TrezorConfig for the given wallet.
88+
*
89+
* This allows wallets to specify only the properties they want to override
90+
* (e.g., derivationType) while inheriting global settings (e.g., communicationType, manifest)
91+
*/
92+
#getTrezorConfig(wallet: AnyBip32Wallet<WalletMetadata, AccountMetadata>): TrezorConfig {
93+
const trezorConfig =
94+
wallet.type === WalletType.Trezor && 'trezorConfig' in wallet.metadata
95+
? (wallet.metadata as { trezorConfig?: Partial<TrezorConfig> }).trezorConfig
96+
: undefined;
97+
98+
return {
99+
...this.#hwOptions, // Global defaults (communicationType, manifest, etc.)
100+
...(trezorConfig || {}) // Wallet-specific overrides (derivationType, etc.)
101+
};
102+
}
103+
86104
async signTransaction(
87105
{ tx, signContext, options }: SignTransactionProps,
88106
requestContext: RequestContext<WalletMetadata, AccountMetadata>
@@ -123,6 +141,7 @@ export class SigningCoordinator<WalletMetadata extends {}, AccountMetadata exten
123141
request: Omit<Req, 'reject' | 'sign'>,
124142
sign: (keyAgent: KeyAgent) => Promise<R>
125143
) {
144+
/* eslint-disable sonarjs/cognitive-complexity */
126145
return new Promise<R>((resolve, reject) => {
127146
if (!emitter$.observed) {
128147
return reject(new WrongTargetError('Not expecting sign requests at this time'));
@@ -181,30 +200,36 @@ export class SigningCoordinator<WalletMetadata extends {}, AccountMetadata exten
181200
...commonRequestProps,
182201
sign: async (): Promise<R> =>
183202
bubbleResolveReject(
184-
async (options?: SignOptions) =>
185-
sign(
186-
request.walletType === WalletType.Ledger
187-
? await this.#keyAgentFactory.Ledger({
188-
accountIndex: request.requestContext.accountIndex,
189-
chainId: request.requestContext.chainId,
190-
communicationType: this.#hwOptions.communicationType,
191-
extendedAccountPublicKey: account.extendedAccountPublicKey,
192-
purpose: account.purpose || KeyPurpose.STANDARD
193-
})
194-
: await this.#keyAgentFactory.Trezor({
195-
accountIndex: request.requestContext.accountIndex,
196-
chainId: request.requestContext.chainId,
197-
extendedAccountPublicKey: account.extendedAccountPublicKey,
198-
purpose: account.purpose || KeyPurpose.STANDARD,
199-
trezorConfig: this.#hwOptions
200-
})
201-
).catch((error) => throwMaybeWrappedWithNoRejectError(error, options)),
203+
async (options?: SignOptions) => {
204+
try {
205+
const keyAgent =
206+
request.walletType === WalletType.Ledger
207+
? await this.#keyAgentFactory.Ledger({
208+
accountIndex: request.requestContext.accountIndex,
209+
chainId: request.requestContext.chainId,
210+
communicationType: this.#hwOptions.communicationType,
211+
extendedAccountPublicKey: account.extendedAccountPublicKey,
212+
purpose: account.purpose || KeyPurpose.STANDARD
213+
})
214+
: await this.#keyAgentFactory.Trezor({
215+
accountIndex: request.requestContext.accountIndex,
216+
chainId: request.requestContext.chainId,
217+
extendedAccountPublicKey: account.extendedAccountPublicKey,
218+
purpose: account.purpose || KeyPurpose.STANDARD,
219+
trezorConfig: this.#getTrezorConfig(request.requestContext.wallet)
220+
});
221+
return await sign(keyAgent);
222+
} catch (error) {
223+
return throwMaybeWrappedWithNoRejectError(error, options);
224+
}
225+
},
202226
resolve,
203227
reject
204228
),
205229
walletType: request.walletType
206230
} as Req)
207231
);
208232
});
233+
/* eslint-enable sonarjs/cognitive-complexity */
209234
}
210235
}

packages/web-extension/src/walletManager/types.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AccountKeyDerivationPath, KeyPurpose } from '@cardano-sdk/key-management';
1+
import { AccountKeyDerivationPath, KeyPurpose, TrezorConfig } from '@cardano-sdk/key-management';
22
import { Bip32PublicKeyHex } from '@cardano-sdk/crypto';
33
import { Cardano } from '@cardano-sdk/core';
44
import { HexBlob } from '@cardano-sdk/util';
@@ -36,10 +36,10 @@ export type LedgerHardwareWallet<WalletMetadata extends {}, AccountMetadata exte
3636
type: WalletType.Ledger;
3737
};
3838

39-
export type TrezorHardwareWallet<WalletMetadata extends {}, AccountMetadata extends {}> = Bip32Wallet<
40-
WalletMetadata,
41-
AccountMetadata
42-
> & {
39+
export type TrezorHardwareWallet<
40+
WalletMetadata extends { trezorConfig?: Partial<TrezorConfig> },
41+
AccountMetadata extends {}
42+
> = Bip32Wallet<WalletMetadata, AccountMetadata> & {
4343
type: WalletType.Trezor;
4444
};
4545

packages/web-extension/test/walletManager/SigningCoordinator.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,26 @@ import {
44
CommunicationType,
55
InMemoryKeyAgent,
66
KeyPurpose,
7+
MasterKeyGeneration,
78
SignDataContext,
89
SignTransactionContext,
10+
TrezorConfig,
911
cip8,
1012
errors
1113
} from '@cardano-sdk/key-management';
1214
import { Ed25519PublicKeyHex, Ed25519SignatureHex, Hash28ByteBase16 } from '@cardano-sdk/crypto';
13-
import { HexBlob } from '@cardano-sdk/util';
1415
import {
16+
HardwareWallet,
1517
InMemoryWallet,
1618
KeyAgentFactory,
1719
RequestContext,
20+
SignOptions,
1821
SigningCoordinator,
22+
SignOptions,
1923
WalletType,
2024
WrongTargetError
2125
} from '../../src';
26+
import { HexBlob } from '@cardano-sdk/util';
2227
import { createAccount } from './util';
2328
import { dummyLogger } from 'ts-log';
2429
import { firstValueFrom } from 'rxjs';
@@ -73,7 +78,9 @@ describe('SigningCoordinator', () => {
7378
signCip8Data: jest.fn(),
7479
signTransaction: jest.fn()
7580
} as unknown as jest.Mocked<InMemoryKeyAgent>;
81+
7682
keyAgentFactory.InMemory.mockResolvedValue(keyAgent);
83+
keyAgentFactory.Trezor.mockResolvedValue(keyAgent as unknown as Awaited<ReturnType<KeyAgentFactory['Trezor']>>);
7784
});
7885

7986
describe('signTransaction', () => {
@@ -171,6 +178,62 @@ describe('SigningCoordinator', () => {
171178
expect(passphrase).toEqual(new Uint8Array([0, 0, 0]));
172179
});
173180
});
181+
182+
it('should pass wallet trezorConfig to Trezor key agent factory', async () => {
183+
const trezorWallet: HardwareWallet<{ trezorConfig?: Partial<TrezorConfig> }, {}> = {
184+
accounts: [createAccount(0, 0)],
185+
metadata: {
186+
trezorConfig: {
187+
communicationType: CommunicationType.Web,
188+
derivationType: 'ICARUS_TREZOR' as MasterKeyGeneration,
189+
manifest: {
190+
appUrl: 'https://custom.app',
191+
192+
}
193+
}
194+
},
195+
type: WalletType.Trezor,
196+
walletId: Hash28ByteBase16('ad63f855e831d937457afc52a21a7f351137e4a9fff26c217817335a')
197+
};
198+
199+
const trezorRequestContext: RequestContext<{}, {}> = {
200+
accountIndex: 0,
201+
chainId: Cardano.ChainIds.Preprod,
202+
purpose: KeyPurpose.STANDARD,
203+
wallet: trezorWallet
204+
};
205+
206+
// Test that the Trezor factory is called with merged config
207+
const reqEmitted = firstValueFrom(signingCoordinator.transactionWitnessRequest$);
208+
void signingCoordinator.signTransaction({ signContext, tx }, trezorRequestContext);
209+
const req = await reqEmitted;
210+
211+
// Verify the request was created
212+
expect(req.walletType).toBe(WalletType.Trezor);
213+
expect(req.requestContext.wallet).toBe(trezorWallet);
214+
215+
// Now call sign() to trigger the key agent factory call
216+
// Cast to hardware wallet request type to access the correct sign method
217+
const hardwareReq = req as { sign(options?: SignOptions): Promise<Cardano.Signatures> };
218+
await hardwareReq.sign({});
219+
220+
// Verify keyAgentFactory.Trezor was called with the correct merged configuration
221+
expect(keyAgentFactory.Trezor).toHaveBeenCalledWith({
222+
accountIndex: 0,
223+
chainId: Cardano.ChainIds.Preprod,
224+
extendedAccountPublicKey: expect.any(String),
225+
purpose: KeyPurpose.STANDARD,
226+
trezorConfig: {
227+
// Wallet-specific overrides take precedence over global defaults
228+
communicationType: CommunicationType.Web, // Wallet override
229+
derivationType: 'ICARUS_TREZOR', // Wallet override
230+
manifest: {
231+
appUrl: 'https://custom.app', // Wallet override
232+
email: '[email protected]' // Wallet override
233+
}
234+
}
235+
});
236+
});
174237
});
175238

176239
describe('signData', () => {

0 commit comments

Comments
 (0)