Skip to content

Commit f262d73

Browse files
authored
Merge pull request #1243 from IntersectMBO/cardano-wasm-getTxId
cardano-wasm: expose the transaction id (getTxId)
2 parents 49d3ec7 + 491698a commit f262d73

10 files changed

Lines changed: 131 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
description: Added `getTxId` to the `cardano-wasm` API (on `UnsignedTx` and `SignedTx`), returning the transaction id (the blake2b-256 hash of the transaction body) as a hex string. The id is available as soon as the transaction body is final and is not affected by signing, so flows that do not submit through the app (e.g. air-gapped signing) can display, copy, or link the id.
2+
kind:
3+
- feature
4+
pr: 1243
5+
project: cardano-wasm

cardano-wasm/cardano-wasm.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ executable cardano-wasi
8080
if arch(wasm32)
8181
ghc-options:
8282
-no-hs-main
83-
"-optl-Wl,--strip-all,--export=hs_init,--export=newTx,--export=newUpcomingEraTx,--export=addTxInput,--export=addSimpleTxOut,--export=appendCertificateToTx,--export=setFee,--export=estimateMinFee,--export=signWithPaymentKey,--export=signWithStakeKey,--export=alsoSignWithPaymentKey,--export=alsoSignWithStakeKey,--export=toCbor,--export=makeStakeAddressStakeDelegationCertificate,--export=makeStakeAddressStakeDelegationCertificateUpcomingEra,--export=makeStakeAddressRegistrationCertificate,--export=makeStakeAddressRegistrationCertificateUpcomingEra,--export=makeStakeAddressUnregistrationCertificate,--export=makeStakeAddressUnregistrationCertificateUpcomingEra,--export=generatePaymentWallet,--export=generateStakeWallet,--export=restorePaymentWalletFromSigningKeyBech32,--export=restoreStakeWalletFromSigningKeyBech32,--export=generateTestnetPaymentWallet,--export=generateTestnetStakeWallet,--export=restoreTestnetPaymentWalletFromSigningKeyBech32,--export=restoreTestnetStakeWalletFromSigningKeyBech32,--export=getAddressBech32,--export=getBech32ForPaymentVerificationKey,--export=getBech32ForPaymentSigningKey,--export=getBech32ForStakeVerificationKey,--export=getBech32ForStakeSigningKey,--export=getBase16ForPaymentVerificationKeyHash,--export=getBase16ForStakeVerificationKeyHash,--export=mallocNBytes,--export=getStrLen,--export=freeMemory"
83+
"-optl-Wl,--strip-all,--export=hs_init,--export=newTx,--export=newUpcomingEraTx,--export=addTxInput,--export=addSimpleTxOut,--export=appendCertificateToTx,--export=setFee,--export=estimateMinFee,--export=signWithPaymentKey,--export=signWithStakeKey,--export=getUnsignedTxId,--export=alsoSignWithPaymentKey,--export=alsoSignWithStakeKey,--export=toCbor,--export=getSignedTxId,--export=makeStakeAddressStakeDelegationCertificate,--export=makeStakeAddressStakeDelegationCertificateUpcomingEra,--export=makeStakeAddressRegistrationCertificate,--export=makeStakeAddressRegistrationCertificateUpcomingEra,--export=makeStakeAddressUnregistrationCertificate,--export=makeStakeAddressUnregistrationCertificateUpcomingEra,--export=generatePaymentWallet,--export=generateStakeWallet,--export=restorePaymentWalletFromSigningKeyBech32,--export=restoreStakeWalletFromSigningKeyBech32,--export=generateTestnetPaymentWallet,--export=generateTestnetStakeWallet,--export=restoreTestnetPaymentWalletFromSigningKeyBech32,--export=restoreTestnetStakeWalletFromSigningKeyBech32,--export=getAddressBech32,--export=getBech32ForPaymentVerificationKey,--export=getBech32ForPaymentSigningKey,--export=getBech32ForStakeVerificationKey,--export=getBech32ForStakeSigningKey,--export=getBase16ForPaymentVerificationKeyHash,--export=getBase16ForStakeVerificationKeyHash,--export=mallocNBytes,--export=getStrLen,--export=freeMemory"
8484
other-modules:
8585
Cardano.Wasi.Internal.Api.Certificate.StakeCertificate
8686
Cardano.Wasi.Internal.Api.GRPC

