Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e3d3503

Browse files
committedFeb 26, 2025··
feat: sign multi-sig transaction with trezor
1 parent 06289fb commit e3d3503

File tree

6 files changed

+162
-17
lines changed

6 files changed

+162
-17
lines changed
 

‎packages/core/src/Cardano/util/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './resolveInputValue';
55
export * from './phase2Validation';
66
export * from './addressesShareAnyKey';
77
export * from './plutusDataUtils';
8+
export * from './isScriptAddress';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Address, CredentialType } from '../Address';
2+
3+
// TODO: Would like to verify if this is needed before writing test for it
4+
export const isScriptAddress = (address: string): boolean => {
5+
const baseAddress = Address.fromBech32(address).asBase();
6+
const paymentCredential = baseAddress?.getPaymentCredential();
7+
const stakeCredential = baseAddress?.getStakeCredential();
8+
return paymentCredential?.type === CredentialType.ScriptHash && stakeCredential?.type === CredentialType.ScriptHash;
9+
};

‎packages/hardware-ledger/src/transformers/txOut.ts

+1-11
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,6 @@ import { LedgerTxTransformerContext } from '../types';
55
import { mapTokenMap } from './assets';
66
import { util } from '@cardano-sdk/key-management';
77

8-
const isScriptAddress = (address: string): boolean => {
9-
const baseAddress = Cardano.Address.fromBech32(address).asBase();
10-
const paymentCredential = baseAddress?.getPaymentCredential();
11-
const stakeCredential = baseAddress?.getStakeCredential();
12-
return (
13-
paymentCredential?.type === Cardano.CredentialType.ScriptHash &&
14-
stakeCredential?.type === Cardano.CredentialType.ScriptHash
15-
);
16-
};
17-
188
const toInlineDatum: Transform<Cardano.PlutusData, Ledger.Datum> = (datum) => ({
199
datumHex: Serialization.PlutusData.fromCore(datum).toCbor(),
2010
type: Ledger.DatumType.INLINE
@@ -30,7 +20,7 @@ const toDestination: Transform<Cardano.TxOut, Ledger.TxOutputDestination, Ledger
3020
context
3121
) => {
3222
const knownAddress = context?.knownAddresses.find((address) => address.address === txOut.address);
33-
const isScriptWallet = isScriptAddress(txOut.address);
23+
const isScriptWallet = Cardano.util.isScriptAddress(txOut.address);
3424

3525
if (knownAddress && !isScriptWallet) {
3626
const paymentKeyPath = util.paymentKeyPathFromGroupedAddress(knownAddress);

‎packages/hardware-trezor/src/TrezorKeyAgent.ts

+32-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import * as Crypto from '@cardano-sdk/crypto';
33
import * as Trezor from '@trezor/connect';
4+
import { BIP32Path } from '@cardano-sdk/crypto';
45
import { Cardano, NotImplementedError, Serialization } from '@cardano-sdk/core';
56
import {
67
CardanoKeyConst,
@@ -9,6 +10,7 @@ import {
910
KeyAgentDependencies,
1011
KeyAgentType,
1112
KeyPurpose,
13+
KeyRole,
1214
SerializableTrezorKeyAgentData,
1315
SignBlobResult,
1416
SignTransactionContext,
@@ -68,6 +70,10 @@ const containsOnlyScriptHashCredentials = (tx: Omit<Trezor.CardanoSignTransactio
6870
return !tx.withdrawals?.some((withdrawal) => !withdrawal.scriptHash);
6971
};
7072

73+
const multiSigWitnessPaths: BIP32Path[] = [
74+
util.accountKeyDerivationPathToBip32Path(0, { index: 0, role: KeyRole.External }, KeyPurpose.MULTI_SIG)
75+
];
76+
7177
const isMultiSig = (tx: Omit<Trezor.CardanoSignTransaction, 'signingMode'>): boolean => {
7278
const allThirdPartyInputs = !tx.inputs.some((input) => input.path);
7379
// Trezor doesn't allow change outputs to address controlled by your keys and instead you have to use script address for change out
@@ -116,6 +122,21 @@ export class TrezorKeyAgent extends KeyAgentBase {
116122
// Show Trezor Suite popup. Disabled for node based apps
117123
popup: communicationType !== CommunicationType.Node && !silentMode
118124
});
125+
126+
trezorConnect.on(Trezor.UI_EVENT, (event) => {
127+
// React on ui-request_passphrase event
128+
if (event.type === Trezor.UI.REQUEST_PASSPHRASE && event.payload.device) {
129+
trezorConnect.uiResponse({
130+
payload: {
131+
passphraseOnDevice: true,
132+
save: true,
133+
value: ''
134+
},
135+
type: Trezor.UI.RECEIVE_PASSPHRASE
136+
});
137+
}
138+
});
139+
119140
return true;
120141
} catch (error: any) {
121142
if (error.code === 'Init_AlreadyInitialized') return true;
@@ -235,14 +256,20 @@ export class TrezorKeyAgent extends KeyAgentBase {
235256
const trezorConnect = getTrezorConnect(this.#communicationType);
236257
const result = await trezorConnect.cardanoSignTransaction({
237258
...trezorTxData,
259+
...(signingMode === Trezor.PROTO.CardanoTxSigningMode.MULTISIG_TRANSACTION && {
260+
additionalWitnessRequests: multiSigWitnessPaths
261+
}),
238262
signingMode
239263
});
240264

241-
const expectedPublicKeys = await Promise.all(
242-
util
243-
.ownSignatureKeyPaths(body, knownAddresses, txInKeyPathMap)
244-
.map((derivationPath) => this.derivePublicKey(derivationPath))
245-
);
265+
const expectedPublicKeys =
266+
signingMode === Trezor.PROTO.CardanoTxSigningMode.MULTISIG_TRANSACTION
267+
? [await this.derivePublicKey({ index: 0, role: KeyRole.External })]
268+
: await Promise.all(
269+
util
270+
.ownSignatureKeyPaths(body, knownAddresses, txInKeyPathMap)
271+
.map((derivationPath) => this.derivePublicKey(derivationPath))
272+
);
246273

247274
if (!result.success) {
248275
throw new errors.TransportError('Failed to export extended account public key', result.payload);

‎packages/hardware-trezor/src/transformers/txOut.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ const toDestination: Transform<Cardano.TxOut, TrezorTxOutputDestination, TrezorT
1010
context
1111
) => {
1212
const knownAddress = context?.knownAddresses.find((address: GroupedAddress) => address.address === txOut.address);
13+
const isScriptWallet = Cardano.util.isScriptAddress(txOut.address);
1314

14-
if (!knownAddress) {
15+
if (!knownAddress || isScriptWallet) {
1516
return {
1617
address: txOut.address
1718
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import * as Crypto from '@cardano-sdk/crypto';
2+
import { BaseWallet, createSharedWallet } from '../../../src';
3+
import { Cardano } from '@cardano-sdk/core';
4+
import { CommunicationType, KeyPurpose, KeyRole, util } from '@cardano-sdk/key-management';
5+
import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction';
6+
import { TrezorKeyAgent } from '@cardano-sdk/hardware-trezor';
7+
import { dummyLogger as logger } from 'ts-log';
8+
import { mockProviders as mocks } from '@cardano-sdk/util-dev';
9+
10+
describe('TrezorSharedWalletKeyAgent', () => {
11+
let wallet: BaseWallet;
12+
let trezorKeyAgent: TrezorKeyAgent;
13+
let txSubmitProvider: mocks.TxSubmitProviderStub;
14+
15+
const trezorConfig = {
16+
communicationType: CommunicationType.Node,
17+
manifest: {
18+
appUrl: 'https://your.application.com',
19+
email: 'email@developer.com'
20+
}
21+
};
22+
23+
beforeAll(async () => {
24+
txSubmitProvider = mocks.mockTxSubmitProvider();
25+
trezorKeyAgent = await TrezorKeyAgent.createWithDevice(
26+
{
27+
chainId: Cardano.ChainIds.Preprod,
28+
purpose: KeyPurpose.MULTI_SIG,
29+
trezorConfig
30+
},
31+
{
32+
bip32Ed25519: await Crypto.SodiumBip32Ed25519.create(),
33+
logger
34+
}
35+
);
36+
37+
const walletPubKey = await trezorKeyAgent.derivePublicKey({ index: 0, role: KeyRole.External });
38+
const walletKeyHash = trezorKeyAgent.bip32Ed25519.getPubKeyHash(walletPubKey);
39+
40+
const walletStakePubKey = await trezorKeyAgent.derivePublicKey({ index: 0, role: KeyRole.Stake });
41+
const walletStakeKeyHash = trezorKeyAgent.bip32Ed25519.getPubKeyHash(walletStakePubKey);
42+
43+
const paymentScript: Cardano.NativeScript = {
44+
__type: Cardano.ScriptType.Native,
45+
kind: Cardano.NativeScriptKind.RequireAnyOf,
46+
scripts: [
47+
{
48+
__type: Cardano.ScriptType.Native,
49+
keyHash: walletKeyHash,
50+
kind: Cardano.NativeScriptKind.RequireSignature
51+
},
52+
{
53+
__type: Cardano.ScriptType.Native,
54+
keyHash: Crypto.Ed25519KeyHashHex('b275b08c999097247f7c17e77007c7010cd19f20cc086ad99d398539'),
55+
kind: Cardano.NativeScriptKind.RequireSignature
56+
}
57+
]
58+
};
59+
60+
const stakingScript: Cardano.NativeScript = {
61+
__type: Cardano.ScriptType.Native,
62+
kind: Cardano.NativeScriptKind.RequireAnyOf,
63+
scripts: [
64+
{
65+
__type: Cardano.ScriptType.Native,
66+
keyHash: walletStakeKeyHash,
67+
kind: Cardano.NativeScriptKind.RequireSignature
68+
},
69+
{
70+
__type: Cardano.ScriptType.Native,
71+
keyHash: Crypto.Ed25519KeyHashHex('b275b08c999097247f7c17e77007c7010cd19f20cc086ad99d398539'),
72+
kind: Cardano.NativeScriptKind.RequireSignature
73+
}
74+
]
75+
};
76+
77+
wallet = createSharedWallet(
78+
{ name: 'Shared HW Wallet' },
79+
{
80+
assetProvider: mocks.mockAssetProvider(),
81+
chainHistoryProvider: mocks.mockChainHistoryProvider(),
82+
logger,
83+
networkInfoProvider: mocks.mockNetworkInfoProvider(),
84+
paymentScript,
85+
rewardAccountInfoProvider: mocks.mockRewardAccountInfoProvider(),
86+
rewardsProvider: mocks.mockRewardsProvider(),
87+
stakingScript,
88+
// txSubmitProvider: mocks.mockTxSubmitProvider(),
89+
txSubmitProvider,
90+
utxoProvider: mocks.mockUtxoProvider(),
91+
witnesser: util.createBip32Ed25519Witnesser(util.createAsyncKeyAgent(trezorKeyAgent))
92+
}
93+
);
94+
});
95+
96+
afterAll(() => wallet.shutdown());
97+
98+
describe('Sign Transaction', () => {
99+
let props: InitializeTxProps;
100+
let txInternals: InitializeTxResult;
101+
const simpleOutput = {
102+
address: Cardano.PaymentAddress(
103+
'addr_test1qpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qum8x5w'
104+
),
105+
value: { coins: 11_111_111n }
106+
};
107+
108+
it('should sign simple multi-sig transaction', async () => {
109+
props = {
110+
outputs: new Set<Cardano.TxOut>([simpleOutput])
111+
};
112+
txInternals = await wallet.initializeTx(props);
113+
const witnessedTx = await wallet.finalizeTx({ tx: txInternals });
114+
expect(witnessedTx.witness.signatures.size).toBe(1);
115+
});
116+
});
117+
});

0 commit comments

Comments
 (0)
Please sign in to comment.