Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/src/signer/ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
4 changes: 2 additions & 2 deletions api/src/signer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down Expand Up @@ -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,
});

Expand Down
53 changes: 48 additions & 5 deletions types/src/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,36 @@ use crate::{
AccountId, Action, CryptoHash, Nonce, PublicKey, Signature, errors::DataConversionError,
};

/// Borsh-serialize an `Option<CryptoHash>` as a plain `CryptoHash`, preserving
/// the on-chain wire format. Panics 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<W: std::io::Write>(
val: &Option<CryptoHash>,
writer: &mut W,
) -> Result<(), std::io::Error> {
let hash = val.expect("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)
Comment on lines +19 to +31
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

borsh_ser_optional_hash currently calls ok_or_else directly on &Option<CryptoHash>, which works because Option<CryptoHash> is Copy today, but is a bit non-obvious and introduces an unnecessary copy. Using as_ref()/as_deref() (or a match) to get a borrowed hash before serializing would make the intent clearer and avoid relying on Copy semantics.

Copilot uses AI. Check for mistakes.
}

/// Borsh-deserialize a plain `CryptoHash` into `Some(CryptoHash)`.
fn borsh_de_optional_hash<R: std::io::Read>(
reader: &mut R,
) -> Result<Option<CryptoHash>, 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<CryptoHash>,
pub actions: Vec<Action>,
}

Expand All @@ -28,7 +51,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<CryptoHash>,
pub actions: Vec<Action>,
pub priority_fee: u64,
}
Expand Down Expand Up @@ -68,6 +95,13 @@ impl Transaction {
}
}

pub const fn block_hash(&self) -> Option<CryptoHash> {
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,
Expand Down Expand Up @@ -134,13 +168,18 @@ impl TryFrom<near_openapi_types::SignedTransactionView> 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)
Expand All @@ -153,15 +192,19 @@ impl TryFrom<near_openapi_types::SignedTransactionView> 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)
.collect::<Result<Vec<_>, _>>()?,
})
};

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);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signed.hash.set(tx_hash) returns a Result indicating whether the OnceLock was initialized. Ignoring it can mask unexpected state (e.g., if SignedTransaction::new ever starts pre-populating the cache) and could reintroduce incorrect hashes silently. Consider handling the Result explicitly (e.g., expect with a message or propagate/return an error) so failures are visible.

Suggested change
let _ = signed.hash.set(tx_hash);
#[allow(clippy::expect_used)]
signed
.hash
.set(tx_hash)
.expect("SignedTransaction hash OnceLock unexpectedly initialized");

Copilot uses AI. Check for mistakes.
Ok(signed)
}
}

Expand Down
Loading