Skip to content

Commit fd4b610

Browse files
committed
feat: sign multi-sig transaction with trezor
1 parent 06289fb commit fd4b610

File tree

6 files changed

+159
-17
lines changed

6 files changed

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

0 commit comments

Comments
 (0)