use crate::{ RpcOptionalHeader, RpcOptionalTransaction, prelude::{RpcHash, RpcScriptClass, RpcSubnetworkId}, }; use borsh::{BorshDeserialize, BorshSerialize}; use kaspa_addresses::Address; use kaspa_consensus_core::tx::{ CovenantBinding, ScriptPublicKey, ScriptVec, TransactionId, TransactionIndexType, TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry, }; use kaspa_hashes::Hash; use kaspa_utils::{hex::ToHex, serde_bytes_fixed_ref}; use serde::{Deserialize, Serialize}; use std::io::{Read, Write}; use workflow_serializer::prelude::*; /// Represents the ID of a Kaspa transaction pub type RpcTransactionId = TransactionId; pub type RpcScriptVec = ScriptVec; pub type RpcScriptPublicKey = ScriptPublicKey; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct RpcUtxoEntry { pub amount: u64, pub script_public_key: ScriptPublicKey, pub block_daa_score: u64, pub is_coinbase: bool, pub covenant_id: Option, } impl RpcUtxoEntry { pub fn new( amount: u64, script_public_key: ScriptPublicKey, block_daa_score: u64, is_coinbase: bool, covenant_id: Option, ) -> Self { Self { amount, script_public_key, block_daa_score, is_coinbase, covenant_id } } } impl From for RpcUtxoEntry { fn from(entry: UtxoEntry) -> Self { Self { amount: entry.amount, script_public_key: entry.script_public_key, block_daa_score: entry.block_daa_score, is_coinbase: entry.is_coinbase, covenant_id: entry.covenant_id, } } } impl From for UtxoEntry { fn from(entry: RpcUtxoEntry) -> Self { Self { amount: entry.amount, script_public_key: entry.script_public_key, block_daa_score: entry.block_daa_score, is_coinbase: entry.is_coinbase, covenant_id: entry.covenant_id, } } } impl Serializer for RpcUtxoEntry { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { store!(u8, &2, writer)?; store!(u64, &self.amount, writer)?; store!(ScriptPublicKey, &self.script_public_key, writer)?; store!(u64, &self.block_daa_score, writer)?; store!(bool, &self.is_coinbase, writer)?; store!(Option, &self.covenant_id, writer)?; Ok(()) } } impl Deserializer for RpcUtxoEntry { fn deserialize(reader: &mut R) -> std::io::Result { let version = load!(u8, reader)?; let amount = load!(u64, reader)?; let script_public_key = load!(ScriptPublicKey, reader)?; let block_daa_score = load!(u64, reader)?; let is_coinbase = load!(bool, reader)?; let covenant_id = if version > 1 { load!(Option, reader)? } else { None }; Ok(Self { amount, script_public_key, block_daa_score, is_coinbase, covenant_id }) } } /// Represents a Kaspa transaction outpoint #[derive(Eq, Hash, PartialEq, Debug, Copy, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionOutpoint { #[serde(with = "serde_bytes_fixed_ref")] pub transaction_id: TransactionId, pub index: TransactionIndexType, } impl From for RpcTransactionOutpoint { fn from(outpoint: TransactionOutpoint) -> Self { Self { transaction_id: outpoint.transaction_id, index: outpoint.index } } } impl From for TransactionOutpoint { fn from(outpoint: RpcTransactionOutpoint) -> Self { Self { transaction_id: outpoint.transaction_id, index: outpoint.index } } } impl From for RpcTransactionOutpoint { fn from(outpoint: kaspa_consensus_client::TransactionOutpoint) -> Self { TransactionOutpoint::from(outpoint).into() } } impl From for kaspa_consensus_client::TransactionOutpoint { fn from(outpoint: RpcTransactionOutpoint) -> Self { TransactionOutpoint::from(outpoint).into() } } impl Serializer for RpcTransactionOutpoint { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { store!(u8, &1, writer)?; store!(TransactionId, &self.transaction_id, writer)?; store!(TransactionIndexType, &self.index, writer)?; Ok(()) } } impl Deserializer for RpcTransactionOutpoint { fn deserialize(reader: &mut R) -> std::io::Result { let _version = load!(u8, reader)?; let transaction_id = load!(TransactionId, reader)?; let index = load!(TransactionIndexType, reader)?; Ok(Self { transaction_id, index }) } } /// Represents a Kaspa transaction input #[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionInput { pub previous_outpoint: RpcTransactionOutpoint, #[serde(with = "hex::serde")] pub signature_script: Vec, pub sequence: u64, pub sig_op_count: u8, #[serde(default)] pub compute_budget: u16, pub verbose_data: Option, } impl std::fmt::Debug for RpcTransactionInput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("RpcTransactionInput") .field("previous_outpoint", &self.previous_outpoint) .field("signature_script", &self.signature_script.to_hex()) .field("sequence", &self.sequence) .field("sig_op_count", &self.sig_op_count) .field("compute_budget", &self.compute_budget) .field("verbose_data", &self.verbose_data) .finish() } } impl From for RpcTransactionInput { fn from(input: TransactionInput) -> Self { Self { previous_outpoint: input.previous_outpoint.into(), signature_script: input.signature_script, sequence: input.sequence, sig_op_count: input.compute_commit.sig_op_count().unwrap_or(0), compute_budget: input.compute_commit.compute_budget().unwrap_or(0), verbose_data: None, } } } impl RpcTransactionInput { pub fn from_transaction_inputs(other: Vec) -> Vec { other.into_iter().map(Self::from).collect() } } impl Serializer for RpcTransactionInput { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { store!(u8, &2, writer)?; serialize!(RpcTransactionOutpoint, &self.previous_outpoint, writer)?; store!(Vec, &self.signature_script, writer)?; store!(u64, &self.sequence, writer)?; store!(u8, &self.sig_op_count, writer)?; serialize!(Option, &self.verbose_data, writer)?; store!(u16, &self.compute_budget, writer)?; Ok(()) } } impl Deserializer for RpcTransactionInput { fn deserialize(reader: &mut R) -> std::io::Result { let version = load!(u8, reader)?; let previous_outpoint = deserialize!(RpcTransactionOutpoint, reader)?; let signature_script = load!(Vec, reader)?; let sequence = load!(u64, reader)?; let sig_op_count = load!(u8, reader)?; let verbose_data = deserialize!(Option, reader)?; let compute_budget = if version > 1 { load!(u16, reader)? } else { 0 }; Ok(Self { previous_outpoint, signature_script, sequence, sig_op_count, compute_budget, verbose_data }) } } /// Represent Kaspa transaction input verbose data #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionInputVerboseData {} impl Serializer for RpcTransactionInputVerboseData { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { store!(u8, &1, writer)?; Ok(()) } } impl Deserializer for RpcTransactionInputVerboseData { fn deserialize(reader: &mut R) -> std::io::Result { let _version = load!(u8, reader)?; Ok(Self {}) } } /// Represents a Kaspad transaction output #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionOutput { pub value: u64, pub script_public_key: RpcScriptPublicKey, pub verbose_data: Option, pub covenant: Option, } impl RpcTransactionOutput { pub fn from_transaction_outputs(other: Vec) -> Vec { other.into_iter().map(Self::from).collect() } } impl From for RpcTransactionOutput { fn from(output: TransactionOutput) -> Self { Self { value: output.value, script_public_key: output.script_public_key, verbose_data: None, covenant: output.covenant.map(Into::into), } } } impl Serializer for RpcTransactionOutput { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { store!(u8, &2, writer)?; store!(u64, &self.value, writer)?; store!(RpcScriptPublicKey, &self.script_public_key, writer)?; serialize!(Option, &self.verbose_data, writer)?; serialize!(Option, &self.covenant, writer)?; Ok(()) } } impl Deserializer for RpcTransactionOutput { fn deserialize(reader: &mut R) -> std::io::Result { let version = load!(u8, reader)?; let value = load!(u64, reader)?; let script_public_key = load!(RpcScriptPublicKey, reader)?; let verbose_data = deserialize!(Option, reader)?; let covenant = if version > 1 { deserialize!(Option, reader)? } else { None }; Ok(Self { value, script_public_key, verbose_data, covenant }) } } #[repr(transparent)] #[derive(Clone, Debug, Serialize, Deserialize, Copy)] pub struct RpcCovenantBinding(pub CovenantBinding); impl RpcCovenantBinding { pub fn new(authorizing_input: u16, covenant_id: Hash) -> Self { Self(CovenantBinding { authorizing_input, covenant_id }) } } impl From for RpcCovenantBinding { fn from(value: CovenantBinding) -> Self { Self(value) } } impl From for CovenantBinding { fn from(value: RpcCovenantBinding) -> Self { value.0 } } impl From for RpcCovenantBinding { fn from(covenant: kaspa_consensus_client::CovenantBinding) -> Self { CovenantBinding::from(covenant).into() } } impl Serializer for RpcCovenantBinding { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { store!(u8, &1, writer)?; store!(u16, &self.0.authorizing_input, writer)?; store!(Hash, &self.0.covenant_id, writer)?; Ok(()) } } impl Deserializer for RpcCovenantBinding { fn deserialize(reader: &mut R) -> std::io::Result { let _version = load!(u8, reader)?; let authorizing_input = load!(u16, reader)?; let covenant_id = load!(Hash, reader)?; Ok(Self(CovenantBinding { authorizing_input, covenant_id })) } } /// Represent Kaspa transaction output verbose data #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionOutputVerboseData { pub script_public_key_type: RpcScriptClass, pub script_public_key_address: Address, } impl Serializer for RpcTransactionOutputVerboseData { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { store!(u8, &1, writer)?; store!(RpcScriptClass, &self.script_public_key_type, writer)?; store!(Address, &self.script_public_key_address, writer)?; Ok(()) } } impl Deserializer for RpcTransactionOutputVerboseData { fn deserialize(reader: &mut R) -> std::io::Result { let _version = load!(u8, reader)?; let script_public_key_type = load!(RpcScriptClass, reader)?; let script_public_key_address = load!(Address, reader)?; Ok(Self { script_public_key_type, script_public_key_address }) } } /// Represents a Kaspa transaction #[derive(Clone)] pub struct RpcTransaction { pub version: u16, pub inputs: Vec, pub outputs: Vec, pub lock_time: u64, pub subnetwork_id: RpcSubnetworkId, pub gas: u64, pub payload: Vec, pub storage_mass: u64, pub verbose_data: Option, } // This struct is used only for human-readable serialization of RpcTransaction, and is not intended to be used directly. // It exists to avoid breaking existing clients that rely on the "mass" field in JSON serialization of RpcTransaction, // while allowing us to rename the internal storage mass field to storage_mass for better clarity. // Once the "mass" field is removed from RpcTransaction, this struct and the related code can be removed as well. #[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct RpcTransactionHumanReadable { version: u16, inputs: Vec, outputs: Vec, lock_time: u64, subnetwork_id: RpcSubnetworkId, gas: u64, #[serde(with = "hex::serde")] payload: Vec, storage_mass: Option, mass: Option, // Deprecated field for storage mass to avoid breaking existing clients. Should be removed in the future. verbose_data: Option, } impl<'de> serde::Deserialize<'de> for RpcTransaction { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { if !deserializer.is_human_readable() { return Err(serde::de::Error::custom("RpcTransaction does not support non-human-readable deserialization")); } let value = RpcTransactionHumanReadable::deserialize(deserializer)?; let storage_mass = match (value.storage_mass, value.mass) { (Some(storage_mass), Some(mass)) if storage_mass != mass => { return Err(serde::de::Error::custom(format!( "storageMass and mass must match when both are provided: storageMass={storage_mass}, mass={mass}" ))); } (Some(storage_mass), _) => storage_mass, (None, None) => return Err(serde::de::Error::custom("Either storageMass or mass must be provided")), (None, Some(mass)) => mass, }; Ok(Self { version: value.version, inputs: value.inputs, outputs: value.outputs, lock_time: value.lock_time, subnetwork_id: value.subnetwork_id, gas: value.gas, payload: value.payload, storage_mass, verbose_data: value.verbose_data, }) } } impl Serialize for RpcTransaction { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { if !serializer.is_human_readable() { return Err(serde::ser::Error::custom("RpcTransaction does not support non-human-readable serialization")); } // We use this destructuring so any change in the fields of RpcTransaction will cause a compile error here, reminding us to update the serialization code of RpcTransactionHumanReadable accordingly. let Self { version, inputs, outputs, lock_time, subnetwork_id, gas, payload, storage_mass, verbose_data } = self; let hr = RpcTransactionHumanReadable { version: *version, inputs: inputs.clone(), outputs: outputs.clone(), lock_time: *lock_time, subnetwork_id: *subnetwork_id, gas: *gas, payload: payload.clone(), storage_mass: Some(*storage_mass), mass: Some(*storage_mass), verbose_data: verbose_data.clone(), }; hr.serialize(serializer) } } impl std::fmt::Debug for RpcTransaction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("RpcTransaction") .field("version", &self.version) .field("lock_time", &self.lock_time) .field("subnetwork_id", &self.subnetwork_id) .field("gas", &self.gas) .field("payload", &self.payload.to_hex()) .field("storage_mass", &self.storage_mass) .field("inputs", &self.inputs) // Inputs and outputs are placed purposely at the end for better debug visibility .field("outputs", &self.outputs) .field("verbose_data", &self.verbose_data) .finish() } } impl Serializer for RpcTransaction { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { store!(u16, &1, writer)?; store!(u16, &self.version, writer)?; serialize!(Vec, &self.inputs, writer)?; serialize!(Vec, &self.outputs, writer)?; store!(u64, &self.lock_time, writer)?; store!(RpcSubnetworkId, &self.subnetwork_id, writer)?; store!(u64, &self.gas, writer)?; store!(Vec, &self.payload, writer)?; store!(u64, &self.storage_mass, writer)?; serialize!(Option, &self.verbose_data, writer)?; Ok(()) } } impl Deserializer for RpcTransaction { fn deserialize(reader: &mut R) -> std::io::Result { let _struct_version = load!(u16, reader)?; let version = load!(u16, reader)?; let inputs = deserialize!(Vec, reader)?; let outputs = deserialize!(Vec, reader)?; let lock_time = load!(u64, reader)?; let subnetwork_id = load!(RpcSubnetworkId, reader)?; let gas = load!(u64, reader)?; let payload = load!(Vec, reader)?; let mass = load!(u64, reader)?; let verbose_data = deserialize!(Option, reader)?; Ok(Self { version, inputs, outputs, lock_time, subnetwork_id, gas, payload, storage_mass: mass, verbose_data }) } } #[cfg(test)] mod rpc_transaction_json_tests { use super::*; use kaspa_consensus_core::subnets::SubnetworkId; use serde_json::json; fn transaction() -> RpcTransaction { RpcTransaction { version: 1, inputs: vec![], outputs: vec![], lock_time: 0, subnetwork_id: SubnetworkId::from_byte(0), gas: 0, payload: vec![], storage_mass: 123, verbose_data: None, } } #[test] fn rpc_transaction_json_serializes_mass_and_storage_mass() { let json = serde_json::to_value(transaction()).unwrap(); assert_eq!(json["mass"], json!(123)); assert_eq!(json["storageMass"], json!(123)); } #[test] fn rpc_transaction_json_deserializes_mass_or_storage_mass_to_storage_mass() { let storage_mass_json = r#"{ "version": 1, "inputs": [], "outputs": [], "lockTime": 0, "subnetworkId": "0000000000000000000000000000000000000000", "gas": 0, "payload": "", "storageMass": 123, "verboseData": null }"#; let storage_mass_tx: RpcTransaction = serde_json::from_str(storage_mass_json).unwrap(); assert_eq!(storage_mass_tx.storage_mass, 123); let mass_json = r#"{ "version": 1, "inputs": [], "outputs": [], "lockTime": 0, "subnetworkId": "0000000000000000000000000000000000000000", "gas": 0, "payload": "", "mass": 123, "verboseData": null }"#; let mass_tx: RpcTransaction = serde_json::from_str(mass_json).unwrap(); assert_eq!(mass_tx.storage_mass, 123); } #[test] fn rpc_transaction_json_deserializes_matching_mass_and_storage_mass_to_storage_mass() { let json = r#"{ "version": 1, "inputs": [], "outputs": [], "lockTime": 0, "subnetworkId": "0000000000000000000000000000000000000000", "gas": 0, "payload": "", "storageMass": 123, "mass": 123, "verboseData": null }"#; let tx: RpcTransaction = serde_json::from_str(json).unwrap(); assert_eq!(tx.storage_mass, 123); } #[test] fn rpc_transaction_json_rejects_conflicting_positive_mass_and_storage_mass() { let json = r#"{ "version": 1, "inputs": [], "outputs": [], "lockTime": 0, "subnetworkId": "0000000000000000000000000000000000000000", "gas": 0, "payload": "", "storageMass": 123, "mass": 456, "verboseData": null }"#; let err = serde_json::from_str::(json).unwrap_err(); assert!(err.to_string().contains("storageMass and mass must match when both are provided"), "got: {err}"); } } /// Represent Kaspa transaction verbose data #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionVerboseData { pub transaction_id: RpcTransactionId, pub hash: RpcHash, pub compute_mass: u64, pub block_hash: RpcHash, pub block_time: u64, } impl Serializer for RpcTransactionVerboseData { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { store!(u8, &1, writer)?; store!(RpcTransactionId, &self.transaction_id, writer)?; store!(RpcHash, &self.hash, writer)?; store!(u64, &self.compute_mass, writer)?; store!(RpcHash, &self.block_hash, writer)?; store!(u64, &self.block_time, writer)?; Ok(()) } } impl Deserializer for RpcTransactionVerboseData { fn deserialize(reader: &mut R) -> std::io::Result { let _version = load!(u8, reader)?; let transaction_id = load!(RpcTransactionId, reader)?; let hash = load!(RpcHash, reader)?; let compute_mass = load!(u64, reader)?; let block_hash = load!(RpcHash, reader)?; let block_time = load!(u64, reader)?; Ok(Self { transaction_id, hash, compute_mass, block_hash, block_time }) } } /// Represents accepted transaction ids #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct RpcAcceptedTransactionIds { pub accepting_block_hash: RpcHash, pub accepted_transaction_ids: Vec, } /// Represents accepted transaction ids #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcChainBlockAcceptedTransactions { pub chain_block_header: RpcOptionalHeader, pub accepted_transactions: Vec, } impl Serializer for RpcChainBlockAcceptedTransactions { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { store!(u16, &1, writer)?; serialize!(RpcOptionalHeader, &self.chain_block_header, writer)?; serialize!(Vec, &self.accepted_transactions, writer)?; Ok(()) } } impl Deserializer for RpcChainBlockAcceptedTransactions { fn deserialize(reader: &mut R) -> std::io::Result { let _struct_version = load!(u16, reader)?; let chain_block_header = deserialize!(RpcOptionalHeader, reader)?; let accepted_transactions = deserialize!(Vec, reader)?; Ok(Self { chain_block_header, accepted_transactions }) } }