cardano-wasm/js-test/basic-test.golden

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
    estimateMinFee: function (protocolParams,numKeyWitnesses,numByronKeyWitnesses,totalRefScriptSize)
1919
    signWithPaymentKey: function (signingKey)
2020
    signWithStakeKey: function (signingKey)
21+
    getTxId: function ()
2122
  }
2223
> "Estimated fee:"
2324
> 164005n
@@ -27,6 +28,7 @@
2728
    alsoSignWithPaymentKey: function (signingKey)
2829
    alsoSignWithStakeKey: function (signingKey)
2930
    txToCbor: function ()
31+
    getTxId: function ()
3032
  }
3133
> "Tx CBOR:"
3234
> "84a300d9010281825820be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd97800018182581d6082935e44937e8b530f32ce672b5d600d0a286b4e8a52c6555f659b871a00989680021a000280a5a100d9010281825820adfc1c30385916da87db1ba3328f0690a57ebb2a6ac9f6f86b2d97f943adae005840a49259b5977aea523b46f01261fbff93e0899e8700319e11f5ab96b67eb628fca1a233ce2d50ee3227b591b84f27237d920d63974d65728362382f751c4d9400f5f6"

cardano-wasm/lib-wrapper/signed-tx.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ declare interface SignedTx {
3030
* @returns A promise that resolves to the CBOR representation of the transaction as a hex string.
3131
*/
3232
txToCbor(): Promise<string>;
33+
34+
/**
35+
* Gets the transaction id (the hash of the transaction body). It is not affected by signing, so it matches the id of the unsigned transaction just before signing.
36+
* @returns A promise that resolves to the transaction id as a hex string.
37+
*/
38+
getTxId(): Promise<string>;
3339
}
3440

3541
export default SignedTx;

cardano-wasm/lib-wrapper/unsigned-tx.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ declare interface UnsignedTx {
6666
* @returns A promise that resolves to a `SignedTx` object.
6767
*/
6868
signWithStakeKey(signingKey: string): Promise<SignedTx>;
69+
70+
/**
71+
* Gets the transaction id (the hash of the transaction body). It can change if the transaction body is modified.
72+
* @returns A promise that resolves to the transaction id as a hex string.
73+
*/
74+
getTxId(): Promise<string>;
6975
}
7076

7177
export default UnsignedTx;

cardano-wasm/npm-wrapper/api.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,37 @@ describe('Cardano API', () => {
4646
expect(txCbor).toBe("84a300d9010281825820be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd97800018182581d6082935e44937e8b530f32ce672b5d600d0a286b4e8a52c6555f659b871a0098968002192710a100d9010281825820adfc1c30385916da87db1ba3328f0690a57ebb2a6ac9f6f86b2d97f943adae0058400b19a00593e659ad0f10951f0f7d1e8a8b93112c60f67277529f91340581639e92ed4d0042ff92a0076cd69deb7e708acfdb73bb4ae79cf4bc06fd6d15efa208f5f6");
4747
});
4848

