Skip to content

Commit

Permalink
feat: sign multi-sig transaction with ledger
Browse files Browse the repository at this point in the history
  • Loading branch information
greatertomi committed Feb 26, 2025
1 parent a78683f commit 06289fb
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 2 deletions.
9 changes: 8 additions & 1 deletion packages/hardware-ledger/src/LedgerKeyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ const getDerivationPath = (
};
};

const multiSigWitnessPaths: BIP32Path[] = [
util.accountKeyDerivationPathToBip32Path(0, { index: 0, role: KeyRole.External }, KeyPurpose.MULTI_SIG)
];

export class LedgerKeyAgent extends KeyAgentBase {
readonly deviceConnection?: LedgerConnection;
readonly #communicationType: CommunicationType;
Expand Down Expand Up @@ -733,7 +737,10 @@ export class LedgerKeyAgent extends KeyAgentBase {
tagCborSets: txBody.hasTaggedSets()
},
signingMode,
tx: ledgerTxData
tx: ledgerTxData,
...(signingMode === TransactionSigningMode.MULTISIG_TRANSACTION && {
additionalWitnessPaths: multiSigWitnessPaths
})
});

if (!areStringsEqualInConstantTime(result.txHashHex, hash)) {
Expand Down
13 changes: 12 additions & 1 deletion packages/hardware-ledger/src/transformers/txOut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import { LedgerTxTransformerContext } from '../types';
import { mapTokenMap } from './assets';
import { util } from '@cardano-sdk/key-management';

const isScriptAddress = (address: string): boolean => {
const baseAddress = Cardano.Address.fromBech32(address).asBase();
const paymentCredential = baseAddress?.getPaymentCredential();
const stakeCredential = baseAddress?.getStakeCredential();
return (
paymentCredential?.type === Cardano.CredentialType.ScriptHash &&
stakeCredential?.type === Cardano.CredentialType.ScriptHash
);
};

const toInlineDatum: Transform<Cardano.PlutusData, Ledger.Datum> = (datum) => ({
datumHex: Serialization.PlutusData.fromCore(datum).toCbor(),
type: Ledger.DatumType.INLINE
Expand All @@ -20,8 +30,9 @@ const toDestination: Transform<Cardano.TxOut, Ledger.TxOutputDestination, Ledger
context
) => {
const knownAddress = context?.knownAddresses.find((address) => address.address === txOut.address);
const isScriptWallet = isScriptAddress(txOut.address);

if (knownAddress) {
if (knownAddress && !isScriptWallet) {
const paymentKeyPath = util.paymentKeyPathFromGroupedAddress(knownAddress);
const stakeKeyPath = util.stakeKeyPathFromGroupedAddress(knownAddress);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as Crypto from '@cardano-sdk/crypto';
import { BaseWallet, createSharedWallet } from '../../../src';
import { Cardano } from '@cardano-sdk/core';
import { CommunicationType, KeyPurpose, KeyRole, util } from '@cardano-sdk/key-management';
import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction';
import { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger';
import { dummyLogger as logger } from 'ts-log';
import { mockProviders as mocks } from '@cardano-sdk/util-dev';

describe('LedgerSharedWalletKeyAgent', () => {
let ledgerKeyAgent: LedgerKeyAgent;
let wallet: BaseWallet;

beforeAll(async () => {
ledgerKeyAgent = await LedgerKeyAgent.createWithDevice(
{
chainId: Cardano.ChainIds.Preprod,
communicationType: CommunicationType.Node,
purpose: KeyPurpose.MULTI_SIG
},
{ bip32Ed25519: await Crypto.SodiumBip32Ed25519.create(), logger }
);
});

afterAll(async () => {
await ledgerKeyAgent.deviceConnection?.transport.close();
});

describe('signTransaction', () => {
let txInternals: InitializeTxResult;

beforeAll(async () => {
const walletPubKey = await ledgerKeyAgent.derivePublicKey({ index: 0, role: KeyRole.External });
const walletKeyHash = ledgerKeyAgent.bip32Ed25519.getPubKeyHash(walletPubKey);

const walletStakePubKey = await ledgerKeyAgent.derivePublicKey({ index: 0, role: KeyRole.Stake });
const walletStakeKeyHash = ledgerKeyAgent.bip32Ed25519.getPubKeyHash(walletStakePubKey);

const paymentScript: Cardano.NativeScript = {
__type: Cardano.ScriptType.Native,
kind: Cardano.NativeScriptKind.RequireAnyOf,
scripts: [
{
__type: Cardano.ScriptType.Native,
keyHash: walletKeyHash,
kind: Cardano.NativeScriptKind.RequireSignature
},
{
__type: Cardano.ScriptType.Native,
keyHash: Crypto.Ed25519KeyHashHex('b275b08c999097247f7c17e77007c7010cd19f20cc086ad99d398539'),
kind: Cardano.NativeScriptKind.RequireSignature
}
]
};

const stakingScript: Cardano.NativeScript = {
__type: Cardano.ScriptType.Native,
kind: Cardano.NativeScriptKind.RequireAnyOf,
scripts: [
{
__type: Cardano.ScriptType.Native,
keyHash: walletStakeKeyHash,
kind: Cardano.NativeScriptKind.RequireSignature
},
{
__type: Cardano.ScriptType.Native,
keyHash: Crypto.Ed25519KeyHashHex('b275b08c999097247f7c17e77007c7010cd19f20cc086ad99d398539'),
kind: Cardano.NativeScriptKind.RequireSignature
}
]
};

const outputs: Cardano.TxOut[] = [
{
address: Cardano.PaymentAddress(
'addr_test1qpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qum8x5w'
),
scriptReference: paymentScript,
value: { coins: 11_111_111n }
}
];
const props: InitializeTxProps = {
outputs: new Set<Cardano.TxOut>(outputs)
};

wallet = createSharedWallet(
{ name: 'Shared HW Wallet' },
{
assetProvider: mocks.mockAssetProvider(),
chainHistoryProvider: mocks.mockChainHistoryProvider(),
logger,
networkInfoProvider: mocks.mockNetworkInfoProvider(),
paymentScript,
rewardAccountInfoProvider: mocks.mockRewardAccountInfoProvider(),
rewardsProvider: mocks.mockRewardsProvider(),
stakingScript,
txSubmitProvider: mocks.mockTxSubmitProvider(),
utxoProvider: mocks.mockUtxoProvider(),
witnesser: util.createBip32Ed25519Witnesser(util.createAsyncKeyAgent(ledgerKeyAgent))
}
);
txInternals = await wallet.initializeTx(props);
});

afterAll(() => wallet.shutdown());

it('successfully signs a transaction', async () => {
const tx = await wallet.finalizeTx({ tx: txInternals });
expect(tx.witness.signatures.size).toBe(1);
});
});
});

0 comments on commit 06289fb

Please sign in to comment.