diff --git a/api/src/signer/ledger.rs b/api/src/signer/ledger.rs index 1a905ee..39cdc85 100644 --- a/api/src/signer/ledger.rs +++ b/api/src/signer/ledger.rs @@ -47,7 +47,7 @@ impl SignerTrait for LedgerSigner { public_key, receiver_id: transaction.receiver_id, nonce, - block_hash, + block_hash: Some(block_hash), actions: transaction.actions, }); let unsigned_tx_bytes = borsh::to_vec(&unsigned_tx).map_err(LedgerError::from)?; diff --git a/api/src/signer/mod.rs b/api/src/signer/mod.rs index 43fa37c..3717769 100644 --- a/api/src/signer/mod.rs +++ b/api/src/signer/mod.rs @@ -316,7 +316,7 @@ pub trait SignerTrait { public_key, nonce, receiver_id: transaction.receiver_id, - block_hash, + block_hash: Some(block_hash), actions: transaction.actions, }); @@ -345,7 +345,7 @@ pub trait SignerTrait { public_key, nonce, receiver_id: transaction.receiver_id, - block_hash, + block_hash: Some(block_hash), actions: transaction.actions, }); diff --git a/types/src/transaction/mod.rs b/types/src/transaction/mod.rs index edaede3..5458cdf 100644 --- a/types/src/transaction/mod.rs +++ b/types/src/transaction/mod.rs @@ -12,13 +12,43 @@ use crate::{ AccountId, Action, CryptoHash, Nonce, PublicKey, Signature, errors::DataConversionError, }; +/// Borsh-serialize an `Option` as a plain `CryptoHash`, preserving +/// the on-chain wire format. Returns an error if the value is `None`, since +/// serialization is only valid for fully-constructed transactions (i.e. those +/// with a known block hash). +fn borsh_ser_optional_hash( + val: &Option, + writer: &mut W, +) -> Result<(), std::io::Error> { + let hash = val.ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "cannot borsh-serialize a Transaction whose block_hash is None \ + (this transaction was deserialized from an RPC response that \ + lacks block hash information)", + ) + })?; + BorshSerialize::serialize(&hash, writer) +} + +/// Borsh-deserialize a plain `CryptoHash` into `Some(CryptoHash)`. +fn borsh_de_optional_hash( + reader: &mut R, +) -> Result, std::io::Error> { + CryptoHash::deserialize_reader(reader).map(Some) +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct TransactionV0 { pub signer_id: AccountId, pub public_key: PublicKey, pub nonce: Nonce, pub receiver_id: AccountId, - pub block_hash: CryptoHash, + #[borsh( + serialize_with = "borsh_ser_optional_hash", + deserialize_with = "borsh_de_optional_hash" + )] + pub block_hash: Option, pub actions: Vec, } @@ -28,7 +58,11 @@ pub struct TransactionV1 { pub public_key: PublicKey, pub nonce: Nonce, pub receiver_id: AccountId, - pub block_hash: CryptoHash, + #[borsh( + serialize_with = "borsh_ser_optional_hash", + deserialize_with = "borsh_de_optional_hash" + )] + pub block_hash: Option, pub actions: Vec, pub priority_fee: u64, } @@ -68,6 +102,13 @@ impl Transaction { } } + pub const fn block_hash(&self) -> Option { + match self { + Self::V0(tx) => tx.block_hash, + Self::V1(tx) => tx.block_hash, + } + } + pub fn actions(&self) -> &[Action] { match self { Self::V0(tx) => &tx.actions, @@ -134,13 +175,18 @@ impl TryFrom for SignedTransaction { signature, } = value; + // The RPC response provides the transaction hash but not the block hash + // that was used when signing. We store the real tx hash and set block_hash + // to None since it is unavailable. + let tx_hash: CryptoHash = hash.into(); + let transaction = if priority_fee > 0 { Transaction::V1(TransactionV1 { signer_id, public_key: public_key.try_into()?, nonce, receiver_id, - block_hash: hash.into(), + block_hash: None, actions: actions .into_iter() .map(Action::try_from) @@ -153,7 +199,7 @@ impl TryFrom for SignedTransaction { public_key: public_key.try_into()?, nonce, receiver_id, - block_hash: hash.into(), + block_hash: None, actions: actions .into_iter() .map(Action::try_from) @@ -161,7 +207,11 @@ impl TryFrom for SignedTransaction { }) }; - Ok(Self::new(Signature::from_str(&signature)?, transaction)) + let signed = Self::new(Signature::from_str(&signature)?, transaction); + // Pre-populate with the correct hash from the RPC response, + // since we cannot recompute it without the block hash. + let _ = signed.hash.set(tx_hash); + Ok(signed) } }