Skip to content

Commit ac42650

Browse files
Merge pull request #1618 from input-output-hk/fix/output-mapping-on-hw
Fix/output mapping on hw
2 parents ea65f63 + ed91155 commit ac42650

File tree

17 files changed

+242
-121
lines changed

17 files changed

+242
-121
lines changed

packages/core/src/Serialization/TransactionBody/TransactionOutput.ts

+10
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,16 @@ export class TransactionOutput {
304304
this.#scriptRef = script;
305305
}
306306

307+
/**
308+
* Checks if the output is formatted as legacy array or babbage map.
309+
*
310+
* @returns true if the output is babbage format, false otherwise.
311+
*/
312+
isBabbageOutput(): boolean {
313+
const reader = new CborReader(this.toCbor());
314+
return reader.peekState() === CborReaderState.StartMap;
315+
}
316+
307317
/**
308318
* Gets the size of the serialized map.
309319
*

packages/core/test/Serialization/TransactionBody/TransactionOutput.test.ts

+9
Original file line numberDiff line numberDiff line change
@@ -396,5 +396,14 @@ describe('TransactionOutput', () => {
396396
expect(output.toCore()).toEqual(basicOutput);
397397
});
398398
});
399+
400+
describe('isBabbageOutput', () => {
401+
it('cant distinguish a babbage output from a legacy output', () => {
402+
const babbageOut = TransactionOutput.fromCbor(babbageAllFieldsCbor);
403+
const legacyOutput = TransactionOutput.fromCbor(legacyOutputNoDatumCbor);
404+
expect(babbageOut.isBabbageOutput()).toBe(true);
405+
expect(legacyOutput.isBabbageOutput()).toBe(false);
406+
});
407+
});
399408
});
400409
});

packages/hardware-ledger/src/LedgerKeyAgent.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import * as Crypto from '@cardano-sdk/crypto';
3+
import * as Ledger from '@cardano-foundation/ledgerjs-hw-app-cardano';
34
import {
45
AlgorithmId,
56
CBORValue,
@@ -720,13 +721,21 @@ export class LedgerKeyAgent extends KeyAgentBase {
720721
const dRepPublicKey = await this.derivePublicKey(util.DREP_KEY_DERIVATION_PATH);
721722
const dRepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepPublicKey).hash()).hex();
722723

724+
const outputsFormat = txBody
725+
.outputs()
726+
.map((out) => (out.isBabbageOutput() ? Ledger.TxOutputFormat.MAP_BABBAGE : Ledger.TxOutputFormat.ARRAY_LEGACY));
727+
const collateralReturnFormat = txBody.collateralReturn()?.isBabbageOutput()
728+
? Ledger.TxOutputFormat.MAP_BABBAGE
729+
: Ledger.TxOutputFormat.ARRAY_LEGACY;
730+
723731
const ledgerTxData = await toLedgerTx(body, {
724732
accountIndex: this.accountIndex,
725733
chainId: this.chainId,
734+
collateralReturnFormat,
726735
dRepKeyHashHex,
727736
knownAddresses,
728-
txInKeyPathMap,
729-
useBabbageOutputs: txBody.hasBabbageOutput()
737+
outputsFormat,
738+
txInKeyPathMap
730739
});
731740

732741
const deviceConnection = await LedgerKeyAgent.checkDeviceConnection(

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ import { LedgerTxTransformerContext } from '../types';
33
import { toTxOut } from './txOut';
44

55
export const mapCollateralTxOut = (collateralTxOut: Cardano.TxOut | undefined, context: LedgerTxTransformerContext) =>
6-
collateralTxOut ? toTxOut(collateralTxOut, context) : null;
6+
collateralTxOut ? toTxOut({ index: 0, isCollateral: true, txOut: collateralTxOut }, context) : null;

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

+24-18
Original file line numberDiff line numberDiff line change
@@ -56,27 +56,33 @@ const getScriptHex = (output: Serialization.TransactionOutput): HexBlob | null =
5656
return scriptRef.toCbor();
5757
};
5858

59-
export const toTxOut: Transform<Cardano.TxOut, Ledger.TxOutput, LedgerTxTransformerContext> = (txOut, context) => {
59+
export const toTxOut: Transform<
60+
{ txOut: Cardano.TxOut; index: number; isCollateral: boolean },
61+
Ledger.TxOutput,
62+
LedgerTxTransformerContext
63+
> = (elem, context) => {
64+
const { txOut, index, isCollateral } = elem;
6065
const output = Serialization.TransactionOutput.fromCore(txOut);
6166
const scriptHex = getScriptHex(output);
67+
const format = isCollateral ? context?.collateralReturnFormat : context?.outputsFormat[index];
68+
const isBabbageFormat = format === Ledger.TxOutputFormat.MAP_BABBAGE;
6269

63-
return context?.useBabbageOutputs
64-
? {
65-
amount: txOut.value.coins,
66-
datum: txOut.datumHash ? toDatumHash(txOut.datumHash) : txOut.datum ? toInlineDatum(txOut.datum) : null,
67-
destination: toDestination(txOut, context),
68-
format: Ledger.TxOutputFormat.MAP_BABBAGE,
69-
referenceScriptHex: scriptHex,
70-
tokenBundle: mapTokenMap(txOut.value.assets)
71-
}
72-
: {
73-
amount: txOut.value.coins,
74-
datumHashHex: txOut.datumHash ? txOut.datumHash : null,
75-
destination: toDestination(txOut, context),
76-
format: Ledger.TxOutputFormat.ARRAY_LEGACY,
77-
tokenBundle: mapTokenMap(txOut.value.assets)
78-
};
70+
return {
71+
amount: txOut.value.coins,
72+
destination: toDestination(txOut, context),
73+
tokenBundle: mapTokenMap(txOut.value.assets),
74+
...(isBabbageFormat
75+
? {
76+
datum: txOut.datumHash ? toDatumHash(txOut.datumHash) : txOut.datum ? toInlineDatum(txOut.datum) : null,
77+
format: Ledger.TxOutputFormat.MAP_BABBAGE,
78+
referenceScriptHex: scriptHex
79+
}
80+
: {
81+
datumHashHex: txOut.datumHash ?? null,
82+
format: Ledger.TxOutputFormat.ARRAY_LEGACY
83+
})
84+
};
7985
};
8086

8187
export const mapTxOuts = (txOuts: Cardano.TxOut[], context: LedgerTxTransformerContext): Ledger.TxOutput[] =>
82-
txOuts.map((txOut) => toTxOut(txOut, context));
88+
txOuts.map((txOut, index) => toTxOut({ index, isCollateral: false, txOut }, context));

packages/hardware-ledger/src/types.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Cardano } from '@cardano-sdk/core';
22
import { HID } from 'node-hid';
33
import { SignTransactionContext } from '@cardano-sdk/key-management';
4+
import { TxOutputFormat } from '@cardano-foundation/ledgerjs-hw-app-cardano';
45
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid-noevents';
56
import TransportWebUSB from '@ledgerhq/hw-transport-webusb';
67

@@ -21,6 +22,8 @@ export type LedgerTxTransformerContext = {
2122
chainId: Cardano.ChainId;
2223
/** Non-hardened account in cip1852 */
2324
accountIndex: number;
24-
/** Whether to use Babbage output format or not. */
25-
useBabbageOutputs: boolean;
25+
/** The outputs format in the same order as they appear in the transaction. */
26+
outputsFormat: Array<TxOutputFormat>;
27+
/** The collateral return output format. */
28+
collateralReturnFormat: TxOutputFormat | undefined;
2629
} & SignTransactionContext;

