Skip to content

Commit 7a0d3be

Browse files
authored
Merge pull request #201 from BitGo/BTC-3123-unsigned-tx-blake2b-id
feat: return blake2b hash for unsigned DOT transactions
2 parents 122eb28 + d517035 commit 7a0d3be

2 files changed

Lines changed: 36 additions & 9 deletions

File tree

packages/wasm-dot/js/transaction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class DotTransaction {
7878
}
7979

8080
/**
81-
* Get the transaction ID (hash) if signed
81+
* Get the transaction ID (Blake2-256 hash of transaction bytes)
8282
*/
8383
get id(): string | undefined {
8484
return this._wasm.id ?? undefined;

packages/wasm-dot/src/transaction.rs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -253,19 +253,27 @@ impl Transaction {
253253
Ok(result)
254254
}
255255

256-
/// Get transaction ID (Blake2-256 hash of signed transaction)
256+
/// Get transaction ID (Blake2-256 hash of transaction bytes)
257+
///
258+
/// For signed transactions, hashes the fully serialized extrinsic.
259+
/// For unsigned transactions, hashes the raw bytes as a stable identifier.
257260
pub fn id(&self) -> Option<String> {
258261
use blake2::{digest::consts::U32, Blake2b, Digest};
259262

260-
if self.is_signed && self.signature.is_some() {
261-
let bytes = self.to_bytes().ok()?;
262-
let mut hasher = Blake2b::<U32>::new();
263-
hasher.update(&bytes);
264-
let hash = hasher.finalize();
265-
Some(format!("0x{}", hex::encode(hash)))
263+
let bytes = if self.is_signed && self.signature.is_some() {
264+
self.to_bytes().ok()?
266265
} else {
267-
None
266+
self.raw_bytes.clone()
267+
};
268+
269+
if bytes.is_empty() {
270+
return None;
268271
}
272+
273+
let mut hasher = Blake2b::<U32>::new();
274+
hasher.update(&bytes);
275+
let hash = hasher.finalize();
276+
Some(format!("0x{}", hex::encode(hash)))
269277
}
270278

271279
/// Get the signable payload for this transaction
@@ -734,6 +742,25 @@ fn decode_era_bytes(bytes: &[u8]) -> Result<(Era, usize), WasmDotError> {
734742
mod tests {
735743
use super::*;
736744

745+
#[test]
746+
fn test_unsigned_tx_id_returns_blake2b_hash() {
747+
// Minimal unsigned extrinsic: compact length + version 0x04 + era(immortal) + nonce(0) + tip(0) + call_data
748+
// length=6 (compact 0x18), version=0x04, era=0x00, nonce=0x00, tip=0x00, call=0xFF
749+
let raw = vec![0x18, 0x04, 0x00, 0x00, 0x00, 0xFF];
750+
let tx = Transaction::from_bytes(&raw, None, None).unwrap();
751+
752+
assert!(!tx.is_signed());
753+
let id = tx.id();
754+
assert!(id.is_some(), "unsigned tx should have an id");
755+
let id = id.unwrap();
756+
assert!(id.starts_with("0x"), "id should be 0x-prefixed hex");
757+
assert_eq!(id.len(), 66, "blake2b-256 hash = 0x + 64 hex chars");
758+
759+
// Same bytes should produce the same hash
760+
let tx2 = Transaction::from_bytes(&raw, None, None).unwrap();
761+
assert_eq!(tx.id(), tx2.id(), "same bytes should produce same id");
762+
}
763+
737764
#[test]
738765
fn test_era_encoding_roundtrip() {
739766
let immortal = Era::Immortal;

0 commit comments

Comments
 (0)