Skip to content

Commit c572657

Browse files
committed
feat: sign multi-sig transaction with ledger
1 parent 81782c0 commit c572657

File tree

3 files changed

+132
-2
lines changed

3 files changed

+132
-2
lines changed

packages/hardware-ledger/src/LedgerKeyAgent.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ const getDerivationPath = (
322322
};
323323
};
324324

325+
const multiSigWitnessPaths: BIP32Path[] = [
326+
util.accountKeyDerivationPathToBip32Path(0, { index: 0, role: KeyRole.External }, KeyPurpose.MULTI_SIG)
327+
];
328+
325329
export class LedgerKeyAgent extends KeyAgentBase {
326330
readonly deviceConnection?: LedgerConnection;
327331
readonly #communicationType: CommunicationType;
@@ -733,7 +737,10 @@ export class LedgerKeyAgent extends KeyAgentBase {
733737
tagCborSets: txBody.hasTaggedSets()
734738
},
735739
signingMode,
736-
tx: ledgerTxData
740+
tx: ledgerTxData,
741+
...(signingMode === TransactionSigningMode.MULTISIG_TRANSACTION && {
742+
additionalWitnessPaths: multiSigWitnessPaths
743+
})
737744
});
738745

739746
if (!areStringsEqualInConstantTime(result.txHashHex, hash)) {

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ 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+
818
const toInlineDatum: Transform<Cardano.PlutusData, Ledger.Datum> = (datum) => ({
919
datumHex: Serialization.PlutusData.fromCore(datum).toCbor(),
1020
type: Ledger.DatumType.INLINE
@@ -20,8 +30,9 @@ const toDestination: Transform<Cardano.TxOut, Ledger.TxOutputDestination, Ledger
2030
context
2131
) => {
2232
const knownAddress = context?.knownAddresses.find((address) => address.address === txOut.address);
33+
const isScriptWallet = isScriptAddress(txOut.address);
2334

24-
if (knownAddress) {
35+
if (knownAddress && !isScriptWallet) {
2536
const paymentKeyPath = util.paymentKeyPathFromGroupedAddress(knownAddress);
2637
const stakeKeyPath = util.stakeKeyPathFromGroupedAddress(knownAddress);
2738

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger';
7+
import { dummyLogger as logger } from 'ts-log';
8+
import { mockProviders as mocks } from '@cardano-sdk/util-dev';
9+
10+
describe('LedgerSharedWalletKeyAgent', () => {
11+
let ledgerKeyAgent: LedgerKeyAgent;
12+
let wallet: BaseWallet;
13+
14+
beforeAll(async () => {
15+
ledgerKeyAgent = await LedgerKeyAgent.createWithDevice(
16+
{
17+
chainId: Cardano.ChainIds.Preprod,
18+
communicationType: CommunicationType.Node,
19+
purpose: KeyPurpose.MULTI_SIG
20+
},
21+
{ bip32Ed25519: await Crypto.SodiumBip32Ed25519.create(), logger }
22+
);
23+
});
24+
25+
afterAll(async () => {
26+
await ledgerKeyAgent.deviceConnection?.transport.close();
27+
});
28+
29+
describe('signTransaction', () => {
30+
let txInternals: InitializeTxResult;
31+
32+
beforeAll(async () => {
33+
const walletPubKey = await ledgerKeyAgent.derivePublicKey({ index: 0, role: KeyRole.External });
34+
const walletKeyHash = ledgerKeyAgent.bip32Ed25519.getPubKeyHash(walletPubKey);
35+
36+
const walletStakePubKey = await ledgerKeyAgent.derivePublicKey({ index: 0, role: KeyRole.Stake });
37+
const walletStakeKeyHash = ledgerKeyAgent.bip32Ed25519.getPubKeyHash(walletStakePubKey);
38+
39+
const paymentScript: Cardano.NativeScript = {
40+
__type: Cardano.ScriptType.Native,
41+
kind: Cardano.NativeScriptKind.RequireAnyOf,
42+
scripts: [
43+
{
44+
__type: Cardano.ScriptType.Native,
45+
keyHash: walletKeyHash,
46+
kind: Cardano.NativeScriptKind.RequireSignature
47+
},
48+
{
49+
__type: Cardano.ScriptType.Native,
50+
keyHash: Crypto.Ed25519KeyHashHex('b275b08c999097247f7c17e77007c7010cd19f20cc086ad99d398539'),
51+
kind: Cardano.NativeScriptKind.RequireSignature
52+
}
53+
]
54+
};
55+
56+
const stakingScript: Cardano.NativeScript = {
57+
__type: Cardano.ScriptType.Native,
58+
kind: Cardano.NativeScriptKind.RequireAnyOf,
59+
scripts: [
60+
{
61+
__type: Cardano.ScriptType.Native,
62+
keyHash: walletStakeKeyHash,
63+
kind: Cardano.NativeScriptKind.RequireSignature
64+
},
65+
{
66+
__type: Cardano.ScriptType.Native,
67+
keyHash: Crypto.Ed25519KeyHashHex('b275b08c999097247f7c17e77007c7010cd19f20cc086ad99d398539'),
68+
kind: Cardano.NativeScriptKind.RequireSignature
69+
}
70+
]
71+
};
72+
73+
const outputs: Cardano.TxOut[] = [
74+
{
75+
address: Cardano.PaymentAddress(
76+
'addr_test1qpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qum8x5w'
77+
),
78+
scriptReference: paymentScript,
79+
value: { coins: 11_111_111n }
80+
}
81+
];
82+
const props: InitializeTxProps = {
83+
outputs: new Set<Cardano.TxOut>(outputs)
84+
};
85+
86+
wallet = createSharedWallet(
87+
{ name: 'Shared HW Wallet' },
88+
{
89+
assetProvider: mocks.mockAssetProvider(),
90+
chainHistoryProvider: mocks.mockChainHistoryProvider(),
91+
logger,
92+
networkInfoProvider: mocks.mockNetworkInfoProvider(),
93+
paymentScript,
94+
rewardAccountInfoProvider: mocks.mockRewardAccountInfoProvider(),
95+
rewardsProvider: mocks.mockRewardsProvider(),
96+
stakingScript,
97+
txSubmitProvider: mocks.mockTxSubmitProvider(),
98+
utxoProvider: mocks.mockUtxoProvider(),
99+
witnesser: util.createBip32Ed25519Witnesser(util.createAsyncKeyAgent(ledgerKeyAgent))
100+
}
101+
);
102+
txInternals = await wallet.initializeTx(props);
103+
});
104+
105+
afterAll(() => wallet.shutdown());
106+
107+
it('successfully signs a transaction', async () => {
108+
const tx = await wallet.finalizeTx({ tx: txInternals });
109+
expect(tx.witness.signatures.size).toBe(1);
110+
});
111+
});
112+
});

0 commit comments

Comments
 (0)