packages/hardware-ledger/test/testData.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as Crypto from '@cardano-sdk/crypto';
2+
import * as Ledger from '@cardano-foundation/ledgerjs-hw-app-cardano';
23
import { AddressType, KeyRole } from '@cardano-sdk/key-management';
34
import { Base64Blob, HexBlob } from '@cardano-sdk/util';
45
import { Cardano, Serialization } from '@cardano-sdk/core';
@@ -342,6 +343,7 @@ export const CONTEXT_WITH_KNOWN_ADDRESSES: LedgerTxTransformerContext = {
342343
networkId: Cardano.NetworkId.Testnet,
343344
networkMagic: 999
344345
},
346+
collateralReturnFormat: Ledger.TxOutputFormat.MAP_BABBAGE,
345347
dRepKeyHashHex: Crypto.Ed25519KeyHashHex(dRepCredential.hash),
346348
knownAddresses: [
347349
{
@@ -357,8 +359,8 @@ export const CONTEXT_WITH_KNOWN_ADDRESSES: LedgerTxTransformerContext = {
357359
type: AddressType.Internal
358360
}
359361
],
360-
txInKeyPathMap: {},
361-
useBabbageOutputs: true
362+
outputsFormat: [Ledger.TxOutputFormat.ARRAY_LEGACY, Ledger.TxOutputFormat.MAP_BABBAGE],
363+
txInKeyPathMap: {}
362364
};
363365