49+
// The transaction id is the blake2b-256 hash of the transaction body, so it
50+
// is already available before signing and is not changed by adding witnesses.
51+
it('should expose the transaction id on both unsigned and signed transactions', async () => {
52+
// Test constants
53+
const secretKey = "addr_sk1648253w4tf6fv5fk28dc7crsjsaw7d9ymhztd4favg3cwkhz7x8sl5u3ms";
54+
const txInputHash = "be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd978";
55+
const outputAddress = "addr_test1vzpfxhjyjdlgk5c0xt8xw26avqxs52rtf69993j4tajehpcue4v2v";
56+
57+
// Same transaction as in the simple transaction test above. The expected id
58+
// is the blake2b-256 hash of the transaction body bytes in its expected CBOR
59+
// (computed independently of this library).
60+
const expectedTxId = "4eebc97a1c96eb5537d0ec2172fdb3f0c516dc209d9ab93de585956e0be99ff1";
61+
62+
const tx = (await api.tx.newTx())
63+
.addTxInput(txInputHash, 0)
64+
.addSimpleTxOut(outputAddress, 10_000_000n)
65+
.setFee(10_000n);
66+
67+
// The id is available as soon as the body is final (before signing)
68+
const unsignedTxId = await tx.getTxId();
69+
expect(unsignedTxId).toBe(expectedTxId);
70+
71+
// Signing does not change the transaction id
72+
const signedTx = await tx.signWithPaymentKey(secretKey);
73+
expect(await signedTx.getTxId()).toBe(expectedTxId);
74+
75+
// Nor does adding more witnesses
76+
const multiSignedTx = await signedTx.alsoSignWithPaymentKey(secretKey);
77+
expect(await multiSignedTx.getTxId()).toBe(expectedTxId);
78+
});
79+
4980
// Delegation requires a witness from the stake credential, added with the stake key.
5081
it('should build a delegation transaction signed with a payment key and a stake key', async () => {
5182
// Test constants
@@ -80,6 +111,9 @@ describe('Cardano API', () => {
80111
const txCbor = await signedTx.txToCbor();
81112

82113
expect(txCbor).toBe("84a400d9010281825820be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd97800018182581d6082935e44937e8b530f32ce672b5d600d0a286b4e8a52c6555f659b871a004c4b400219271004d901028183028200581ca9461e687627cddc5f54ffc988bc44321189538c601f1ad1b7979d9b581cbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbba100d9010282825820adfc1c30385916da87db1ba3328f0690a57ebb2a6ac9f6f86b2d97f943adae005840b5df44de4b1302d1b031363cfb9636be2f395561dcf083d1192656677506bfa401d988d745f2ee2d11ed9e563f35cc2dfbb605c7cd58a613e42377e5e2a8da01825820ee31f83c88a71219a6fcf9bee0da9bc22620588f5a15a6145553504df9649e5c58402a2a37f2391bdf3a1ff52a7ed2103473f35b43022ed101bddcb18a557f7e9f9eac75c3562b142cc15dc9ebd006da34f4076ff413914ec90e4359d757d73bfc06f5f6");
114+
115+
// The transaction id only depends on the body, so the stake witness does not change it
116+
expect(await signedTx.getTxId()).toBe(await tx.getTxId());
83117
});
84118

85119
// The stake key can also be the first signer, with the payment witness added afterwards.

cardano-wasm/src-lib/Cardano/Wasm/Api/Info.hs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,16 @@ apiInfo =
374374
, methodReturnType = NewObject (virtualObjectName signedTxObj)
375375
, methodReturnDoc = "A promise that resolves to a `SignedTx` object."
376376
}
377+
, MethodInfoEntry $
378+
MethodInfo
379+
{ methodName = "getUnsignedTxId"
380+
, methodSimpleName = Just "getTxId"
381+
, methodDoc =
382+
"Gets the transaction id (the hash of the transaction body). It can change if the transaction body is modified."
383+
, methodParams = []
384+
, methodReturnType = OtherType TSString
385+
, methodReturnDoc = "A promise that resolves to the transaction id as a hex string."
386+
}
377387
]
378388
}
379389

@@ -410,6 +420,16 @@ apiInfo =
410420
, methodReturnDoc =
411421
"A promise that resolves to the CBOR representation of the transaction as a hex string."
412422
}
423+
, MethodInfoEntry $
424+
MethodInfo
425+
{ methodName = "getSignedTxId"
426+
, methodSimpleName = Just "getTxId"
427+
, methodDoc =
428+
"Gets the transaction id (the hash of the transaction body). It is not affected by signing, so it matches the id of the unsigned transaction just before signing."
429+
, methodParams = []
430+
, methodReturnType = OtherType TSString
431+
, methodReturnDoc = "A promise that resolves to the transaction id as a hex string."
432+
}
413433
]
414434
}
415435

