From 5fbe60625367504a9a58c2620759fe9e7b0de1ec Mon Sep 17 00:00:00 2001 From: Angel Castillo Date: Wed, 16 Apr 2025 21:34:18 +0800 Subject: [PATCH 1/3] feat(core): add isBabbageOutput method to TransactionOutput class --- .../Serialization/TransactionBody/TransactionOutput.ts | 10 ++++++++++ .../TransactionBody/TransactionOutput.test.ts | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/core/src/Serialization/TransactionBody/TransactionOutput.ts b/packages/core/src/Serialization/TransactionBody/TransactionOutput.ts index 355198de125..5896a6f02d2 100644 --- a/packages/core/src/Serialization/TransactionBody/TransactionOutput.ts +++ b/packages/core/src/Serialization/TransactionBody/TransactionOutput.ts @@ -304,6 +304,16 @@ export class TransactionOutput { this.#scriptRef = script; } + /** + * Checks if the output is formatted as legacy array or babbage map. + * + * @returns true if the output is babbage format, false otherwise. + */ + isBabbageOutput(): boolean { + const reader = new CborReader(this.toCbor()); + return reader.peekState() === CborReaderState.StartMap; + } + /** * Gets the size of the serialized map. * diff --git a/packages/core/test/Serialization/TransactionBody/TransactionOutput.test.ts b/packages/core/test/Serialization/TransactionBody/TransactionOutput.test.ts index f8920f3c563..cc2190c60b6 100644 --- a/packages/core/test/Serialization/TransactionBody/TransactionOutput.test.ts +++ b/packages/core/test/Serialization/TransactionBody/TransactionOutput.test.ts @@ -396,5 +396,14 @@ describe('TransactionOutput', () => { expect(output.toCore()).toEqual(basicOutput); }); }); + + describe('isBabbageOutput', () => { + it('cant distinguish a babbage output from a legacy output', () => { + const babbageOut = TransactionOutput.fromCbor(babbageAllFieldsCbor); + const legacyOutput = TransactionOutput.fromCbor(legacyOutputNoDatumCbor); + expect(babbageOut.isBabbageOutput()).toBe(true); + expect(legacyOutput.isBabbageOutput()).toBe(false); + }); + }); }); }); From 1723cb052db6cc0c603403bdc605f9b6fcc566e8 Mon Sep 17 00:00:00 2001 From: Angel Castillo Date: Wed, 16 Apr 2025 21:35:01 +0800 Subject: [PATCH 2/3] fix(hardware-trezor): transaction outputs format are now computed independently --- .../hardware-trezor/src/TrezorKeyAgent.ts | 16 ++- .../hardware-trezor/src/transformers/tx.ts | 2 +- .../hardware-trezor/src/transformers/txOut.ts | 40 +++---- packages/hardware-trezor/src/types.ts | 6 +- packages/hardware-trezor/test/testData.ts | 26 ++++- .../test/transformers/tx.test.ts | 29 ++++- .../test/transformers/txOut.test.ts | 109 +++++++++++------- 7 files changed, 150 insertions(+), 78 deletions(-) diff --git a/packages/hardware-trezor/src/TrezorKeyAgent.ts b/packages/hardware-trezor/src/TrezorKeyAgent.ts index 9f2ad332edf..07a0edcb061 100644 --- a/packages/hardware-trezor/src/TrezorKeyAgent.ts +++ b/packages/hardware-trezor/src/TrezorKeyAgent.ts @@ -246,13 +246,25 @@ export class TrezorKeyAgent extends KeyAgentBase { const body = txBody.toCore(); const hash = txBody.hash() as unknown as HexBlob; + const outputsFormat = txBody + .outputs() + .map((out) => + out.isBabbageOutput() + ? Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE + : Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY + ); + const collateralReturnFormat = txBody.collateralReturn()?.isBabbageOutput() + ? Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE + : Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY; + const trezorTxData = await txToTrezor(body, { accountIndex: this.accountIndex, chainId: this.chainId, + collateralReturnFormat, knownAddresses, + outputsFormat, tagCborSets: txBody.hasTaggedSets(), - txInKeyPathMap, - useBabbageOutputs: txBody.hasBabbageOutput() + txInKeyPathMap }); const signingMode = TrezorKeyAgent.matchSigningMode(trezorTxData); diff --git a/packages/hardware-trezor/src/transformers/tx.ts b/packages/hardware-trezor/src/transformers/tx.ts index 3a78e3b9db3..97555ee8f5b 100644 --- a/packages/hardware-trezor/src/transformers/tx.ts +++ b/packages/hardware-trezor/src/transformers/tx.ts @@ -19,7 +19,7 @@ export const trezorTxTransformer: Transformer< certificates: ({ certificates }, context) => (certificates ? mapCerts(certificates, context!) : undefined), collateralInputs: ({ collaterals }, context) => (collaterals ? mapTxIns(collaterals, context!) : undefined), collateralReturn: ({ collateralReturn }, context) => - collateralReturn ? toTxOut(collateralReturn, context!) : undefined, + collateralReturn ? toTxOut({ index: 0, isCollateral: true, txOut: collateralReturn }, context!) : undefined, fee: ({ fee }) => fee.toString(), inputs: ({ inputs }, context) => mapTxIns(inputs, context!), mint: ({ mint }) => mapTokenMap(mint, true), diff --git a/packages/hardware-trezor/src/transformers/txOut.ts b/packages/hardware-trezor/src/transformers/txOut.ts index 1772d2d6aa2..891c33978f8 100644 --- a/packages/hardware-trezor/src/transformers/txOut.ts +++ b/packages/hardware-trezor/src/transformers/txOut.ts @@ -42,31 +42,27 @@ const getScriptHex = (output: Serialization.TransactionOutput): HexBlob | undefi const getInlineDatum = (datum: Cardano.PlutusData): string => Serialization.PlutusData.fromCore(datum).toCbor(); -export const toTxOut: Transform = (txOut, context) => { - const destination = toDestination(txOut, context); +export const toTxOut: Transform< + { txOut: Cardano.TxOut; index: number; isCollateral: boolean }, + Trezor.CardanoOutput, + TrezorTxTransformerContext +> = (elem, context) => { + const { txOut, index, isCollateral } = elem; const output = Serialization.TransactionOutput.fromCore(txOut); const scriptHex = getScriptHex(output); + const format = isCollateral ? context?.collateralReturnFormat : context?.outputsFormat[index]; + const isBabbage = format === Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE; - return context?.useBabbageOutputs - ? { - ...destination, - amount: txOut.value.coins.toString(), - datumHash: txOut.datumHash?.toString(), - format: Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE, - inlineDatum: txOut.datum ? getInlineDatum(txOut.datum) : undefined, - referenceScript: scriptHex, - tokenBundle: mapTokenMap(txOut.value.assets) - } - : { - ...destination, - amount: txOut.value.coins.toString(), - datumHash: txOut.datumHash?.toString(), - format: Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, - inlineDatum: undefined, - referenceScript: undefined, - tokenBundle: mapTokenMap(txOut.value.assets) - }; + return { + ...toDestination(txOut, context), + amount: txOut.value.coins.toString(), + datumHash: txOut.datumHash?.toString(), + format, + inlineDatum: isBabbage ? (txOut.datum ? getInlineDatum(txOut.datum) : undefined) : undefined, + referenceScript: isBabbage ? scriptHex : undefined, + tokenBundle: mapTokenMap(txOut.value.assets) + }; }; export const mapTxOuts = (txOuts: Cardano.TxOut[], context: TrezorTxTransformerContext): Trezor.CardanoOutput[] => - txOuts.map((txOut) => toTxOut(txOut, context)); + txOuts.map((txOut, index) => toTxOut({ index, isCollateral: false, txOut }, context)); diff --git a/packages/hardware-trezor/src/types.ts b/packages/hardware-trezor/src/types.ts index c82ba49e6b5..8d4825d1e40 100644 --- a/packages/hardware-trezor/src/types.ts +++ b/packages/hardware-trezor/src/types.ts @@ -13,8 +13,10 @@ export type TrezorTxTransformerContext = { accountIndex: number; /** Whether sets should be encoded as tagged set in CBOR */ tagCborSets: boolean; - /** Whether to use Babbage output format or not. */ - useBabbageOutputs: boolean; + /** The outputs format in the same order as they appear in the transaction. */ + outputsFormat: Array; + /** The collateral return output format. */ + collateralReturnFormat: Trezor.PROTO.CardanoTxOutputSerializationFormat | undefined; } & SignTransactionContext; export type TrezorTxOutputDestination = diff --git a/packages/hardware-trezor/test/testData.ts b/packages/hardware-trezor/test/testData.ts index 98006b0a91b..33fa73f50fa 100644 --- a/packages/hardware-trezor/test/testData.ts +++ b/packages/hardware-trezor/test/testData.ts @@ -1,4 +1,5 @@ import * as Crypto from '@cardano-sdk/crypto'; +import * as Trezor from '@trezor/connect'; import { AddressType, GroupedAddress, KeyRole } from '@cardano-sdk/key-management'; import { Cardano } from '@cardano-sdk/core'; import { HexBlob } from '@cardano-sdk/util'; @@ -169,10 +170,15 @@ export const contextWithKnownAddresses: TrezorTxTransformerContext = { networkId: Cardano.NetworkId.Testnet, networkMagic: 999 }, + collateralReturnFormat: Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE, knownAddresses: [knownAddress], + outputsFormat: [ + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE, + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY + ], tagCborSets: false, - txInKeyPathMap: {}, - useBabbageOutputs: false + txInKeyPathMap: {} }; export const contextWithKnownAddressesWithoutStakingCredentials: TrezorTxTransformerContext = { @@ -181,10 +187,14 @@ export const contextWithKnownAddressesWithoutStakingCredentials: TrezorTxTransfo networkId: Cardano.NetworkId.Testnet, networkMagic: 999 }, + collateralReturnFormat: Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE, knownAddresses: [knownAddressWithoutStakingPath], + outputsFormat: [ + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE + ], tagCborSets: false, - txInKeyPathMap: {}, - useBabbageOutputs: false + txInKeyPathMap: {} }; export const contextWithoutKnownAddresses: TrezorTxTransformerContext = { @@ -193,10 +203,14 @@ export const contextWithoutKnownAddresses: TrezorTxTransformerContext = { networkId: Cardano.NetworkId.Testnet, networkMagic: 999 }, + collateralReturnFormat: Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE, knownAddresses: [], + outputsFormat: [ + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE + ], tagCborSets: false, - txInKeyPathMap: {}, - useBabbageOutputs: false + txInKeyPathMap: {} }; export const coreWithdrawalWithKeyHashCredential = { diff --git a/packages/hardware-trezor/test/transformers/tx.test.ts b/packages/hardware-trezor/test/transformers/tx.test.ts index 19d05c16357..8a77de863f7 100644 --- a/packages/hardware-trezor/test/transformers/tx.test.ts +++ b/packages/hardware-trezor/test/transformers/tx.test.ts @@ -4,6 +4,7 @@ import { babbageTxBodyWithScripts, contextWithKnownAddresses, contextWithoutKnownAddresses, + knownAddress, knownAddressKeyPath, knownAddressPaymentKeyPath, knownAddressStakeKeyPath, @@ -45,6 +46,11 @@ describe('tx', () => { expect( await txToTrezor(txBody, { ...contextWithKnownAddresses, + outputsFormat: [ + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY + ], txInKeyPathMap: { [TxInId(txBody.inputs[0])]: knownAddressPaymentKeyPath } }) ).toEqual({ @@ -202,10 +208,13 @@ describe('tx', () => { expect( await txToTrezor(babbageTxBodyWithScripts, { ...contextWithKnownAddresses, + outputsFormat: [ + Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE, + Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE + ], txInKeyPathMap: { [TxInId(babbageTxBodyWithScripts.inputs[0])]: knownAddressPaymentKeyPath - }, - useBabbageOutputs: true + } }) ).toEqual({ additionalWitnessRequests: [ @@ -286,7 +295,12 @@ describe('tx', () => { }); test('can map transaction with collaterals', async () => { - expect(await txToTrezor(txBodyWithCollaterals, contextWithoutKnownAddresses)).toEqual({ + expect( + await txToTrezor(txBodyWithCollaterals, { + ...contextWithoutKnownAddresses, + collateralReturnFormat: Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY + }) + ).toEqual({ additionalWitnessRequests: [], collateralInputs: [ { @@ -325,11 +339,16 @@ describe('tx', () => { expect( await txToTrezor(plutusTxWithBabbage, { ...contextWithKnownAddresses, + collateralReturnFormat: Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE, + knownAddresses: [knownAddress], + outputsFormat: [ + Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE, + Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE + ], txInKeyPathMap: { [TxInId(plutusTxWithBabbage.inputs[0])]: knownAddressPaymentKeyPath, [TxInId(plutusTxWithBabbage.collaterals[0])]: knownAddressPaymentKeyPath - }, - useBabbageOutputs: true + } }) ).toEqual({ additionalWitnessRequests: [ diff --git a/packages/hardware-trezor/test/transformers/txOut.test.ts b/packages/hardware-trezor/test/transformers/txOut.test.ts index aa7cb35b7c1..793acd870e1 100644 --- a/packages/hardware-trezor/test/transformers/txOut.test.ts +++ b/packages/hardware-trezor/test/transformers/txOut.test.ts @@ -19,7 +19,14 @@ import { mapTxOuts, toTxOut } from '../../src/transformers/txOut'; describe('txOut', () => { describe('mapTxOuts', () => { it('can map a set of transaction outputs to third party address', async () => { - const txOuts = mapTxOuts([txOut, txOut, txOut], contextWithKnownAddresses); + const txOuts = mapTxOuts([txOut, txOut, txOut], { + ...contextWithKnownAddresses, + outputsFormat: [ + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY + ] + }); expect(txOuts.length).toEqual(3); for (const out of txOuts) { @@ -33,7 +40,14 @@ describe('txOut', () => { }); it('can map a set of transaction outputs with assets to third party address', async () => { - const txOuts = mapTxOuts([txOutWithAssets, txOutWithAssets, txOutWithAssets], contextWithKnownAddresses); + const txOuts = mapTxOuts([txOutWithAssets, txOutWithAssets, txOutWithAssets], { + ...contextWithKnownAddresses, + outputsFormat: [ + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY + ] + }); expect(txOuts.length).toEqual(3); @@ -81,10 +95,14 @@ describe('txOut', () => { }); it('can map a set of transaction outputs to owned address', async () => { - const txOuts = mapTxOuts( - [txOutToOwnedAddress, txOutToOwnedAddress, txOutToOwnedAddress], - contextWithKnownAddresses - ); + const txOuts = mapTxOuts([txOutToOwnedAddress, txOutToOwnedAddress, txOutToOwnedAddress], { + ...contextWithKnownAddresses, + outputsFormat: [ + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY + ] + }); expect(txOuts.length).toEqual(3); @@ -104,7 +122,14 @@ describe('txOut', () => { it('can map a set of transaction outputs with assets to owned address', async () => { const txOuts = mapTxOuts( [txOutWithAssetsToOwnedAddress, txOutWithAssetsToOwnedAddress, txOutWithAssetsToOwnedAddress], - contextWithKnownAddresses + { + ...contextWithKnownAddresses, + outputsFormat: [ + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY, + Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY + ] + } ); expect(txOuts.length).toEqual(3); @@ -156,16 +181,14 @@ describe('txOut', () => { }); it('can map a set of transaction outputs with both output formats', async () => { - const legacyTxOuts = mapTxOuts([txOutWithDatumHashAndOwnedAddress], contextWithKnownAddresses); - - const babbageTxOuts = mapTxOuts([txOutWithReferenceScriptAndDatumHash], { - ...contextWithKnownAddresses, - useBabbageOutputs: true - }); + const txOuts = mapTxOuts( + [txOutWithDatumHashAndOwnedAddress, txOutWithReferenceScriptAndDatumHash], + contextWithKnownAddresses + ); - expect(legacyTxOuts.length).toEqual(1); + expect(txOuts.length).toEqual(2); - expect(legacyTxOuts).toEqual([ + expect(txOuts).toEqual([ { addressParameters: { addressType: Trezor.PROTO.CardanoAddressType.BASE, @@ -175,12 +198,7 @@ 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, @@ -198,7 +216,7 @@ describe('txOut', () => { describe('toTxOut', () => { it('can map a simple transaction output to third party address', async () => { - const out = toTxOut(txOut, contextWithKnownAddresses); + const out = toTxOut({ index: 0, isCollateral: false, txOut }, contextWithKnownAddresses); expect(out).toEqual({ address: 'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp', @@ -208,7 +226,7 @@ describe('txOut', () => { }); it('can map a simple transaction output with assets to third party address', async () => { - const out = toTxOut(txOutWithAssets, contextWithKnownAddresses); + const out = toTxOut({ index: 0, isCollateral: false, txOut: txOutWithAssets }, contextWithKnownAddresses); expect(out).toEqual({ address: 'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp', @@ -251,7 +269,7 @@ describe('txOut', () => { }); it('can map a simple transaction output to owned address', async () => { - const out = toTxOut(txOutToOwnedAddress, contextWithKnownAddresses); + const out = toTxOut({ index: 0, isCollateral: false, txOut: txOutToOwnedAddress }, contextWithKnownAddresses); expect(out).toEqual({ addressParameters: { @@ -265,7 +283,10 @@ describe('txOut', () => { }); it('can map a simple transaction output with assets to owned address', async () => { - const out = toTxOut(txOutWithAssetsToOwnedAddress, contextWithKnownAddresses); + const out = toTxOut( + { index: 0, isCollateral: false, txOut: txOutWithAssetsToOwnedAddress }, + contextWithKnownAddresses + ); expect(out).toEqual({ addressParameters: { @@ -312,7 +333,7 @@ describe('txOut', () => { }); it('can map simple transaction output with datum hash', async () => { - const out = toTxOut(txOutWithDatumHash, contextWithKnownAddresses); + const out = toTxOut({ index: 0, isCollateral: false, txOut: txOutWithDatumHash }, contextWithKnownAddresses); expect(out).toEqual({ address: @@ -324,7 +345,10 @@ describe('txOut', () => { }); it('can map simple transaction output with datum hash to owned address', async () => { - const out = toTxOut(txOutWithDatumHashAndOwnedAddress, contextWithKnownAddresses); + const out = toTxOut( + { index: 0, isCollateral: false, txOut: txOutWithDatumHashAndOwnedAddress }, + contextWithKnownAddresses + ); expect(out).toEqual({ addressParameters: { @@ -339,7 +363,10 @@ describe('txOut', () => { }); it('can map simple transaction with inline datum', async () => { - const out = toTxOut(txOutWithInlineDatum, { ...contextWithKnownAddresses, useBabbageOutputs: true }); + const out = toTxOut( + { index: 0, isCollateral: false, txOut: txOutWithInlineDatum }, + { ...contextWithKnownAddresses, outputsFormat: [Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE] } + ); expect(out).toEqual({ address: @@ -351,10 +378,10 @@ describe('txOut', () => { }); it('can map simple transaction with inline datum to owned address', async () => { - const out = toTxOut(txOutWithInlineDatumAndOwnedAddress, { - ...contextWithKnownAddresses, - useBabbageOutputs: true - }); + const out = toTxOut( + { index: 0, isCollateral: false, txOut: txOutWithInlineDatumAndOwnedAddress }, + { ...contextWithKnownAddresses, outputsFormat: [Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE] } + ); expect(out).toEqual({ addressParameters: { @@ -369,10 +396,11 @@ describe('txOut', () => { }); it('can map a simple transaction output with reference script and datum hash', async () => { - const out = toTxOut(txOutWithReferenceScriptAndDatumHash, { - ...contextWithKnownAddresses, - useBabbageOutputs: true - }); + const out = toTxOut( + { index: 0, isCollateral: false, txOut: txOutWithReferenceScriptAndDatumHash }, + { ...contextWithKnownAddresses, outputsFormat: [Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE] } + ); + expect(out).toEqual({ addressParameters: { addressType: Trezor.PROTO.CardanoAddressType.BASE, @@ -387,10 +415,11 @@ describe('txOut', () => { }); it('can map a simple transaction output with reference script and inline datum', async () => { - const out = toTxOut(txOutWithReferenceScriptAndInlineDatum, { - ...contextWithKnownAddresses, - useBabbageOutputs: true - }); + const out = toTxOut( + { index: 0, isCollateral: false, txOut: txOutWithReferenceScriptAndInlineDatum }, + { ...contextWithKnownAddresses, outputsFormat: [Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE] } + ); + expect(out).toEqual({ addressParameters: { addressType: Trezor.PROTO.CardanoAddressType.BASE, From ed91155d6bada948d1dccf1e37d1ca6e784d6683 Mon Sep 17 00:00:00 2001 From: Angel Castillo Date: Wed, 16 Apr 2025 21:35:19 +0800 Subject: [PATCH 3/3] fix(hardware-ledger): transaction outputs format are now computed independently --- .../hardware-ledger/src/LedgerKeyAgent.ts | 13 +++++- .../src/transformers/collateralOutput.ts | 2 +- .../hardware-ledger/src/transformers/txOut.ts | 42 +++++++++++-------- packages/hardware-ledger/src/types.ts | 7 +++- packages/hardware-ledger/test/testData.ts | 11 +++-- .../test/transformers/certificates.test.ts | 7 ++-- .../test/transformers/tx.test.ts | 11 ++--- .../test/transformers/txOut.test.ts | 23 +++++++--- 8 files changed, 73 insertions(+), 43 deletions(-) diff --git a/packages/hardware-ledger/src/LedgerKeyAgent.ts b/packages/hardware-ledger/src/LedgerKeyAgent.ts index 1afb466d6a1..057df404063 100644 --- a/packages/hardware-ledger/src/LedgerKeyAgent.ts +++ b/packages/hardware-ledger/src/LedgerKeyAgent.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as Crypto from '@cardano-sdk/crypto'; +import * as Ledger from '@cardano-foundation/ledgerjs-hw-app-cardano'; import { AlgorithmId, CBORValue, @@ -720,13 +721,21 @@ export class LedgerKeyAgent extends KeyAgentBase { const dRepPublicKey = await this.derivePublicKey(util.DREP_KEY_DERIVATION_PATH); const dRepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepPublicKey).hash()).hex(); + const outputsFormat = txBody + .outputs() + .map((out) => (out.isBabbageOutput() ? Ledger.TxOutputFormat.MAP_BABBAGE : Ledger.TxOutputFormat.ARRAY_LEGACY)); + const collateralReturnFormat = txBody.collateralReturn()?.isBabbageOutput() + ? Ledger.TxOutputFormat.MAP_BABBAGE + : Ledger.TxOutputFormat.ARRAY_LEGACY; + const ledgerTxData = await toLedgerTx(body, { accountIndex: this.accountIndex, chainId: this.chainId, + collateralReturnFormat, dRepKeyHashHex, knownAddresses, - txInKeyPathMap, - useBabbageOutputs: txBody.hasBabbageOutput() + outputsFormat, + txInKeyPathMap }); const deviceConnection = await LedgerKeyAgent.checkDeviceConnection( diff --git a/packages/hardware-ledger/src/transformers/collateralOutput.ts b/packages/hardware-ledger/src/transformers/collateralOutput.ts index 55404236b2f..3917df739b4 100644 --- a/packages/hardware-ledger/src/transformers/collateralOutput.ts +++ b/packages/hardware-ledger/src/transformers/collateralOutput.ts @@ -3,4 +3,4 @@ import { LedgerTxTransformerContext } from '../types'; import { toTxOut } from './txOut'; export const mapCollateralTxOut = (collateralTxOut: Cardano.TxOut | undefined, context: LedgerTxTransformerContext) => - collateralTxOut ? toTxOut(collateralTxOut, context) : null; + collateralTxOut ? toTxOut({ index: 0, isCollateral: true, txOut: collateralTxOut }, context) : null; diff --git a/packages/hardware-ledger/src/transformers/txOut.ts b/packages/hardware-ledger/src/transformers/txOut.ts index e8e6509600d..27d9aa6414a 100644 --- a/packages/hardware-ledger/src/transformers/txOut.ts +++ b/packages/hardware-ledger/src/transformers/txOut.ts @@ -56,27 +56,33 @@ const getScriptHex = (output: Serialization.TransactionOutput): HexBlob | null = return scriptRef.toCbor(); }; -export const toTxOut: Transform = (txOut, context) => { +export const toTxOut: Transform< + { txOut: Cardano.TxOut; index: number; isCollateral: boolean }, + Ledger.TxOutput, + LedgerTxTransformerContext +> = (elem, context) => { + const { txOut, index, isCollateral } = elem; const output = Serialization.TransactionOutput.fromCore(txOut); const scriptHex = getScriptHex(output); + const format = isCollateral ? context?.collateralReturnFormat : context?.outputsFormat[index]; + const isBabbageFormat = format === Ledger.TxOutputFormat.MAP_BABBAGE; - return context?.useBabbageOutputs - ? { - amount: txOut.value.coins, - datum: txOut.datumHash ? toDatumHash(txOut.datumHash) : txOut.datum ? toInlineDatum(txOut.datum) : null, - destination: toDestination(txOut, context), - format: Ledger.TxOutputFormat.MAP_BABBAGE, - referenceScriptHex: scriptHex, - tokenBundle: mapTokenMap(txOut.value.assets) - } - : { - amount: txOut.value.coins, - datumHashHex: txOut.datumHash ? txOut.datumHash : null, - destination: toDestination(txOut, context), - format: Ledger.TxOutputFormat.ARRAY_LEGACY, - tokenBundle: mapTokenMap(txOut.value.assets) - }; + return { + amount: txOut.value.coins, + destination: toDestination(txOut, context), + tokenBundle: mapTokenMap(txOut.value.assets), + ...(isBabbageFormat + ? { + datum: txOut.datumHash ? toDatumHash(txOut.datumHash) : txOut.datum ? toInlineDatum(txOut.datum) : null, + format: Ledger.TxOutputFormat.MAP_BABBAGE, + referenceScriptHex: scriptHex + } + : { + datumHashHex: txOut.datumHash ?? null, + format: Ledger.TxOutputFormat.ARRAY_LEGACY + }) + }; }; export const mapTxOuts = (txOuts: Cardano.TxOut[], context: LedgerTxTransformerContext): Ledger.TxOutput[] => - txOuts.map((txOut) => toTxOut(txOut, context)); + txOuts.map((txOut, index) => toTxOut({ index, isCollateral: false, txOut }, context)); diff --git a/packages/hardware-ledger/src/types.ts b/packages/hardware-ledger/src/types.ts index bd784fd5e89..3bd37bec4f9 100644 --- a/packages/hardware-ledger/src/types.ts +++ b/packages/hardware-ledger/src/types.ts @@ -1,6 +1,7 @@ import { Cardano } from '@cardano-sdk/core'; import { HID } from 'node-hid'; import { SignTransactionContext } from '@cardano-sdk/key-management'; +import { TxOutputFormat } from '@cardano-foundation/ledgerjs-hw-app-cardano'; import TransportNodeHid from '@ledgerhq/hw-transport-node-hid-noevents'; import TransportWebUSB from '@ledgerhq/hw-transport-webusb'; @@ -21,6 +22,8 @@ export type LedgerTxTransformerContext = { chainId: Cardano.ChainId; /** Non-hardened account in cip1852 */ accountIndex: number; - /** Whether to use Babbage output format or not. */ - useBabbageOutputs: boolean; + /** The outputs format in the same order as they appear in the transaction. */ + outputsFormat: Array; + /** The collateral return output format. */ + collateralReturnFormat: TxOutputFormat | undefined; } & SignTransactionContext; diff --git a/packages/hardware-ledger/test/testData.ts b/packages/hardware-ledger/test/testData.ts index 394e6d9540c..f9f084e48ce 100644 --- a/packages/hardware-ledger/test/testData.ts +++ b/packages/hardware-ledger/test/testData.ts @@ -1,4 +1,5 @@ import * as Crypto from '@cardano-sdk/crypto'; +import * as Ledger from '@cardano-foundation/ledgerjs-hw-app-cardano'; import { AddressType, KeyRole } from '@cardano-sdk/key-management'; import { Base64Blob, HexBlob } from '@cardano-sdk/util'; import { Cardano, Serialization } from '@cardano-sdk/core'; @@ -342,6 +343,7 @@ export const CONTEXT_WITH_KNOWN_ADDRESSES: LedgerTxTransformerContext = { networkId: Cardano.NetworkId.Testnet, networkMagic: 999 }, + collateralReturnFormat: Ledger.TxOutputFormat.MAP_BABBAGE, dRepKeyHashHex: Crypto.Ed25519KeyHashHex(dRepCredential.hash), knownAddresses: [ { @@ -357,8 +359,8 @@ export const CONTEXT_WITH_KNOWN_ADDRESSES: LedgerTxTransformerContext = { type: AddressType.Internal } ], - txInKeyPathMap: {}, - useBabbageOutputs: true + outputsFormat: [Ledger.TxOutputFormat.ARRAY_LEGACY, Ledger.TxOutputFormat.MAP_BABBAGE], + txInKeyPathMap: {} }; export const CONTEXT_WITHOUT_KNOWN_ADDRESSES: LedgerTxTransformerContext = { @@ -367,9 +369,10 @@ export const CONTEXT_WITHOUT_KNOWN_ADDRESSES: LedgerTxTransformerContext = { networkId: Cardano.NetworkId.Testnet, networkMagic: 999 }, + collateralReturnFormat: Ledger.TxOutputFormat.ARRAY_LEGACY, knownAddresses: [], - txInKeyPathMap: {}, - useBabbageOutputs: true + outputsFormat: [Ledger.TxOutputFormat.ARRAY_LEGACY, Ledger.TxOutputFormat.MAP_BABBAGE], + txInKeyPathMap: {} }; export const votes = [ diff --git a/packages/hardware-ledger/test/transformers/certificates.test.ts b/packages/hardware-ledger/test/transformers/certificates.test.ts index de53e8cfb4e..d3450d6b5ad 100644 --- a/packages/hardware-ledger/test/transformers/certificates.test.ts +++ b/packages/hardware-ledger/test/transformers/certificates.test.ts @@ -73,20 +73,21 @@ export const createTxInKeyPathMapMock = (knownAddresses: GroupedAddress[]): TxIn const mockContext: LedgerTxTransformerContext = { accountIndex: 0, chainId: createChainId(1, 764_824_073), + collateralReturnFormat: Ledger.TxOutputFormat.ARRAY_LEGACY, + dRepKeyHashHex: undefined, handleResolutions: [], - knownAddresses: [ createGroupedAddress(address1, ownRewardAccount, AddressType.External, 0, stakeKeyPath), createGroupedAddress(address2, ownRewardAccount, AddressType.External, 1, stakeKeyPath) ], + outputsFormat: [Ledger.TxOutputFormat.ARRAY_LEGACY, Ledger.TxOutputFormat.MAP_BABBAGE], sender: undefined, 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 c74f3e7e99c..1d3eb25182d 100644 --- a/packages/hardware-ledger/test/transformers/tx.test.ts +++ b/packages/hardware-ledger/test/transformers/tx.test.ts @@ -15,8 +15,7 @@ describe('tx', () => { txInKeyPathMap: { [TxInId(tx.body.inputs[0])]: paymentKeyPath, [TxInId(tx.body.collaterals![0])]: paymentKeyPath - }, - useBabbageOutputs: false + } }) ).toEqual({ auxiliaryData: { @@ -181,6 +180,7 @@ describe('tx', () => { expect( await toLedgerTx(babbageTxWithoutScript.body, { ...CONTEXT_WITH_KNOWN_ADDRESSES, + outputsFormat: [Ledger.TxOutputFormat.MAP_BABBAGE], txInKeyPathMap: { [TxInId(babbageTxWithoutScript.body.inputs[0])]: paymentKeyPath } @@ -266,12 +266,7 @@ describe('tx', () => { ] }; - expect( - await toLedgerTx(txBodyWithRegistrationCert, { - ...CONTEXT_WITH_KNOWN_ADDRESSES, - useBabbageOutputs: false - }) - ).toEqual({ + expect(await toLedgerTx(txBodyWithRegistrationCert, CONTEXT_WITH_KNOWN_ADDRESSES)).toEqual({ auxiliaryData: { params: { hashHex: '2ceb364d93225b4a0f004a0975a13eb50c3cc6348474b4fe9121f8dc72ca0cfa' diff --git a/packages/hardware-ledger/test/transformers/txOut.test.ts b/packages/hardware-ledger/test/transformers/txOut.test.ts index 97c09dbe46c..cfe9d3d7137 100644 --- a/packages/hardware-ledger/test/transformers/txOut.test.ts +++ b/packages/hardware-ledger/test/transformers/txOut.test.ts @@ -18,7 +18,14 @@ describe('txOut', () => { txOutWithReferenceScriptWithInlineDatum, txOutWithReferenceScriptWithInlineDatum ], - CONTEXT_WITH_KNOWN_ADDRESSES + { + ...CONTEXT_WITH_KNOWN_ADDRESSES, + outputsFormat: [ + Ledger.TxOutputFormat.MAP_BABBAGE, + Ledger.TxOutputFormat.MAP_BABBAGE, + Ledger.TxOutputFormat.MAP_BABBAGE + ] + } ); expect(txOuts.length).toEqual(3); @@ -64,7 +71,7 @@ describe('txOut', () => { describe('toTxOut', () => { it('can map a simple txOut to third party address', async () => { - const out = toTxOut(txOut, { ...CONTEXT_WITH_KNOWN_ADDRESSES, useBabbageOutputs: false }); + const out = toTxOut({ index: 0, isCollateral: false, txOut }, CONTEXT_WITH_KNOWN_ADDRESSES); expect(out).toEqual({ amount: 10n, @@ -114,7 +121,7 @@ describe('txOut', () => { }); it('can map a simple txOut to owned address', async () => { - const out = toTxOut(txOutToOwnedAddress, { ...CONTEXT_WITH_KNOWN_ADDRESSES, useBabbageOutputs: false }); + const out = toTxOut({ index: 0, isCollateral: false, txOut: txOutToOwnedAddress }, CONTEXT_WITH_KNOWN_ADDRESSES); expect(out).toEqual({ amount: 10n, @@ -179,7 +186,10 @@ describe('txOut', () => { }); it('can map a txOut with a reference script - datum hash', async () => { - const out = toTxOut(txOutWithReferenceScript, CONTEXT_WITH_KNOWN_ADDRESSES); + const out = toTxOut( + { index: 1, isCollateral: false, txOut: txOutWithReferenceScript }, + CONTEXT_WITH_KNOWN_ADDRESSES + ); expect(out).toEqual({ amount: 10n, @@ -216,7 +226,10 @@ describe('txOut', () => { }); it('can map a txOut with a reference script - inline datum', async () => { - const out = toTxOut(txOutWithReferenceScriptWithInlineDatum, CONTEXT_WITH_KNOWN_ADDRESSES); + const out = toTxOut( + { index: 1, isCollateral: false, txOut: txOutWithReferenceScriptWithInlineDatum }, + CONTEXT_WITH_KNOWN_ADDRESSES + ); expect(out).toEqual({ amount: 10n,