364366
export const CONTEXT_WITHOUT_KNOWN_ADDRESSES: LedgerTxTransformerContext = {
@@ -367,9 +369,10 @@ export const CONTEXT_WITHOUT_KNOWN_ADDRESSES: LedgerTxTransformerContext = {
367369
networkId: Cardano.NetworkId.Testnet,
368370
networkMagic: 999
369371
},
372+
collateralReturnFormat: Ledger.TxOutputFormat.ARRAY_LEGACY,
370373
knownAddresses: [],
371-
txInKeyPathMap: {},
372-
useBabbageOutputs: true
374+
outputsFormat: [Ledger.TxOutputFormat.ARRAY_LEGACY, Ledger.TxOutputFormat.MAP_BABBAGE],
375+
txInKeyPathMap: {}
373376
};
374377

375378
export const votes = [

packages/hardware-ledger/test/transformers/certificates.test.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,21 @@ export const createTxInKeyPathMapMock = (knownAddresses: GroupedAddress[]): TxIn
7373
const mockContext: LedgerTxTransformerContext = {
7474
accountIndex: 0,
7575
chainId: createChainId(1, 764_824_073),
76+
collateralReturnFormat: Ledger.TxOutputFormat.ARRAY_LEGACY,
77+
7678
dRepKeyHashHex: undefined,
7779

7880
handleResolutions: [],
79-
8081
knownAddresses: [
8182
createGroupedAddress(address1, ownRewardAccount, AddressType.External, 0, stakeKeyPath),
8283
createGroupedAddress(address2, ownRewardAccount, AddressType.External, 1, stakeKeyPath)
8384
],
85+
outputsFormat: [Ledger.TxOutputFormat.ARRAY_LEGACY, Ledger.TxOutputFormat.MAP_BABBAGE],
8486
sender: undefined,
8587
txInKeyPathMap: createTxInKeyPathMapMock([
8688
createGroupedAddress(address1, ownRewardAccount, AddressType.External, 0, stakeKeyPath),
8789
createGroupedAddress(address2, ownRewardAccount, AddressType.External, 1, stakeKeyPath)
88-
]),
89-
useBabbageOutputs: true
90+
])
9091
};
9192

9293
const EXAMPLE_URL = 'https://example.com';

packages/hardware-ledger/test/transformers/tx.test.ts

