kaspa_consensus_client/
transaction.rs

1//!
2//! Declares the client-side [`Transaction`] type, which represents a Kaspa transaction.
3//!
4
5#![allow(non_snake_case)]
6
7use crate::imports::*;
8use crate::input::{TransactionInput, TransactionInputArrayAsArgT, TransactionInputArrayAsResultT};
9use crate::outpoint::TransactionOutpoint;
10use crate::output::{TransactionOutput, TransactionOutputArrayAsArgT, TransactionOutputArrayAsResultT};
11use crate::result::Result;
12use crate::serializable::{numeric, string, SerializableTransactionT};
13use crate::utxo::{UtxoEntryId, UtxoEntryReference};
14use ahash::AHashMap;
15use kaspa_consensus_core::network::NetworkType;
16use kaspa_consensus_core::network::NetworkTypeT;
17use kaspa_consensus_core::subnets::{self, SubnetworkId};
18use kaspa_consensus_core::tx::UtxoEntry;
19use kaspa_txscript::extract_script_pub_key_address;
20use kaspa_utils::hex::*;
21
22#[wasm_bindgen(typescript_custom_section)]
23const TS_TRANSACTION: &'static str = r#"
24/**
25 * Interface defining the structure of a transaction.
26 * 
27 * @category Consensus
28 */
29export interface ITransaction {
30    version: number;
31    inputs: ITransactionInput[];
32    outputs: ITransactionOutput[];
33    lockTime: bigint;
34    subnetworkId: HexString;
35    gas: bigint;
36    payload: HexString;
37    /** The mass of the transaction (the mass is undefined or zero unless explicitly set or obtained from the node) */
38    mass?: bigint;
39
40    /** Optional verbose data provided by RPC */
41    verboseData?: ITransactionVerboseData;
42}
43
44/**
45 * Optional transaction verbose data.
46 * 
47 * @category Node RPC
48 */
49export interface ITransactionVerboseData {
50    transactionId : HexString;
51    hash : HexString;
52    computeMass : bigint;
53    blockHash : HexString;
54    blockTime : bigint;
55}
56"#;
57
58#[wasm_bindgen]
59extern "C" {
60    /// WASM (TypeScript) type representing `ITransaction | Transaction`
61    /// @category Consensus
62    #[wasm_bindgen(typescript_type = "ITransaction | Transaction")]
63    pub type TransactionT;
64}
65
66/// Inner type used by [`Transaction`]
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct TransactionInner {
70    pub version: u16,
71    pub inputs: Vec<TransactionInput>,
72    pub outputs: Vec<TransactionOutput>,
73    pub lock_time: u64,
74    pub subnetwork_id: SubnetworkId,
75    pub gas: u64,
76    pub payload: Vec<u8>,
77    pub mass: u64,
78
79    // A field that is used to cache the transaction ID.
80    // Always use the corresponding self.id() instead of accessing this field directly
81    pub id: TransactionId,
82}
83
84/// Represents a Kaspa transaction.
85/// This is an artificial construct that includes additional
86/// transaction-related data such as additional data from UTXOs
87/// used by transaction inputs.
88/// @category Consensus
89#[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)]
90#[wasm_bindgen(inspectable)]
91pub struct Transaction {
92    inner: Arc<Mutex<TransactionInner>>,
93}
94
95impl Transaction {
96    pub fn new(
97        id: Option<TransactionId>,
98        version: u16,
99        inputs: Vec<TransactionInput>,
100        outputs: Vec<TransactionOutput>,
101        lock_time: u64,
102        subnetwork_id: SubnetworkId,
103        gas: u64,
104        payload: Vec<u8>,
105        mass: u64,
106    ) -> Result<Self> {
107        let finalize = id.is_none();
108        let tx = Self {
109            inner: Arc::new(Mutex::new(TransactionInner {
110                id: id.unwrap_or_default(),
111                version,
112                inputs,
113                outputs,
114                lock_time,
115                subnetwork_id,
116                gas,
117                payload,
118                mass,
119            })),
120        };
121        if finalize {
122            tx.finalize()?;
123        }
124        Ok(tx)
125    }
126
127    pub fn new_with_inner(inner: TransactionInner) -> Self {
128        Self { inner: Arc::new(Mutex::new(inner)) }
129    }
130
131    pub fn inner(&self) -> MutexGuard<'_, TransactionInner> {
132        self.inner.lock().unwrap()
133    }
134
135    pub fn id(&self) -> TransactionId {
136        self.inner().id
137    }
138}
139
140#[wasm_bindgen]
141impl Transaction {
142    /// Determines whether or not a transaction is a coinbase transaction. A coinbase
143    /// transaction is a special transaction created by miners that distributes fees and block subsidy
144    /// to the previous blocks' miners, and specifies the script_pub_key that will be used to pay the current
145    /// miner in future blocks.
146    pub fn is_coinbase(&self) -> bool {
147        self.inner().subnetwork_id == subnets::SUBNETWORK_ID_COINBASE
148    }
149
150    /// Recompute and finalize the tx id based on updated tx fields
151    pub fn finalize(&self) -> Result<TransactionId> {
152        let tx: cctx::Transaction = self.into();
153        self.inner().id = tx.id();
154        Ok(self.inner().id)
155    }
156
157    /// Returns the transaction ID
158    #[wasm_bindgen(getter, js_name = id)]
159    pub fn id_string(&self) -> String {
160        self.inner().id.to_string()
161    }
162
163    #[wasm_bindgen(constructor)]
164    pub fn constructor(js_value: &TransactionT) -> std::result::Result<Transaction, JsError> {
165        Ok(js_value.try_into_owned()?)
166    }
167
168    #[wasm_bindgen(getter = inputs)]
169    pub fn get_inputs_as_js_array(&self) -> TransactionInputArrayAsResultT {
170        let inputs = self.inner.lock().unwrap().inputs.clone().into_iter().map(JsValue::from);
171        Array::from_iter(inputs).unchecked_into()
172    }
173
174    /// Returns a list of unique addresses used by transaction inputs.
175    /// This method can be used to determine addresses used by transaction inputs
176    /// in order to select private keys needed for transaction signing.
177    pub fn addresses(&self, network_type: &NetworkTypeT) -> Result<kaspa_addresses::AddressArrayT> {
178        let mut list = std::collections::HashSet::new();
179        for input in &self.inner.lock().unwrap().inputs {
180            if let Some(utxo) = input.get_utxo() {
181                if let Some(address) = &utxo.utxo.address {
182                    list.insert(address.clone());
183                } else if let Ok(address) =
184                    extract_script_pub_key_address(&utxo.utxo.script_public_key, NetworkType::try_from(network_type)?.into())
185                {
186                    list.insert(address);
187                }
188            }
189        }
190        Ok(Array::from_iter(list.into_iter().map(JsValue::from)).unchecked_into())
191    }
192
193    #[wasm_bindgen(setter = inputs)]
194    pub fn set_inputs_from_js_array(&mut self, js_value: &TransactionInputArrayAsArgT) {
195        let inputs = Array::from(js_value)
196            .iter()
197            .map(|js_value| {
198                TransactionInput::try_owned_from(&js_value).unwrap_or_else(|err| panic!("invalid transaction input: {err}"))
199            })
200            .collect::<Vec<_>>();
201        self.inner().inputs = inputs;
202    }
203
204    #[wasm_bindgen(getter = outputs)]
205    pub fn get_outputs_as_js_array(&self) -> TransactionOutputArrayAsResultT {
206        let outputs = self.inner.lock().unwrap().outputs.clone().into_iter().map(JsValue::from);
207        Array::from_iter(outputs).unchecked_into()
208    }
209
210    #[wasm_bindgen(setter = outputs)]
211    pub fn set_outputs_from_js_array(&mut self, js_value: &TransactionOutputArrayAsArgT) {
212        let outputs = Array::from(js_value)
213            .iter()
214            .map(|js_value| TryCastFromJs::try_owned_from(&js_value).unwrap_or_else(|err| panic!("invalid transaction output: {err}")))
215            .collect::<Vec<_>>();
216        self.inner().outputs = outputs;
217    }
218
219    #[wasm_bindgen(getter, js_name = version)]
220    pub fn get_version(&self) -> u16 {
221        self.inner().version
222    }
223
224    #[wasm_bindgen(setter, js_name = version)]
225    pub fn set_version(&self, v: u16) {
226        self.inner().version = v;
227    }
228
229    #[wasm_bindgen(getter, js_name = lockTime)]
230    pub fn get_lock_time(&self) -> u64 {
231        self.inner().lock_time
232    }
233
234    #[wasm_bindgen(setter, js_name = lockTime)]
235    pub fn set_lock_time(&self, v: u64) {
236        self.inner().lock_time = v;
237    }
238
239    #[wasm_bindgen(getter, js_name = gas)]
240    pub fn get_gas(&self) -> u64 {
241        self.inner().gas
242    }
243
244    #[wasm_bindgen(setter, js_name = gas)]
245    pub fn set_gas(&self, v: u64) {
246        self.inner().gas = v;
247    }
248
249    #[wasm_bindgen(getter = subnetworkId)]
250    pub fn get_subnetwork_id_as_hex(&self) -> String {
251        self.inner().subnetwork_id.to_hex()
252    }
253
254    #[wasm_bindgen(setter = subnetworkId)]
255    pub fn set_subnetwork_id_from_js_value(&mut self, js_value: JsValue) {
256        let subnetwork_id = js_value.try_as_vec_u8().unwrap_or_else(|err| panic!("subnetwork id error: {err}"));
257        self.inner().subnetwork_id = subnetwork_id.as_slice().try_into().unwrap_or_else(|err| panic!("subnetwork id error: {err}"));
258    }
259
260    #[wasm_bindgen(getter = payload)]
261    pub fn get_payload_as_hex_string(&self) -> String {
262        self.inner().payload.to_hex()
263    }
264
265    #[wasm_bindgen(setter = payload)]
266    pub fn set_payload_from_js_value(&mut self, js_value: JsValue) {
267        self.inner.lock().unwrap().payload = js_value.try_as_vec_u8().unwrap_or_else(|err| panic!("payload value error: {err}"));
268    }
269
270    #[wasm_bindgen(getter = mass)]
271    pub fn get_mass(&self) -> u64 {
272        self.inner().mass
273    }
274
275    #[wasm_bindgen(setter = mass)]
276    pub fn set_mass(&self, v: u64) {
277        self.inner().mass = v;
278    }
279}
280
281impl TryCastFromJs for Transaction {
282    type Error = Error;
283    fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result<Cast<Self>, Self::Error>
284    where
285        R: AsRef<JsValue> + 'a,
286    {
287        Self::resolve_cast(value, || {
288            if let Some(object) = Object::try_from(value.as_ref()) {
289                if let Some(tx) = object.try_get_value("tx")? {
290                    Transaction::try_captured_cast_from(tx)
291                } else {
292                    let id = object.try_cast_into::<TransactionId>("id")?;
293                    let version = object.get_u16("version")?;
294                    let lock_time = object.get_u64("lockTime")?;
295                    let gas = object.get_u64("gas")?;
296                    let payload = object.get_vec_u8("payload")?;
297                    // mass field is optional
298                    let mass = object.get_u64("mass").unwrap_or_default();
299                    let subnetwork_id = object.get_vec_u8("subnetworkId")?;
300                    if subnetwork_id.len() != subnets::SUBNETWORK_ID_SIZE {
301                        return Err(Error::Custom("subnetworkId must be 20 bytes long".into()));
302                    }
303                    let subnetwork_id: SubnetworkId = subnetwork_id
304                        .as_slice()
305                        .try_into()
306                        .map_err(|err| Error::Custom(format!("`subnetworkId` property error: `{err}`")))?;
307                    let inputs = object
308                        .get_vec("inputs")?
309                        .iter()
310                        .map(TryCastFromJs::try_owned_from)
311                        .collect::<std::result::Result<Vec<TransactionInput>, Error>>()?;
312                    let outputs: Vec<TransactionOutput> = object
313                        .get_vec("outputs")?
314                        .iter()
315                        .map(TryCastFromJs::try_owned_from)
316                        .collect::<std::result::Result<Vec<TransactionOutput>, Error>>()?;
317                    Transaction::new(id, version, inputs, outputs, lock_time, subnetwork_id, gas, payload, mass).map(Into::into)
318                }
319            } else {
320                Err("Transaction must be an object".into())
321            }
322        })
323        // Transaction::try_from(value)
324    }
325}
326
327impl From<cctx::Transaction> for Transaction {
328    fn from(tx: cctx::Transaction) -> Self {
329        let id = tx.id();
330        let mass = tx.mass();
331        let inputs: Vec<TransactionInput> = tx.inputs.into_iter().map(|input| input.into()).collect::<Vec<TransactionInput>>();
332        let outputs: Vec<TransactionOutput> = tx.outputs.into_iter().map(|output| output.into()).collect::<Vec<TransactionOutput>>();
333        Self::new_with_inner(TransactionInner {
334            version: tx.version,
335            inputs,
336            outputs,
337            lock_time: tx.lock_time,
338            gas: tx.gas,
339            payload: tx.payload,
340            mass,
341            subnetwork_id: tx.subnetwork_id,
342            id,
343        })
344    }
345}
346
347impl From<&Transaction> for cctx::Transaction {
348    fn from(tx: &Transaction) -> Self {
349        let inner = tx.inner();
350        let inputs: Vec<cctx::TransactionInput> =
351            inner.inputs.clone().into_iter().map(|input| input.as_ref().into()).collect::<Vec<cctx::TransactionInput>>();
352        let outputs: Vec<cctx::TransactionOutput> =
353            inner.outputs.clone().into_iter().map(|output| output.as_ref().into()).collect::<Vec<cctx::TransactionOutput>>();
354        cctx::Transaction::new(
355            inner.version,
356            inputs,
357            outputs,
358            inner.lock_time,
359            inner.subnetwork_id.clone(),
360            inner.gas,
361            inner.payload.clone(),
362        )
363        .with_mass(inner.mass)
364    }
365}
366
367impl Transaction {
368    pub fn from_cctx_transaction(tx: &cctx::Transaction, utxos: &AHashMap<UtxoEntryId, UtxoEntryReference>) -> Self {
369        let inputs: Vec<TransactionInput> = tx
370            .inputs
371            .iter()
372            .map(|input| {
373                let previous_outpoint: TransactionOutpoint = input.previous_outpoint.into();
374                let utxo = utxos.get(previous_outpoint.id()).cloned();
375                TransactionInput::new(
376                    previous_outpoint,
377                    Some(input.signature_script.clone()),
378                    input.sequence,
379                    input.sig_op_count,
380                    utxo,
381                )
382            })
383            .collect::<Vec<TransactionInput>>();
384        let outputs: Vec<TransactionOutput> = tx.outputs.iter().map(|output| output.into()).collect::<Vec<TransactionOutput>>();
385
386        Self::new_with_inner(TransactionInner {
387            id: tx.id(),
388            version: tx.version,
389            inputs,
390            outputs,
391            lock_time: tx.lock_time,
392            gas: tx.gas,
393            payload: tx.payload.clone(),
394            mass: tx.mass(),
395            subnetwork_id: tx.subnetwork_id.clone(),
396        })
397    }
398
399    pub fn tx_and_utxos(&self) -> Result<(cctx::Transaction, Vec<UtxoEntry>)> {
400        let mut inputs = vec![];
401        let inner = self.inner();
402        let utxos: Vec<cctx::UtxoEntry> = inner
403            .inputs
404            .clone()
405            .into_iter()
406            .map(|input| {
407                inputs.push(input.as_ref().into());
408                Ok(input.get_utxo().ok_or(Error::MissingUtxoEntry)?.entry().as_ref().into())
409            })
410            .collect::<Result<Vec<_>>>()?;
411        let outputs: Vec<cctx::TransactionOutput> =
412            inner.outputs.clone().into_iter().map(|output| output.as_ref().into()).collect::<Vec<cctx::TransactionOutput>>();
413        let tx = cctx::Transaction::new(
414            inner.version,
415            inputs,
416            outputs,
417            inner.lock_time,
418            inner.subnetwork_id.clone(),
419            inner.gas,
420            inner.payload.clone(),
421        )
422        .with_mass(inner.mass);
423
424        Ok((tx, utxos))
425    }
426
427    pub fn utxo_entry_references(&self) -> Result<Vec<UtxoEntryReference>> {
428        let inner = self.inner();
429        let utxo_entry_references = inner
430            .inputs
431            .clone()
432            .into_iter()
433            .map(|input| input.get_utxo().ok_or(Error::MissingUtxoEntry))
434            .collect::<Result<Vec<UtxoEntryReference>>>()?;
435        Ok(utxo_entry_references)
436    }
437
438    pub fn outputs(&self) -> Vec<cctx::TransactionOutput> {
439        let inner = self.inner();
440        let outputs = inner.outputs.iter().map(|output| output.into()).collect::<Vec<cctx::TransactionOutput>>();
441        outputs
442    }
443
444    pub fn inputs(&self) -> Vec<cctx::TransactionInput> {
445        let inner = self.inner();
446        let inputs = inner.inputs.iter().map(Into::into).collect::<Vec<cctx::TransactionInput>>();
447        inputs
448    }
449
450    pub fn inputs_outputs(&self) -> (Vec<cctx::TransactionInput>, Vec<cctx::TransactionOutput>) {
451        let inner = self.inner();
452        let inputs = inner.inputs.iter().map(Into::into).collect::<Vec<cctx::TransactionInput>>();
453        let outputs = inner.outputs.iter().map(Into::into).collect::<Vec<cctx::TransactionOutput>>();
454        (inputs, outputs)
455    }
456
457    pub fn set_signature_script(&self, input_index: usize, signature_script: Vec<u8>) -> Result<()> {
458        if self.inner().inputs.len() <= input_index {
459            return Err(Error::Custom("Input index is invalid".to_string()));
460        }
461        self.inner().inputs[input_index].set_signature_script(signature_script);
462        Ok(())
463    }
464
465    pub fn payload(&self) -> Vec<u8> {
466        self.inner().payload.clone()
467    }
468
469    pub fn payload_len(&self) -> usize {
470        self.inner().payload.len()
471    }
472}
473
474#[wasm_bindgen]
475impl Transaction {
476    /// Serializes the transaction to a pure JavaScript Object.
477    /// The schema of the JavaScript object is defined by {@link ISerializableTransaction}.
478    /// @see {@link ISerializableTransaction}
479    #[wasm_bindgen(js_name = "serializeToObject")]
480    pub fn serialize_to_object(&self) -> Result<SerializableTransactionT> {
481        Ok(numeric::SerializableTransaction::from_client_transaction(self)?.serialize_to_object()?.into())
482    }
483
484    /// Serializes the transaction to a JSON string.
485    /// The schema of the JSON is defined by {@link ISerializableTransaction}.
486    #[wasm_bindgen(js_name = "serializeToJSON")]
487    pub fn serialize_to_json(&self) -> Result<String> {
488        numeric::SerializableTransaction::from_client_transaction(self)?.serialize_to_json()
489    }
490
491    /// Serializes the transaction to a "Safe" JSON schema where it converts all `bigint` values to `string` to avoid potential client-side precision loss.
492    #[wasm_bindgen(js_name = "serializeToSafeJSON")]
493    pub fn serialize_to_json_safe(&self) -> Result<String> {
494        string::SerializableTransaction::from_client_transaction(self)?.serialize_to_json()
495    }
496
497    /// Deserialize the {@link Transaction} Object from a pure JavaScript Object.
498    #[wasm_bindgen(js_name = "deserializeFromObject")]
499    pub fn deserialize_from_object(js_value: &JsValue) -> Result<Transaction> {
500        numeric::SerializableTransaction::deserialize_from_object(js_value.clone())?.try_into()
501    }
502
503    /// Deserialize the {@link Transaction} Object from a JSON string.
504    #[wasm_bindgen(js_name = "deserializeFromJSON")]
505    pub fn deserialize_from_json(json: &str) -> Result<Transaction> {
506        numeric::SerializableTransaction::deserialize_from_json(json)?.try_into()
507    }
508
509    /// Deserialize the {@link Transaction} Object from a "Safe" JSON schema where all `bigint` values are represented as `string`.
510    #[wasm_bindgen(js_name = "deserializeFromSafeJSON")]
511    pub fn deserialize_from_safe_json(json: &str) -> Result<Transaction> {
512        string::SerializableTransaction::deserialize_from_json(json)?.try_into()
513    }
514}