cardano-wasm/src-lib/Cardano/Wasm/Api/Tx.hs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ module Cardano.Wasm.Api.Tx
2424
, alsoSignWithPaymentKeyImpl
2525
, alsoSignWithStakeKeyImpl
2626
, toCborImpl
27+
, getUnsignedTxIdImpl
28+
, getSignedTxIdImpl
2729
)
2830
where
2931

@@ -49,7 +51,7 @@ import Data.Set qualified as Set
4951
import Data.Text qualified as Text
5052
import Data.Text.Encoding qualified as Text
5153
import GHC.Stack (HasCallStack)
52-
import Lens.Micro ((%~), (&), (.~), (<>~))
54+
import Lens.Micro ((%~), (&), (.~), (<>~), (^.))
5355

5456
-- * @UnsignedTx@ object
5557

@@ -211,6 +213,16 @@ estimateMinFeeImpl
211213
numByronKeyWitnesses
212214
totalRefScriptSize
213215

216+
-- | Get the transaction id of an unsigned transaction object. The transaction
217+
-- id is the blake2b-256 hash of the transaction body, so it is fully determined
218+
-- once the body is final and it is not affected by signing.
219+
getUnsignedTxIdImpl :: UnsignedTxObject -> String
220+
getUnsignedTxIdImpl (UnsignedTxObject era (Exp.UnsignedTx tx)) =
221+
obtainCommonConstraints era $
222+
Text.unpack $
223+
Api.serialiseToRawBytesHexText $
224+
TxBody.getTxIdShelley (Api.convert era) (tx ^. Ledger.bodyTxL)
225+
214226
-- * @SignedTx@ object
215227

216228
-- | An object representing a signed transaction.
@@ -272,3 +284,13 @@ toCborImpl (SignedTxObject era signedTx) =
272284
obtainCommonConstraints era $
273285
Text.unpack $
274286
Text.decodeUtf8 (Api.serialiseToRawBytesHex signedTx)
287+
288+
-- | Get the transaction id of a signed transaction object. The transaction
289+
-- id is the blake2b-256 hash of the transaction body, so it is not affected
290+
-- by signing and it matches the id of the unsigned transaction.
291+
getSignedTxIdImpl :: SignedTxObject -> String
292+
getSignedTxIdImpl (SignedTxObject era (Exp.SignedTx tx)) =
293+
obtainCommonConstraints era $
294+
Text.unpack $
295+
Api.serialiseToRawBytesHexText $
296+
TxBody.getTxIdShelley (Api.convert era) (tx ^. Ledger.bodyTxL)

cardano-wasm/src-wasi/Cardano/Wasi/Internal/Api/Tx.hs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ module Cardano.Wasi.Internal.Api.Tx
1313
, alsoSignWithPaymentKey
1414
, alsoSignWithStakeKey
1515
, toCbor
16+
, getUnsignedTxId
17+
, getSignedTxId
1618
)
1719
where
1820

@@ -64,6 +66,9 @@ foreign export ccall "signWithPaymentKey"
6466
foreign export ccall "signWithStakeKey"
6567
signWithStakeKey :: UnsignedTxObjectJSON -> CString -> IO SignedTxObjectJSON
6668

69+
foreign export ccall "getUnsignedTxId"
70+
getUnsignedTxId :: UnsignedTxObjectJSON -> IO CString
71+
6772
#endif
6873

6974
type UnsignedTxObjectJSON = CString
@@ -138,6 +143,11 @@ signWithStakeKey unsignedTxObject signingKeyBech32 =
138143
<*> stringToSigningKey signingKeyBech32
139144
)
140145