+3-8
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ describe('tx', () => {
1515
txInKeyPathMap: {
1616
[TxInId(tx.body.inputs[0])]: paymentKeyPath,
1717
[TxInId(tx.body.collaterals![0])]: paymentKeyPath
18-
},
19-
useBabbageOutputs: false
18+
}
2019
})
2120
).toEqual({
2221
auxiliaryData: {
@@ -181,6 +180,7 @@ describe('tx', () => {
181180
expect(
182181
await toLedgerTx(babbageTxWithoutScript.body, {
183182
...CONTEXT_WITH_KNOWN_ADDRESSES,
183+
outputsFormat: [Ledger.TxOutputFormat.MAP_BABBAGE],
184184
txInKeyPathMap: {
185185
[TxInId(babbageTxWithoutScript.body.inputs[0])]: paymentKeyPath
186186
}
@@ -266,12 +266,7 @@ describe('tx', () => {
266266
]
267267
};
268268

269-
expect(
270-
await toLedgerTx(txBodyWithRegistrationCert, {
271-
...CONTEXT_WITH_KNOWN_ADDRESSES,
272-
useBabbageOutputs: false
273-
})
274-
).toEqual({
269+
expect(await toLedgerTx(txBodyWithRegistrationCert, CONTEXT_WITH_KNOWN_ADDRESSES)).toEqual({
275270
auxiliaryData: {
276271
params: {
277272
hashHex: '2ceb364d93225b4a0f004a0975a13eb50c3cc6348474b4fe9121f8dc72ca0cfa'

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

+18-5
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ describe('txOut', () => {
1818
txOutWithReferenceScriptWithInlineDatum,
1919
txOutWithReferenceScriptWithInlineDatum
2020
],
21-
CONTEXT_WITH_KNOWN_ADDRESSES
21+
{
22+
...CONTEXT_WITH_KNOWN_ADDRESSES,
23+
outputsFormat: [
24+
Ledger.TxOutputFormat.MAP_BABBAGE,
25+
Ledger.TxOutputFormat.MAP_BABBAGE,
26+
Ledger.TxOutputFormat.MAP_BABBAGE
27+
]
28+
}
2229
);
2330

2431
expect(txOuts.length).toEqual(3);
@@ -64,7 +71,7 @@ describe('txOut', () => {
6471

6572
describe('toTxOut', () => {
6673
it('can map a simple txOut to third party address', async () => {
67-
const out = toTxOut(txOut, { ...CONTEXT_WITH_KNOWN_ADDRESSES, useBabbageOutputs: false });
74+
const out = toTxOut({ index: 0, isCollateral: false, txOut }, CONTEXT_WITH_KNOWN_ADDRESSES);
6875

6976
expect(out).toEqual({
7077
amount: 10n,
@@ -114,7 +121,7 @@ describe('txOut', () => {
114121
});
115122

116123
it('can map a simple txOut to owned address', async () => {
117-
const out = toTxOut(txOutToOwnedAddress, { ...CONTEXT_WITH_KNOWN_ADDRESSES, useBabbageOutputs: false });
124+
const out = toTxOut({ index: 0, isCollateral: false, txOut: txOutToOwnedAddress }, CONTEXT_WITH_KNOWN_ADDRESSES);
118125

119126
expect(out).toEqual({
120127
amount: 10n,
@@ -179,7 +186,10 @@ describe('txOut', () => {
179186
});
180187

181188
it('can map a txOut with a reference script - datum hash', async () => {
182-
const out = toTxOut(txOutWithReferenceScript, CONTEXT_WITH_KNOWN_ADDRESSES);
189+
const out = toTxOut(
190+
{ index: 1, isCollateral: false, txOut: txOutWithReferenceScript },
191+
CONTEXT_WITH_KNOWN_ADDRESSES
192+
);
183193

184194
expect(out).toEqual({
185195
amount: 10n,
@@ -216,7 +226,10 @@ describe('txOut', () => {
216226
});
217227

218228
it('can map a txOut with a reference script - inline datum', async () => {
219-
const out = toTxOut(txOutWithReferenceScriptWithInlineDatum, CONTEXT_WITH_KNOWN_ADDRESSES);
229+
const out = toTxOut(
230+
{ index: 1, isCollateral: false, txOut: txOutWithReferenceScriptWithInlineDatum },
231+
CONTEXT_WITH_KNOWN_ADDRESSES
232+
);
220233

221234
expect(out).toEqual({
222235
amount: 10n,

packages/hardware-trezor/src/TrezorKeyAgent.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,25 @@ export class TrezorKeyAgent extends KeyAgentBase {
246246
const body = txBody.toCore();
247247
const hash = txBody.hash() as unknown as HexBlob;
248248

249+
const outputsFormat = txBody
250+
.outputs()
251+
.map((out) =>
252+
out.isBabbageOutput()
253+
? Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE
254+
: Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY
255+
);
256+
const collateralReturnFormat = txBody.collateralReturn()?.isBabbageOutput()
257+
? Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE
258+
: Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY;
259+
249260
const trezorTxData = await txToTrezor(body, {
250261
accountIndex: this.accountIndex,
251262
chainId: this.chainId,
263+
collateralReturnFormat,
252264
knownAddresses,
265+
outputsFormat,
253266
tagCborSets: txBody.hasTaggedSets(),
254-
txInKeyPathMap,
255-
useBabbageOutputs: txBody.hasBabbageOutput()
267+
txInKeyPathMap
256268
});
257269

258270
const signingMode = TrezorKeyAgent.matchSigningMode(trezorTxData);

packages/hardware-trezor/src/transformers/tx.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const trezorTxTransformer: Transformer<
1919
certificates: ({ certificates }, context) => (certificates ? mapCerts(certificates, context!) : undefined),
2020
collateralInputs: ({ collaterals }, context) => (collaterals ? mapTxIns(collaterals, context!) : undefined),
2121
collateralReturn: ({ collateralReturn }, context) =>
22-
collateralReturn ? toTxOut(collateralReturn, context!) : undefined,
22+
collateralReturn ? toTxOut({ index: 0, isCollateral: true, txOut: collateralReturn }, context!) : undefined,
2323
fee: ({ fee }) => fee.toString(),
2424
inputs: ({ inputs }, context) => mapTxIns(inputs, context!),
2525
mint: ({ mint }) => mapTokenMap(mint, true),

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

+18-22
Original file line numberDiff line numberDiff line change
@@ -42,31 +42,27 @@ const getScriptHex = (output: Serialization.TransactionOutput): HexBlob | undefi
4242

4343
const getInlineDatum = (datum: Cardano.PlutusData): string => Serialization.PlutusData.fromCore(datum).toCbor();
4444

45-
export const toTxOut: Transform<Cardano.TxOut, Trezor.CardanoOutput, TrezorTxTransformerContext> = (txOut, context) => {
46-
const destination = toDestination(txOut, context);
45+
export const toTxOut: Transform<
46+
{ txOut: Cardano.TxOut; index: number; isCollateral: boolean },
47+
Trezor.CardanoOutput,
48+
TrezorTxTransformerContext
49+
> = (elem, context) => {
50+
const { txOut, index, isCollateral } = elem;
4751
const output = Serialization.TransactionOutput.fromCore(txOut);
4852
const scriptHex = getScriptHex(output);
53+
const format = isCollateral ? context?.collateralReturnFormat : context?.outputsFormat[index];
54+
const isBabbage = format === Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE;
4955

50-
return context?.useBabbageOutputs
51-
? {
52-
...destination,
53-
amount: txOut.value.coins.toString(),
54-
datumHash: txOut.datumHash?.toString(),
55-
format: Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE,
56-
inlineDatum: txOut.datum ? getInlineDatum(txOut.datum) : undefined,
57-
referenceScript: scriptHex,
58-
tokenBundle: mapTokenMap(txOut.value.assets)
59-
}
60-
: {
61-
...destination,
62-
amount: txOut.value.coins.toString(),
63-
datumHash: txOut.datumHash?.toString(),
64-
format: Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY,
65-
inlineDatum: undefined,
66-
referenceScript: undefined,
67-
tokenBundle: mapTokenMap(txOut.value.assets)
68-
};
56+
return {
57+
...toDestination(txOut, context),
58+
amount: txOut.value.coins.toString(),
59+
datumHash: txOut.datumHash?.toString(),
60+
format,
61+
inlineDatum: isBabbage ? (txOut.datum ? getInlineDatum(txOut.datum) : undefined) : undefined,
62+
referenceScript: isBabbage ? scriptHex : undefined,
63+
tokenBundle: mapTokenMap(txOut.value.assets)
64+
};
6965
};
7066

7167
export const mapTxOuts = (txOuts: Cardano.TxOut[], context: TrezorTxTransformerContext): Trezor.CardanoOutput[] =>
72-
txOuts.map((txOut) => toTxOut(txOut, context));
68+
txOuts.map((txOut, index) => toTxOut({ index, isCollateral: false, txOut }, context));

packages/hardware-trezor/src/types.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ export type TrezorTxTransformerContext = {
1313
accountIndex: number;
1414
/** Whether sets should be encoded as tagged set in CBOR */
1515
tagCborSets: boolean;
16-
/** Whether to use Babbage output format or not. */
17-
useBabbageOutputs: boolean;
16+
/** The outputs format in the same order as they appear in the transaction. */
17+
outputsFormat: Array<Trezor.PROTO.CardanoTxOutputSerializationFormat>;
18+
/** The collateral return output format. */
19+
collateralReturnFormat: Trezor.PROTO.CardanoTxOutputSerializationFormat | undefined;
1820
} & SignTransactionContext;
1921

2022
export type TrezorTxOutputDestination =

0 commit comments

Comments
 (0)