Skip to content

Commit a7c5b18

Browse files
Merge pull request #1616 from input-output-hk/fix/output-mapping-on-hardware-wallets
LW-12624: Fix output mapping on hardware wallets
2 parents 17add5a + 3de9a1d commit a7c5b18

File tree

16 files changed

+103
-85
lines changed

16 files changed

+103
-85
lines changed

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

+13
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,19 @@ export class TransactionBody {
947947
return reader.peekState() === CborReaderState.Tag && reader.peekTag() === CborTag.Set;
948948
}
949949

950+
/**
951+
* Checks if the transaction body has Babbage outputs.
952+
*
953+
* @returns true if the transaction body has Babbage outputs, false otherwise.
954+
*/
955+
hasBabbageOutput(): boolean {
956+
if (this.#outputs.length === 0) return false;
957+
958+
const reader = new CborReader(this.#outputs[0].toCbor());
959+
960+
return reader.peekState() === CborReaderState.StartMap;
961+
}
962+
950963
/**
951964
* Gets the size of the serialized map.
952965
*

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

+19-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as Cardano from '../../../src/Cardano';
33
import * as Crypto from '@cardano-sdk/crypto';
44
import { HexBlob } from '@cardano-sdk/util';
5-
import { TransactionBody, TxBodyCBOR, TxCBOR } from '../../../src/Serialization';
5+
import { Transaction, TransactionBody, TxBodyCBOR, TxCBOR } from '../../../src/Serialization';
66
import { babbageTx } from '../testData';
77
import { mintTokenMap, params, txIn, txOut } from './testData';
88

@@ -273,6 +273,24 @@ describe('TransactionBody', () => {
273273
expect(body.toCore()).toEqual(expectedConwayCore);
274274
});
275275

276+
it('can encode identify transactions output format - Babbage outputs', () => {
277+
const tx = Transaction.fromCbor(
278+
TxCBOR(
279+
'84a500818258207aa1264bcd0c06f34a49ed1dd7307a2bdec5a97bdeb546498759ad5b8ed42fd5010182a200583930195bde3deacb613b7e9eb6280b14db4e353e475e96d19f3f7a5e2d66195bde3deacb613b7e9eb6280b14db4e353e475e96d19f3f7a5e2d66011a00e4e1c0a2005839003d3246dc0c50ab3c74a8ffdd8068313ff99d341c461a8fe31f416d0a8fba06d60d71edc077cc5ebcb8ff82137afbce68df98271909332348011b000000025106a838021a00029309031a04a07bc6081a04a07a40a0f5f6'
280+
)
281+
);
282+
expect(tx.body().hasBabbageOutput()).toBeTruthy();
283+
});
284+
285+
it('can encode identify transactions output format - Legacy outputs', () => {
286+
const tx = Transaction.fromCbor(
287+
TxCBOR(
288+
'84a500818258207aa1264bcd0c06f34a49ed1dd7307a2bdec5a97bdeb546498759ad5b8ed42fd501018282583930195bde3deacb613b7e9eb6280b14db4e353e475e96d19f3f7a5e2d66195bde3deacb613b7e9eb6280b14db4e353e475e96d19f3f7a5e2d661a00e4e1c0825839003d3246dc0c50ab3c74a8ffdd8068313ff99d341c461a8fe31f416d0a8fba06d60d71edc077cc5ebcb8ff82137afbce68df982719093323481b000000025106a838021a00029309031a04a07bc6081a04a07a40a0f5f6'
289+
)
290+
);
291+
expect(tx.body().hasBabbageOutput()).toBeFalsy();
292+
});
293+
276294
it('sorts withdrawals canonically', () => {
277295
const body = TransactionBody.fromCbor(cbor);
278296
const withdrawals = body.withdrawals();

packages/hardware-ledger/src/LedgerKeyAgent.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -715,15 +715,18 @@ export class LedgerKeyAgent extends KeyAgentBase {
715715
): Promise<Cardano.Signatures> {
716716
try {
717717
const body = txBody.toCore();
718+
718719
const hash = txBody.hash() as unknown as HexBlob;
719720
const dRepPublicKey = await this.derivePublicKey(util.DREP_KEY_DERIVATION_PATH);
720721
const dRepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepPublicKey).hash()).hex();
722+
721723
const ledgerTxData = await toLedgerTx(body, {
722724
accountIndex: this.accountIndex,
723725
chainId: this.chainId,
724726
dRepKeyHashHex,
725727
knownAddresses,
726-
txInKeyPathMap
728+
txInKeyPathMap,
729+
useBabbageOutputs: txBody.hasBabbageOutput()
727730
});
728731

729732
const deviceConnection = await LedgerKeyAgent.checkDeviceConnection(

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

+1-29
Original file line numberDiff line numberDiff line change
@@ -56,39 +56,11 @@ const getScriptHex = (output: Serialization.TransactionOutput): HexBlob | null =
5656
return scriptRef.toCbor();
5757
};
5858

59-
/**
60-
* There are currently two types of outputs supported by the ledger:
61-
*
62-
* legacy_transaction_output =
63-
* [ address
64-
* , amount : value
65-
* , ? datum_hash : $hash32
66-
* ]
67-
*
68-
* and
69-
*
70-
* post_alonzo_transaction_output =
71-
* { 0 : address
72-
* , 1 : value
73-
* , ? 2 : datum_option ; New; datum option
74-
* , ? 3 : script_ref ; New; script reference
75-
* }
76-
*
77-
* Legacy outputs are definite length arrays of three elements, however the new babbage outputs are definite length maps
78-
* of four elements.
79-
*
80-
* @param out The output to be verified.
81-
*/
82-
const isBabbage = (out: Serialization.TransactionOutput): boolean => {
83-
const reader = new Serialization.CborReader(out.toCbor());
84-
return reader.peekState() === Serialization.CborReaderState.StartMap;
85-
};
86-
8759
export const toTxOut: Transform<Cardano.TxOut, Ledger.TxOutput, LedgerTxTransformerContext> = (txOut, context) => {
8860
const output = Serialization.TransactionOutput.fromCore(txOut);
8961
const scriptHex = getScriptHex(output);
9062

91-
return isBabbage(output)
63+
return context?.useBabbageOutputs
9264
? {
9365
amount: txOut.value.coins,
9466
datum: txOut.datumHash ? toDatumHash(txOut.datumHash) : txOut.datum ? toInlineDatum(txOut.datum) : null,

packages/hardware-ledger/src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ export type LedgerTxTransformerContext = {
2121
chainId: Cardano.ChainId;
2222
/** Non-hardened account in cip1852 */
2323
accountIndex: number;
24+
/** Whether to use Babbage output format or not. */
25+
useBabbageOutputs: boolean;
2426
} & SignTransactionContext;

packages/hardware-ledger/test/testData.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,8 @@ export const CONTEXT_WITH_KNOWN_ADDRESSES: LedgerTxTransformerContext = {
357357
type: AddressType.Internal
358358
}
359359
],
360-
txInKeyPathMap: {}
360+
txInKeyPathMap: {},
361+
useBabbageOutputs: true
361362
};
362363

363364
export const CONTEXT_WITHOUT_KNOWN_ADDRESSES: LedgerTxTransformerContext = {
@@ -367,7 +368,8 @@ export const CONTEXT_WITHOUT_KNOWN_ADDRESSES: LedgerTxTransformerContext = {
367368
networkMagic: 999
368369
},
369370
knownAddresses: [],
370-
txInKeyPathMap: {}
371+
txInKeyPathMap: {},
372+
useBabbageOutputs: true
371373
};
372374

373375
export const votes = [

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ const mockContext: LedgerTxTransformerContext = {
8585
txInKeyPathMap: createTxInKeyPathMapMock([
8686
createGroupedAddress(address1, ownRewardAccount, AddressType.External, 0, stakeKeyPath),
8787
createGroupedAddress(address2, ownRewardAccount, AddressType.External, 1, stakeKeyPath)
88-
])
88+
]),
89+
useBabbageOutputs: true
8990
};
9091

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

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ describe('tx', () => {
1515
txInKeyPathMap: {
1616
[TxInId(tx.body.inputs[0])]: paymentKeyPath,
1717
[TxInId(tx.body.collaterals![0])]: paymentKeyPath
18-
}
18+
},
19+
useBabbageOutputs: false
1920
})
2021
).toEqual({
2122
auxiliaryData: {
@@ -267,7 +268,8 @@ describe('tx', () => {
267268

268269
expect(
269270
await toLedgerTx(txBodyWithRegistrationCert, {
270-
...CONTEXT_WITH_KNOWN_ADDRESSES
271+
...CONTEXT_WITH_KNOWN_ADDRESSES,
272+
useBabbageOutputs: false
271273
})
272274
).toEqual({
273275
auxiliaryData: {

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe('txOut', () => {
6464

6565
describe('toTxOut', () => {
6666
it('can map a simple txOut to third party address', async () => {
67-
const out = toTxOut(txOut, CONTEXT_WITH_KNOWN_ADDRESSES);
67+
const out = toTxOut(txOut, { ...CONTEXT_WITH_KNOWN_ADDRESSES, useBabbageOutputs: false });
6868

6969
expect(out).toEqual({
7070
amount: 10n,
@@ -114,7 +114,7 @@ describe('txOut', () => {
114114
});
115115

116116
it('can map a simple txOut to owned address', async () => {
117-
const out = toTxOut(txOutToOwnedAddress, CONTEXT_WITH_KNOWN_ADDRESSES);
117+
const out = toTxOut(txOutToOwnedAddress, { ...CONTEXT_WITH_KNOWN_ADDRESSES, useBabbageOutputs: false });
118118

119119
expect(out).toEqual({
120120
amount: 10n,

packages/hardware-trezor/src/TrezorKeyAgent.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ export class TrezorKeyAgent extends KeyAgentBase {
251251
chainId: this.chainId,
252252
knownAddresses,
253253
tagCborSets: txBody.hasTaggedSets(),
254-
txInKeyPathMap
254+
txInKeyPathMap,
255+
useBabbageOutputs: txBody.hasBabbageOutput()
255256
});
256257

257258
const signingMode = TrezorKeyAgent.matchSigningMode(trezorTxData);

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

+1-29
Original file line numberDiff line numberDiff line change
@@ -40,42 +40,14 @@ const getScriptHex = (output: Serialization.TransactionOutput): HexBlob | undefi
4040
return scriptRef.toCbor();
4141
};
4242

43-
/**
44-
* There are currently two types of outputs supported by the ledger:
45-
*
46-
* legacy_transaction_output =
47-
* [ address
48-
* , amount : value
49-
* , ? datum_hash : $hash32
50-
* ]
51-
*
52-
* and
53-
*
54-
* post_alonzo_transaction_output =
55-
* { 0 : address
56-
* , 1 : value
57-
* , ? 2 : datum_option ; New; datum option
58-
* , ? 3 : script_ref ; New; script reference
59-
* }
60-
*
61-
* Legacy outputs are definite length arrays of three elements, however the new babbage outputs are definite length maps
62-
* of four elements.
63-
*
64-
* @param out The output to be verified.
65-
*/
66-
const isBabbage = (out: Serialization.TransactionOutput): boolean => {
67-
const reader = new Serialization.CborReader(out.toCbor());
68-
return reader.peekState() === Serialization.CborReaderState.StartMap;
69-
};
70-
7143
const getInlineDatum = (datum: Cardano.PlutusData): string => Serialization.PlutusData.fromCore(datum).toCbor();
7244

7345
export const toTxOut: Transform<Cardano.TxOut, Trezor.CardanoOutput, TrezorTxTransformerContext> = (txOut, context) => {
7446
const destination = toDestination(txOut, context);
7547
const output = Serialization.TransactionOutput.fromCore(txOut);
7648
const scriptHex = getScriptHex(output);
7749

78-
return isBabbage(output)
50+
return context?.useBabbageOutputs
7951
? {
8052
...destination,
8153
amount: txOut.value.coins.toString(),

packages/hardware-trezor/src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ 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;
1618
} & SignTransactionContext;
1719

1820
export type TrezorTxOutputDestination =

packages/hardware-trezor/test/testData.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ export const contextWithKnownAddresses: TrezorTxTransformerContext = {
171171
},
172172
knownAddresses: [knownAddress],
173173
tagCborSets: false,
174-
txInKeyPathMap: {}
174+
txInKeyPathMap: {},
175+
useBabbageOutputs: false
175176
};
176177

177178
export const contextWithKnownAddressesWithoutStakingCredentials: TrezorTxTransformerContext = {
@@ -182,7 +183,8 @@ export const contextWithKnownAddressesWithoutStakingCredentials: TrezorTxTransfo
182183
},
183184
knownAddresses: [knownAddressWithoutStakingPath],
184185
tagCborSets: false,
185-
txInKeyPathMap: {}
186+
txInKeyPathMap: {},
187+
useBabbageOutputs: false
186188
};
187189

188190
export const contextWithoutKnownAddresses: TrezorTxTransformerContext = {
@@ -193,7 +195,8 @@ export const contextWithoutKnownAddresses: TrezorTxTransformerContext = {
193195
},
194196
knownAddresses: [],
195197
tagCborSets: false,
196-
txInKeyPathMap: {}
198+
txInKeyPathMap: {},
199+
useBabbageOutputs: false
197200
};
198201

199202
export const coreWithdrawalWithKeyHashCredential = {

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,8 @@ describe('tx', () => {
204204
...contextWithKnownAddresses,
205205
txInKeyPathMap: {
206206
[TxInId(babbageTxBodyWithScripts.inputs[0])]: knownAddressPaymentKeyPath
207-
}
207+
},
208+
useBabbageOutputs: true
208209
})
209210
).toEqual({
210211
additionalWitnessRequests: [
@@ -327,7 +328,8 @@ describe('tx', () => {
327328
txInKeyPathMap: {
328329
[TxInId(plutusTxWithBabbage.inputs[0])]: knownAddressPaymentKeyPath,
329330
[TxInId(plutusTxWithBabbage.collaterals[0])]: knownAddressPaymentKeyPath
330-
}
331+
},
332+
useBabbageOutputs: true
331333
})
332334
).toEqual({
333335
additionalWitnessRequests: [
@@ -361,7 +363,7 @@ describe('tx', () => {
361363
address:
362364
'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp',
363365
amount: '10',
364-
format: Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY
366+
format: Trezor.PROTO.CardanoTxOutputSerializationFormat.MAP_BABBAGE
365367
},
366368
fee: '10',
367369
inputs: [

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

+27-11
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,16 @@ describe('txOut', () => {
156156
});
157157

158158
it('can map a set of transaction outputs with both output formats', async () => {
159-
const txOuts = mapTxOuts(
160-
[txOutWithDatumHashAndOwnedAddress, txOutWithReferenceScriptAndDatumHash],
161-
contextWithKnownAddresses
162-
);
159+
const legacyTxOuts = mapTxOuts([txOutWithDatumHashAndOwnedAddress], contextWithKnownAddresses);
163160

164-
expect(txOuts.length).toEqual(2);
161+
const babbageTxOuts = mapTxOuts([txOutWithReferenceScriptAndDatumHash], {
162+
...contextWithKnownAddresses,
163+
useBabbageOutputs: true
164+
});
165165

166-
expect(txOuts).toEqual([
166+
expect(legacyTxOuts.length).toEqual(1);
167+
168+
expect(legacyTxOuts).toEqual([
167169
{
168170
addressParameters: {
169171
addressType: Trezor.PROTO.CardanoAddressType.BASE,
@@ -173,7 +175,12 @@ describe('txOut', () => {
173175
amount: '10',
174176
datumHash: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5',
175177
format: Trezor.PROTO.CardanoTxOutputSerializationFormat.ARRAY_LEGACY
176-
},
178+
}
179+
]);
180+
181+
expect(babbageTxOuts.length).toEqual(1);
182+
183+
expect(babbageTxOuts).toEqual([
177184
{
178185
addressParameters: {
179186
addressType: Trezor.PROTO.CardanoAddressType.BASE,
@@ -332,7 +339,7 @@ describe('txOut', () => {
332339
});
333340

334341
it('can map simple transaction with inline datum', async () => {
335-
const out = toTxOut(txOutWithInlineDatum, contextWithKnownAddresses);
342+
const out = toTxOut(txOutWithInlineDatum, { ...contextWithKnownAddresses, useBabbageOutputs: true });
336343

337344
expect(out).toEqual({
338345
address:
@@ -344,7 +351,10 @@ describe('txOut', () => {
344351
});
345352

346353
it('can map simple transaction with inline datum to owned address', async () => {
347-
const out = toTxOut(txOutWithInlineDatumAndOwnedAddress, contextWithKnownAddresses);
354+
const out = toTxOut(txOutWithInlineDatumAndOwnedAddress, {
355+
...contextWithKnownAddresses,
356+
useBabbageOutputs: true
357+
});
348358

349359
expect(out).toEqual({
350360
addressParameters: {
@@ -359,7 +369,10 @@ describe('txOut', () => {
359369
});
360370

361371
it('can map a simple transaction output with reference script and datum hash', async () => {
362-
const out = toTxOut(txOutWithReferenceScriptAndDatumHash, contextWithKnownAddresses);
372+
const out = toTxOut(txOutWithReferenceScriptAndDatumHash, {
373+
...contextWithKnownAddresses,
374+
useBabbageOutputs: true
375+
});
363376
expect(out).toEqual({
364377
addressParameters: {
365378
addressType: Trezor.PROTO.CardanoAddressType.BASE,
@@ -374,7 +387,10 @@ describe('txOut', () => {
374387
});
375388

376389
it('can map a simple transaction output with reference script and inline datum', async () => {
377-
const out = toTxOut(txOutWithReferenceScriptAndInlineDatum, contextWithKnownAddresses);
390+
const out = toTxOut(txOutWithReferenceScriptAndInlineDatum, {
391+
...contextWithKnownAddresses,
392+
useBabbageOutputs: true
393+
});
378394
expect(out).toEqual({
379395
addressParameters: {
380396
addressType: Trezor.PROTO.CardanoAddressType.BASE,

0 commit comments

Comments
 (0)