From ab26ecf766b56801157ee80c28e75bb95e02c0ad Mon Sep 17 00:00:00 2001 From: Angel Castillo Date: Fri, 11 Apr 2025 19:34:52 +0800 Subject: [PATCH 1/3] feat(core): add hasBabbageOutput method to TransactionBody --- .../TransactionBody/TransactionBody.ts | 13 ++++++++++++ .../TransactionBody/TransactionBody.test.ts | 20 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/core/src/Serialization/TransactionBody/TransactionBody.ts b/packages/core/src/Serialization/TransactionBody/TransactionBody.ts index e04c0c80f05..3a7b0ec2c9b 100644 --- a/packages/core/src/Serialization/TransactionBody/TransactionBody.ts +++ b/packages/core/src/Serialization/TransactionBody/TransactionBody.ts @@ -947,6 +947,19 @@ export class TransactionBody { return reader.peekState() === CborReaderState.Tag && reader.peekTag() === CborTag.Set; } + /** + * Checks if the transaction body has Babbage outputs. + * + * @returns true if the transaction body has Babbage outputs, false otherwise. + */ + hasBabbageOutput(): boolean { + if (this.#outputs.length === 0) return false; + + const reader = new CborReader(this.#outputs[0].toCbor()); + + return reader.peekState() === CborReaderState.StartMap; + } + /** * Gets the size of the serialized map. * diff --git a/packages/core/test/Serialization/TransactionBody/TransactionBody.test.ts b/packages/core/test/Serialization/TransactionBody/TransactionBody.test.ts index 23a9b0afff4..1ccb6fedb02 100644 --- a/packages/core/test/Serialization/TransactionBody/TransactionBody.test.ts +++ b/packages/core/test/Serialization/TransactionBody/TransactionBody.test.ts @@ -2,7 +2,7 @@ import * as Cardano from '../../../src/Cardano'; import * as Crypto from '@cardano-sdk/crypto'; import { HexBlob } from '@cardano-sdk/util'; -import { TransactionBody, TxBodyCBOR, TxCBOR } from '../../../src/Serialization'; +import { Transaction, TransactionBody, TxBodyCBOR, TxCBOR } from '../../../src/Serialization'; import { babbageTx } from '../testData'; import { mintTokenMap, params, txIn, txOut } from './testData'; @@ -273,6 +273,24 @@ describe('TransactionBody', () => { expect(body.toCore()).toEqual(expectedConwayCore); }); + it('can encode identify transactions output format - Babbage outputs', () => { + const tx = Transaction.fromCbor( + TxCBOR( + '84a500818258207aa1264bcd0c06f34a49ed1dd7307a2bdec5a97bdeb546498759ad5b8ed42fd5010182a200583930195bde3deacb613b7e9eb6280b14db4e353e475e96d19f3f7a5e2d66195bde3deacb613b7e9eb6280b14db4e353e475e96d19f3f7a5e2d66011a00e4e1c0a2005839003d3246dc0c50ab3c74a8ffdd8068313ff99d341c461a8fe31f416d0a8fba06d60d71edc077cc5ebcb8ff82137afbce68df98271909332348011b000000025106a838021a00029309031a04a07bc6081a04a07a40a0f5f6' + ) + ); + expect(tx.body().hasBabbageOutput()).toBeTruthy(); + }); + + it('can encode identify transactions output format - Legacy outputs', () => { + const tx = Transaction.fromCbor( + TxCBOR( + '84a500818258207aa1264bcd0c06f34a49ed1dd7307a2bdec5a97bdeb546498759ad5b8ed42fd501018282583930195bde3deacb613b7e9eb6280b14db4e353e475e96d19f3f7a5e2d66195bde3deacb613b7e9eb6280b14db4e353e475e96d19f3f7a5e2d661a00e4e1c0825839003d3246dc0c50ab3c74a8ffdd8068313ff99d341c461a8fe31f416d0a8fba06d60d71edc077cc5ebcb8ff82137afbce68df982719093323481b000000025106a838021a00029309031a04a07bc6081a04a07a40a0f5f6' + ) + ); + expect(tx.body().hasBabbageOutput()).toBeFalsy(); + }); + it('sorts withdrawals canonically', () => { const body = TransactionBody.fromCbor(cbor); const withdrawals = body.withdrawals(); From 9256fdde3d19abbc3a4c668883d6502a6fe6018e Mon Sep 17 00:00:00 2001 From: Angel Castillo Date: Fri, 11 Apr 2025 16:17:18 +0800 Subject: [PATCH 2/3] fix(ledger): fix output format mapping on ledger HW devices --- .../hardware-ledger/src/LedgerKeyAgent.ts | 5 +++- .../hardware-ledger/src/transformers/txOut.ts | 30 +------------------ packages/hardware-ledger/src/types.ts | 2 ++ packages/hardware-ledger/test/testData.ts | 6 ++-- .../test/transformers/certificates.test.ts | 3 +- .../test/transformers/tx.test.ts | 6 ++-- .../test/transformers/txOut.test.ts | 4 +-- .../hardware/ledger/LedgerKeyAgent.test.ts | 9 ++++++ 8 files changed, 28 insertions(+), 37 deletions(-) diff --git a/packages/hardware-ledger/src/LedgerKeyAgent.ts b/packages/hardware-ledger/src/LedgerKeyAgent.ts index 12c792f9efc..1afb466d6a1 100644 --- a/packages/hardware-ledger/src/LedgerKeyAgent.ts +++ b/packages/hardware-ledger/src/LedgerKeyAgent.ts @@ -715,15 +715,18 @@ export class LedgerKeyAgent extends KeyAgentBase { ): Promise { try { const body = txBody.toCore(); + const hash = txBody.hash() as unknown as HexBlob; const dRepPublicKey = await this.derivePublicKey(util.DREP_KEY_DERIVATION_PATH); const dRepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepPublicKey).hash()).hex(); + const ledgerTxData = await toLedgerTx(body, { accountIndex: this.accountIndex, chainId: this.chainId, dRepKeyHashHex, knownAddresses, - txInKeyPathMap + txInKeyPathMap, + useBabbageOutputs: txBody.hasBabbageOutput() }); const deviceConnection = await LedgerKeyAgent.checkDeviceConnection( diff --git a/packages/hardware-ledger/src/transformers/txOut.ts b/packages/hardware-ledger/src/transformers/txOut.ts index d59120bb27e..e8e6509600d 100644 --- a/packages/hardware-ledger/src/transformers/txOut.ts +++ b/packages/hardware-ledger/src/transformers/txOut.ts @@ -56,39 +56,11 @@ const getScriptHex = (output: Serialization.TransactionOutput): HexBlob | null = return scriptRef.toCbor(); }; -/** - * There are currently two types of outputs supported by the ledger: - * - * legacy_transaction_output = - * [ address - * , amount : value - * , ? datum_hash : $hash32 - * ] - * - * and - * - * post_alonzo_transaction_output = - * { 0 : address - * , 1 : value - * , ? 2 : datum_option ; New; datum option - * , ? 3 : script_ref ; New; script reference - * } - * - * Legacy outputs are definite length arrays of three elements, however the new babbage outputs are definite length maps - * of four elements. - * - * @param out The output to be verified. - */ -const isBabbage = (out: Serialization.TransactionOutput): boolean => { - const reader = new Serialization.CborReader(out.toCbor()); - return reader.peekState() === Serialization.CborReaderState.StartMap; -}; - export const toTxOut: Transform = (txOut, context) => { const output = Serialization.TransactionOutput.fromCore(txOut); const scriptHex = getScriptHex(output); - return isBabbage(output) + return context?.useBabbageOutputs ? { amount: txOut.value.coins, datum: txOut.datumHash ? toDatumHash(txOut.datumHash) : txOut.datum ? toInlineDatum(txOut.datum) : null, diff --git a/packages/hardware-ledger/src/types.ts b/packages/hardware-ledger/src/types.ts index e175c30fe15..bd784fd5e89 100644 --- a/packages/hardware-ledger/src/types.ts +++ b/packages/hardware-ledger/src/types.ts @@ -21,4 +21,6 @@ export type LedgerTxTransformerContext = { chainId: Cardano.ChainId; /** Non-hardened account in cip1852 */ accountIndex: number; + /** Whether to use Babbage output format or not. */ + useBabbageOutputs: boolean; } & SignTransactionContext; diff --git a/packages/hardware-ledger/test/testData.ts b/packages/hardware-ledger/test/testData.ts index fb177721967..394e6d9540c 100644 --- a/packages/hardware-ledger/test/testData.ts +++ b/packages/hardware-ledger/test/testData.ts @@ -357,7 +357,8 @@ export const CONTEXT_WITH_KNOWN_ADDRESSES: LedgerTxTransformerContext = { type: AddressType.Internal } ], - txInKeyPathMap: {} + txInKeyPathMap: {}, + useBabbageOutputs: true }; export const CONTEXT_WITHOUT_KNOWN_ADDRESSES: LedgerTxTransformerContext = { @@ -367,7 +368,8 @@ export const CONTEXT_WITHOUT_KNOWN_ADDRESSES: LedgerTxTransformerContext = { networkMagic: 999 }, knownAddresses: [], - txInKeyPathMap: {} + txInKeyPathMap: {}, + useBabbageOutputs: true }; export const votes = [ diff --git a/packages/hardware-ledger/test/transformers/certificates.test.ts b/packages/hardware-ledger/test/transformers/certificates.test.ts index b2dd99a2373..de53e8cfb4e 100644 --- a/packages/hardware-ledger/test/transformers/certificates.test.ts +++ b/packages/hardware-ledger/test/transformers/certificates.test.ts @@ -85,7 +85,8 @@ const mockContext: LedgerTxTransformerContext = { txInKeyPathMap: createTxInKeyPathMapMock([ createGroupedAddress(address1, ownRewardAccount, AddressType.External, 0, stakeKeyPath), createGroupedAddress(address2, ownRewardAccount, AddressType.External, 1, stakeKeyPath) - ]) + ]), + useBabbageOutputs: true }; const EXAMPLE_URL = 'https://example.com'; diff --git a/packages/hardware-ledger/test/transformers/tx.test.ts b/packages/hardware-ledger/test/transformers/tx.test.ts index 9cdca7e31ad..c74f3e7e99c 100644 --- a/packages/hardware-ledger/test/transformers/tx.test.ts +++ b/packages/hardware-ledger/test/transformers/tx.test.ts @@ -15,7 +15,8 @@ describe('tx', () => { txInKeyPathMap: { [TxInId(tx.body.inputs[0])]: paymentKeyPath, [TxInId(tx.body.collaterals![0])]: paymentKeyPath - } + }, + useBabbageOutputs: false }) ).toEqual({ auxiliaryData: { @@ -267,7 +268,8 @@ describe('tx', () => { expect( await toLedgerTx(txBodyWithRegistrationCert, { - ...CONTEXT_WITH_KNOWN_ADDRESSES + ...CONTEXT_WITH_KNOWN_ADDRESSES, + useBabbageOutputs: false }) ).toEqual({ auxiliaryData: { diff --git a/packages/hardware-ledger/test/transformers/txOut.test.ts b/packages/hardware-ledger/test/transformers/txOut.test.ts index 78cd1bc9fa9..97c09dbe46c 100644 --- a/packages/hardware-ledger/test/transformers/txOut.test.ts +++ b/packages/hardware-ledger/test/transformers/txOut.test.ts @@ -64,7 +64,7 @@ describe('txOut', () => { describe('toTxOut', () => { it('can map a simple txOut to third party address', async () => { - const out = toTxOut(txOut, CONTEXT_WITH_KNOWN_ADDRESSES); + const out = toTxOut(txOut, { ...CONTEXT_WITH_KNOWN_ADDRESSES, useBabbageOutputs: false }); expect(out).toEqual({ amount: 10n, @@ -114,7 +114,7 @@ describe('txOut', () => { }); it('can map a simple txOut to owned address', async () => { - const out = toTxOut(txOutToOwnedAddress, CONTEXT_WITH_KNOWN_ADDRESSES); + const out = toTxOut(txOutToOwnedAddress, { ...CONTEXT_WITH_KNOWN_ADDRESSES, useBabbageOutputs: false }); expect(out).toEqual({ amount: 10n, diff --git a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts index f812819c081..8d7d46002f0 100644 --- a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts +++ b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts @@ -230,6 +230,15 @@ describe('LedgerKeyAgent', () => { expect(signatures.size).toBe(2); }); + it('successfully signs ADA handle mint transaction', async () => { + const cbor = + '84a500818258207aa1264bcd0c06f34a49ed1dd7307a2bdec5a97bdeb546498759ad5b8ed42fd5010182a200583930195bde3deacb613b7e9eb6280b14db4e353e475e96d19f3f7a5e2d66195bde3deacb613b7e9eb6280b14db4e353e475e96d19f3f7a5e2d66011a00e4e1c0a2005839003d3246dc0c50ab3c74a8ffdd8068313ff99d341c461a8fe31f416d0a8fba06d60d71edc077cc5ebcb8ff82137afbce68df98271909332348011b000000025106a838021a00029309031a04a07bc6081a04a07a40a0f5f6'; + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: cbor as any }); + expect(signatures.size).toBe(1); + }); + it('throws if signed transaction hash doesnt match hash computed by the wallet', async () => { const originalHashFn = Serialization.TransactionBody.prototype.hash; jest From 3de9a1d8287bf5253f15674033931ce540d4c6c5 Mon Sep 17 00:00:00 2001 From: Angel Castillo Date: Fri, 11 Apr 2025 16:48:25 +0800 Subject: [PATCH 3/3] fix(trezor): fix output format mapping on trezor HW devices --- .../hardware-trezor/src/TrezorKeyAgent.ts | 3 +- .../hardware-trezor/src/transformers/txOut.ts | 30 +-------------- packages/hardware-trezor/src/types.ts | 2 + packages/hardware-trezor/test/testData.ts | 9 +++-- .../test/transformers/tx.test.ts | 8 ++-- .../test/transformers/txOut.test.ts | 38 +++++++++++++------ 6 files changed, 43 insertions(+), 47 deletions(-) diff --git a/packages/hardware-trezor/src/TrezorKeyAgent.ts b/packages/hardware-trezor/src/TrezorKeyAgent.ts index f48e7c80c70..9f2ad332edf 100644 --- a/packages/hardware-trezor/src/TrezorKeyAgent.ts +++ b/packages/hardware-trezor/src/TrezorKeyAgent.ts @@ -251,7 +251,8 @@ export class TrezorKeyAgent extends KeyAgentBase { chainId: this.chainId, knownAddresses, tagCborSets: txBody.hasTaggedSets(), - txInKeyPathMap + txInKeyPathMap, + useBabbageOutputs: txBody.hasBabbageOutput() }); const signingMode = TrezorKeyAgent.matchSigningMode(trezorTxData); diff --git a/packages/hardware-trezor/src/transformers/txOut.ts b/packages/hardware-trezor/src/transformers/txOut.ts index 65359b3b50d..1772d2d6aa2 100644 --- a/packages/hardware-trezor/src/transformers/txOut.ts +++ b/packages/hardware-trezor/src/transformers/txOut.ts @@ -40,34 +40,6 @@ const getScriptHex = (output: Serialization.TransactionOutput): HexBlob | undefi return scriptRef.toCbor(); }; -/** - * There are currently two types of outputs supported by the ledger: - * - * legacy_transaction_output = - * [ address - * , amount : value - * , ? datum_hash : $hash32 - * ] - * - * and - * - * post_alonzo_transaction_output = - * { 0 : address - * , 1 : value - * , ? 2 : datum_option ; New; datum option - * , ? 3 : script_ref ; New; script reference - * } - * - * Legacy outputs are definite length arrays of three elements, however the new babbage outputs are definite length maps - * of four elements. - * - * @param out The output to be verified. - */ -const isBabbage = (out: Serialization.TransactionOutput): boolean => { - const reader = new Serialization.CborReader(out.toCbor()); - return reader.peekState() === Serialization.CborReaderState.StartMap; -}; - const getInlineDatum = (datum: Cardano.PlutusData): string => Serialization.PlutusData.fromCore(datum).toCbor(); export const toTxOut: Transform = (txOut, context) => { @@ -75,7 +47,7 @@ export const toTxOut: Transform { ...contextWithKnownAddresses, txInKeyPathMap: { [TxInId(babbageTxBodyWithScripts.inputs[0])]: knownAddressPaymentKeyPath - } + }, + useBabbageOutputs: true }) ).toEqual({ additionalWitnessRequests: [ @@ -327,7 +328,8 @@ describe('tx', () => { txInKeyPathMap: { [TxInId(plutusTxWithBabbage.inputs[0])]: knownAddressPaymentKeyPath, [TxInId(plutusTxWithBabbage.collaterals[0])]: knownAddressPaymentKeyPath - } + }, + useBabbageOutputs: true }) ).toEqual({ additionalWitnessRequests: [ @@ -361,7 +363,7 @@ describe('tx', () => { address: 'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp', amount: '10', - format: Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY + format: Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE }, fee: '10', inputs: [ diff --git a/packages/hardware-trezor/test/transformers/txOut.test.ts b/packages/hardware-trezor/test/transformers/txOut.test.ts index 40e709a8884..aa7cb35b7c1 100644 --- a/packages/hardware-trezor/test/transformers/txOut.test.ts +++ b/packages/hardware-trezor/test/transformers/txOut.test.ts @@ -156,14 +156,16 @@ describe('txOut', () => { }); it('can map a set of transaction outputs with both output formats', async () => { - const txOuts = mapTxOuts( - [txOutWithDatumHashAndOwnedAddress, txOutWithReferenceScriptAndDatumHash], - contextWithKnownAddresses - ); + const legacyTxOuts = mapTxOuts([txOutWithDatumHashAndOwnedAddress], contextWithKnownAddresses); - expect(txOuts.length).toEqual(2); + const babbageTxOuts = mapTxOuts([txOutWithReferenceScriptAndDatumHash], { + ...contextWithKnownAddresses, + useBabbageOutputs: true + }); - expect(txOuts).toEqual([ + expect(legacyTxOuts.length).toEqual(1); + + expect(legacyTxOuts).toEqual([ { addressParameters: { addressType: Trezor.PROTO.CardanoAddressType.BASE, @@ -173,7 +175,12 @@ describe('txOut', () => { amount: '10', datumHash: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5', format: Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY - }, + } + ]); + + expect(babbageTxOuts.length).toEqual(1); + + expect(babbageTxOuts).toEqual([ { addressParameters: { addressType: Trezor.PROTO.CardanoAddressType.BASE, @@ -332,7 +339,7 @@ describe('txOut', () => { }); it('can map simple transaction with inline datum', async () => { - const out = toTxOut(txOutWithInlineDatum, contextWithKnownAddresses); + const out = toTxOut(txOutWithInlineDatum, { ...contextWithKnownAddresses, useBabbageOutputs: true }); expect(out).toEqual({ address: @@ -344,7 +351,10 @@ describe('txOut', () => { }); it('can map simple transaction with inline datum to owned address', async () => { - const out = toTxOut(txOutWithInlineDatumAndOwnedAddress, contextWithKnownAddresses); + const out = toTxOut(txOutWithInlineDatumAndOwnedAddress, { + ...contextWithKnownAddresses, + useBabbageOutputs: true + }); expect(out).toEqual({ addressParameters: { @@ -359,7 +369,10 @@ describe('txOut', () => { }); it('can map a simple transaction output with reference script and datum hash', async () => { - const out = toTxOut(txOutWithReferenceScriptAndDatumHash, contextWithKnownAddresses); + const out = toTxOut(txOutWithReferenceScriptAndDatumHash, { + ...contextWithKnownAddresses, + useBabbageOutputs: true + }); expect(out).toEqual({ addressParameters: { addressType: Trezor.PROTO.CardanoAddressType.BASE, @@ -374,7 +387,10 @@ describe('txOut', () => { }); it('can map a simple transaction output with reference script and inline datum', async () => { - const out = toTxOut(txOutWithReferenceScriptAndInlineDatum, contextWithKnownAddresses); + const out = toTxOut(txOutWithReferenceScriptAndInlineDatum, { + ...contextWithKnownAddresses, + useBabbageOutputs: true + }); expect(out).toEqual({ addressParameters: { addressType: Trezor.PROTO.CardanoAddressType.BASE,