Skip to content

Commit

Permalink
feat: sign multi-sig transaction with trezor
Browse files Browse the repository at this point in the history
  • Loading branch information
greatertomi committed Feb 26, 2025
1 parent 06289fb commit e3d3503
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 17 deletions.
1 change: 1 addition & 0 deletions packages/core/src/Cardano/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './resolveInputValue';
export * from './phase2Validation';
export * from './addressesShareAnyKey';
export * from './plutusDataUtils';
export * from './isScriptAddress';
9 changes: 9 additions & 0 deletions packages/core/src/Cardano/util/isScriptAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Address, CredentialType } from '../Address';

// TODO: Would like to verify if this is needed before writing test for it
export const isScriptAddress = (address: string): boolean => {
const baseAddress = Address.fromBech32(address).asBase();
const paymentCredential = baseAddress?.getPaymentCredential();
const stakeCredential = baseAddress?.getStakeCredential();
return paymentCredential?.type === CredentialType.ScriptHash && stakeCredential?.type === CredentialType.ScriptHash;
};
12 changes: 1 addition & 11 deletions packages/hardware-ledger/src/transformers/txOut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,6 @@ 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 @@ -30,7 +20,7 @@ const toDestination: Transform<Cardano.TxOut, Ledger.TxOutputDestination, Ledger
context
) => {
const knownAddress = context?.knownAddresses.find((address) => address.address === txOut.address);
const isScriptWallet = isScriptAddress(txOut.address);
const isScriptWallet = Cardano.util.isScriptAddress(txOut.address);

if (knownAddress && !isScriptWallet) {
const paymentKeyPath = util.paymentKeyPathFromGroupedAddress(knownAddress);
Expand Down
37 changes: 32 additions & 5 deletions packages/hardware-trezor/src/TrezorKeyAgent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as Crypto from '@cardano-sdk/crypto';
import * as Trezor from '@trezor/connect';
import { BIP32Path } from '@cardano-sdk/crypto';
import { Cardano, NotImplementedError, Serialization } from '@cardano-sdk/core';
import {
CardanoKeyConst,
Expand All @@ -9,6 +10,7 @@ import {
KeyAgentDependencies,
KeyAgentType,
KeyPurpose,
KeyRole,
SerializableTrezorKeyAgentData,
SignBlobResult,
SignTransactionContext,
Expand Down Expand Up @@ -68,6 +70,10 @@ const containsOnlyScriptHashCredentials = (tx: Omit<Trezor.CardanoSignTransactio
return !tx.withdrawals?.some((withdrawal) => !withdrawal.scriptHash);
};

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

const isMultiSig = (tx: Omit<Trezor.CardanoSignTransaction, 'signingMode'>): boolean => {
const allThirdPartyInputs = !tx.inputs.some((input) => input.path);
// Trezor doesn't allow change outputs to address controlled by your keys and instead you have to use script address for change out
Expand Down Expand Up @@ -116,6 +122,21 @@ export class TrezorKeyAgent extends KeyAgentBase {
// Show Trezor Suite popup. Disabled for node based apps
popup: communicationType !== CommunicationType.Node && !silentMode
});

trezorConnect.on(Trezor.UI_EVENT, (event) => {
// React on ui-request_passphrase event
if (event.type === Trezor.UI.REQUEST_PASSPHRASE && event.payload.device) {
trezorConnect.uiResponse({
payload: {
passphraseOnDevice: true,
save: true,
value: ''
},
type: Trezor.UI.RECEIVE_PASSPHRASE
});
}
});

return true;
} catch (error: any) {
if (error.code === 'Init_AlreadyInitialized') return true;
Expand Down Expand Up @@ -235,14 +256,20 @@ export class TrezorKeyAgent extends KeyAgentBase {
const trezorConnect = getTrezorConnect(this.#communicationType);
const result = await trezorConnect.cardanoSignTransaction({
...trezorTxData,
...(signingMode === Trezor.PROTO.CardanoTxSigningMode.MULTISIG_TRANSACTION && {
additionalWitnessRequests: multiSigWitnessPaths
}),
signingMode
});

const expectedPublicKeys = await Promise.all(
util
.ownSignatureKeyPaths(body, knownAddresses, txInKeyPathMap)
.map((derivationPath) => this.derivePublicKey(derivationPath))
);
const expectedPublicKeys =
signingMode === Trezor.PROTO.CardanoTxSigningMode.MULTISIG_TRANSACTION
? [await this.derivePublicKey({ index: 0, role: KeyRole.External })]
: await Promise.all(
util
.ownSignatureKeyPaths(body, knownAddresses, txInKeyPathMap)
.map((derivationPath) => this.derivePublicKey(derivationPath))
);

if (!result.success) {
throw new errors.TransportError('Failed to export extended account public key', result.payload);
Expand Down
3 changes: 2 additions & 1 deletion packages/hardware-trezor/src/transformers/txOut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ const toDestination: Transform<Cardano.TxOut, TrezorTxOutputDestination, TrezorT
context
) => {
const knownAddress = context?.knownAddresses.find((address: GroupedAddress) => address.address === txOut.address);
const isScriptWallet = Cardano.util.isScriptAddress(txOut.address);

if (!knownAddress) {
if (!knownAddress || isScriptWallet) {
return {
address: txOut.address
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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 { TrezorKeyAgent } from '@cardano-sdk/hardware-trezor';
import { dummyLogger as logger } from 'ts-log';
import { mockProviders as mocks } from '@cardano-sdk/util-dev';

describe('TrezorSharedWalletKeyAgent', () => {
let wallet: BaseWallet;
let trezorKeyAgent: TrezorKeyAgent;
let txSubmitProvider: mocks.TxSubmitProviderStub;

const trezorConfig = {
communicationType: CommunicationType.Node,
manifest: {
appUrl: 'https://your.application.com',
email: 'email@developer.com'
}
};

beforeAll(async () => {
txSubmitProvider = mocks.mockTxSubmitProvider();
trezorKeyAgent = await TrezorKeyAgent.createWithDevice(
{
chainId: Cardano.ChainIds.Preprod,
purpose: KeyPurpose.MULTI_SIG,
trezorConfig
},
{
bip32Ed25519: await Crypto.SodiumBip32Ed25519.create(),
logger
}
);

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

const walletStakePubKey = await trezorKeyAgent.derivePublicKey({ index: 0, role: KeyRole.Stake });
const walletStakeKeyHash = trezorKeyAgent.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
}
]
};

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(),
txSubmitProvider,
utxoProvider: mocks.mockUtxoProvider(),
witnesser: util.createBip32Ed25519Witnesser(util.createAsyncKeyAgent(trezorKeyAgent))
}
);
});

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

describe('Sign Transaction', () => {
let props: InitializeTxProps;
let txInternals: InitializeTxResult;
const simpleOutput = {
address: Cardano.PaymentAddress(
'addr_test1qpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qum8x5w'
),
value: { coins: 11_111_111n }
};

it('should sign simple multi-sig transaction', async () => {
props = {
outputs: new Set<Cardano.TxOut>([simpleOutput])
};
txInternals = await wallet.initializeTx(props);
const witnessedTx = await wallet.finalizeTx({ tx: txInternals });
expect(witnessedTx.witness.signatures.size).toBe(1);
});
});
});

0 comments on commit e3d3503

Please sign in to comment.