146+
getUnsignedTxId :: UnsignedTxObjectJSON -> IO CString
147+
getUnsignedTxId unsignedTxObject =
148+
newCString . getUnsignedTxIdImpl
149+
=<< fromCJSON False "UnsignedTx" unsignedTxObject
150+
141151
-- * SignedTxObject
142152

143153
#if defined(wasm32_HOST_ARCH)
@@ -151,6 +161,9 @@ foreign export ccall "alsoSignWithStakeKey"
151161
foreign export ccall "toCbor"
152162
toCbor :: SignedTxObjectJSON -> IO CString
153163

164+
foreign export ccall "getSignedTxId"
165+
getSignedTxId :: SignedTxObjectJSON -> IO CString
166+
154167
#endif
155168

156169
type SignedTxObjectJSON = CString
@@ -175,3 +188,8 @@ toCbor :: SignedTxObjectJSON -> IO CString
175188
toCbor signedTxObject =
176189
newCString . toCborImpl
177190
=<< fromCJSON False "SignedTx" signedTxObject
191+
192+
getSignedTxId :: SignedTxObjectJSON -> IO CString
193+
getSignedTxId signedTxObject =
194+
newCString . getSignedTxIdImpl
195+
=<< fromCJSON False "SignedTx" signedTxObject

cardano-wasm/src-wasm/Cardano/Wasm/Internal/JavaScript/Bridge.hs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,9 @@ foreign export javascript "signWithPaymentKey"
390390
foreign export javascript "signWithStakeKey"
391391
signWithStakeKey :: JSUnsignedTx -> JSSigningKey -> IO JSSignedTx
392392

393+
foreign export javascript "getUnsignedTxId"
394+
getUnsignedTxId :: JSUnsignedTx -> IO JSString
395+
393396
-- | Create a new unsigned transaction in the current era.
394397
newTx :: HasCallStack => IO JSUnsignedTx
395398
newTx = toJSVal Wasm.newTxImpl
@@ -528,6 +531,11 @@ signWithStakeKey jsUnsignedTx jsSigningKey =
528531
<*> fromJSVal jsSigningKey
529532
)
530533

534+
-- | Get the transaction id (the hash of the transaction body) of an unsigned transaction.
535+
getUnsignedTxId :: HasCallStack => JSUnsignedTx -> IO JSString
536+
getUnsignedTxId jsUnsignedTx =
537+
toJSVal . Wasm.getUnsignedTxIdImpl =<< fromJSVal jsUnsignedTx
538+
531539
-- * SignedTxObject
532540

533541
foreign export javascript "alsoSignWithPaymentKey"
@@ -539,6 +547,9 @@ foreign export javascript "alsoSignWithStakeKey"
539547
foreign export javascript "txToCbor"
540548
txToCbor :: JSSignedTx -> IO JSString
541549

550+
foreign export javascript "getSignedTxId"
551+
getSignedTxId :: JSSignedTx -> IO JSString
552+
542553
-- | Sign an unsigned transaction with a payment key.
543554
alsoSignWithPaymentKey :: HasCallStack => JSSignedTx -> JSSigningKey -> IO JSSignedTx
544555
alsoSignWithPaymentKey jsSignedTx jsSigningKey =
@@ -562,6 +573,11 @@ txToCbor :: HasCallStack => JSSignedTx -> IO JSString
562573
txToCbor jsSignedTx =
563574
toJSVal . Wasm.toCborImpl =<< fromJSVal jsSignedTx
564575

576+
-- | Get the transaction id (the hash of the transaction body) of a signed transaction.
577+
getSignedTxId :: HasCallStack => JSSignedTx -> IO JSString
578+
getSignedTxId jsSignedTx =
579+
toJSVal . Wasm.getSignedTxIdImpl =<< fromJSVal jsSignedTx
580+
565581
-- * GrpcObject
566582

567583
foreign export javascript "newGrpcConnection"

0 commit comments

Comments
 (0)