Skip to main content

kaspa_consensus_client/serializable/
numeric.rs

1//!
2//! This module implements transaction-related primitives for JSON serialization
3//! where all large integer values (`u64`) are serialized to JSON using `serde` and
4//! can exceed the largest integer value representable by the JavaScript `number` type.
5//! (i.e. transactions serialized using this module can not be deserialized in JavaScript
6//! but may be deserialized in other JSON-capable environments that support large integers)
7//!
8
9use super::invalid_input_mass_variant;
10use crate::error::Error;
11use crate::imports::*;
12use crate::result::Result;
13use crate::{
14    CovenantBinding, Transaction, TransactionInput, TransactionInputInner, TransactionOutpoint, TransactionOutpointInner,
15    TransactionOutput, UtxoEntry, UtxoEntryId, UtxoEntryReference,
16};
17use ahash::AHashMap;
18use cctx::VerifiableTransaction;
19use kaspa_addresses::Address;
20use kaspa_consensus_core::subnets::SubnetworkId;
21use kaspa_hashes::Hash;
22use workflow_wasm::serde::{from_value, to_value};
23
24pub type SignedTransactionIndexType = u32;
25
26pub struct Options {
27    pub include_utxo: bool,
28}
29
30#[derive(Debug, Serialize, Deserialize, Clone)]
31#[serde(rename_all = "camelCase")]
32pub struct SerializableUtxoEntry {
33    pub address: Option<Address>,
34    pub amount: u64,
35    pub script_public_key: ScriptPublicKey,
36    pub block_daa_score: u64,
37    pub is_coinbase: bool,
38    pub covenant_id: Option<Hash>,
39}
40
41impl AsRef<SerializableUtxoEntry> for SerializableUtxoEntry {
42    fn as_ref(&self) -> &Self {
43        self
44    }
45}
46
47impl From<&UtxoEntryReference> for SerializableUtxoEntry {
48    fn from(utxo: &UtxoEntryReference) -> Self {
49        let utxo = utxo.utxo.as_ref();
50        Self {
51            address: utxo.address.clone(),
52            amount: utxo.amount,
53            script_public_key: utxo.script_public_key.clone(),
54            block_daa_score: utxo.block_daa_score,
55            is_coinbase: utxo.is_coinbase,
56            covenant_id: utxo.covenant_id,
57        }
58    }
59}
60
61impl From<&cctx::UtxoEntry> for SerializableUtxoEntry {
62    fn from(utxo: &cctx::UtxoEntry) -> Self {
63        Self {
64            address: None,
65            amount: utxo.amount,
66            script_public_key: utxo.script_public_key.clone(),
67            block_daa_score: utxo.block_daa_score,
68            is_coinbase: utxo.is_coinbase,
69            covenant_id: utxo.covenant_id,
70        }
71    }
72}
73
74impl TryFrom<&SerializableUtxoEntry> for cctx::UtxoEntry {
75    type Error = crate::error::Error;
76    fn try_from(utxo: &SerializableUtxoEntry) -> Result<Self> {
77        Ok(Self {
78            amount: utxo.amount,
79            script_public_key: utxo.script_public_key.clone(),
80            block_daa_score: utxo.block_daa_score,
81            is_coinbase: utxo.is_coinbase,
82            covenant_id: utxo.covenant_id,
83        })
84    }
85}
86
87#[derive(Debug, Serialize, Deserialize, Clone)]
88#[serde(rename_all = "camelCase")]
89pub struct SerializableTransactionInput {
90    pub transaction_id: TransactionId,
91    pub index: SignedTransactionIndexType,
92    pub sequence: u64,
93    pub sig_op_count: u8,
94    #[serde(default)]
95    pub compute_budget: u16,
96    #[serde(with = "hex::serde")]
97    // TODO - convert to Option<Vec<u8>> and use hex serialization over Option
98    pub signature_script: Vec<u8>,
99    pub utxo: SerializableUtxoEntry,
100}
101
102impl SerializableTransactionInput {
103    pub fn new(input: &cctx::TransactionInput, utxo: &cctx::UtxoEntry) -> Self {
104        let utxo = SerializableUtxoEntry::from(utxo);
105
106        Self {
107            transaction_id: input.previous_outpoint.transaction_id,
108            index: input.previous_outpoint.index,
109            // TODO - convert signature_script to Option<Vec<u8>>
110            // signature_script: (!input.signature_script.is_empty()).then_some(input.signature_script.clone()),
111            signature_script: input.signature_script.clone(),
112            sequence: input.sequence,
113            sig_op_count: input.compute_commit.sig_op_count().unwrap_or(0),
114            compute_budget: input.compute_commit.compute_budget().unwrap_or(0),
115            utxo: utxo.clone(),
116        }
117    }
118}
119
120impl TryFrom<&SerializableTransactionInput> for UtxoEntryReference {
121    type Error = Error;
122    fn try_from(input: &SerializableTransactionInput) -> Result<Self> {
123        let outpoint = TransactionOutpoint::new(input.transaction_id, input.index);
124
125        let utxo = UtxoEntry {
126            outpoint,
127            address: input.utxo.address.clone(),
128            amount: input.utxo.amount,
129            script_public_key: input.utxo.script_public_key.clone(),
130            block_daa_score: input.utxo.block_daa_score,
131            is_coinbase: input.utxo.is_coinbase,
132            covenant_id: input.utxo.covenant_id,
133        };
134
135        Ok(Self { utxo: Arc::new(utxo) })
136    }
137}
138
139struct SerializableInputWithVersion {
140    version: u16,
141    input: SerializableTransactionInput,
142}
143
144impl TryFrom<SerializableInputWithVersion> for cctx::TransactionInput {
145    type Error = Error;
146
147    fn try_from(value: SerializableInputWithVersion) -> Result<Self> {
148        let input = value.input;
149        Ok(Self {
150            previous_outpoint: cctx::TransactionOutpoint { transaction_id: input.transaction_id, index: input.index },
151            signature_script: input.signature_script,
152            sequence: input.sequence,
153            compute_commit: if cctx::ComputeCommit::version_expects_compute_budget_field(value.version) {
154                if input.sig_op_count != 0 {
155                    return Err(invalid_input_mass_variant("sig_op_count", value.version));
156                }
157                cctx::ComputeCommit::ComputeBudget(input.compute_budget.into())
158            } else {
159                if input.compute_budget != 0 {
160                    return Err(invalid_input_mass_variant("compute_budget", value.version));
161                }
162                cctx::ComputeCommit::SigopCount(input.sig_op_count.into())
163            },
164        })
165    }
166}
167
168impl TryFrom<&SerializableTransactionInput> for TransactionInput {
169    type Error = Error;
170    fn try_from(serializable_input: &SerializableTransactionInput) -> Result<Self> {
171        let utxo = UtxoEntryReference::try_from(serializable_input)?;
172
173        let previous_outpoint = TransactionOutpoint::new(serializable_input.transaction_id, serializable_input.index);
174        let inner = TransactionInputInner {
175            previous_outpoint,
176            // TODO - convert to Option<Vec<u8>> and use hex serialization over Option
177            signature_script: (!serializable_input.signature_script.is_empty()).then_some(serializable_input.signature_script.clone()),
178            sequence: serializable_input.sequence,
179            sig_op_count: serializable_input.sig_op_count,
180            compute_budget: serializable_input.compute_budget,
181            utxo: Some(utxo),
182        };
183
184        Ok(TransactionInput::new_with_inner(inner))
185    }
186}
187
188impl TryFrom<&TransactionInput> for SerializableTransactionInput {
189    type Error = Error;
190    fn try_from(input: &TransactionInput) -> Result<Self> {
191        let inner = input.inner();
192        let utxo = inner.utxo.as_ref().ok_or(Error::MissingUtxoEntry)?;
193        let utxo = SerializableUtxoEntry::from(utxo);
194        Ok(Self {
195            transaction_id: inner.previous_outpoint.transaction_id(),
196            index: inner.previous_outpoint.index(),
197            // TODO - convert to Option<Vec<u8>> and use hex serialization over Option
198            signature_script: inner.signature_script.clone().unwrap_or_default(),
199            sequence: inner.sequence,
200            sig_op_count: inner.sig_op_count,
201            compute_budget: inner.compute_budget,
202            utxo,
203        })
204    }
205}
206
207#[derive(Debug, Serialize, Deserialize, Clone)]
208#[serde(rename_all = "camelCase")]
209pub struct SerializableCovenantBinding {
210    authorizing_input: u16,
211    covenant_id: Hash,
212}
213
214impl From<cctx::CovenantBinding> for SerializableCovenantBinding {
215    fn from(covenant: cctx::CovenantBinding) -> Self {
216        Self { authorizing_input: covenant.authorizing_input, covenant_id: covenant.covenant_id }
217    }
218}
219
220impl From<CovenantBinding> for SerializableCovenantBinding {
221    fn from(covenant: CovenantBinding) -> Self {
222        Self { authorizing_input: covenant.get_authorizing_input(), covenant_id: covenant.get_covenant_id() }
223    }
224}
225
226impl TryFrom<SerializableCovenantBinding> for cctx::CovenantBinding {
227    type Error = Error;
228    fn try_from(covenant: SerializableCovenantBinding) -> Result<Self> {
229        Ok(Self { authorizing_input: covenant.authorizing_input, covenant_id: covenant.covenant_id })
230    }
231}
232
233#[derive(Debug, Serialize, Deserialize, Clone)]
234#[serde(rename_all = "camelCase")]
235pub struct SerializableTransactionOutput {
236    pub value: u64,
237    pub script_public_key: ScriptPublicKey,
238    pub covenant: Option<SerializableCovenantBinding>,
239}
240
241impl From<cctx::TransactionOutput> for SerializableTransactionOutput {
242    fn from(output: cctx::TransactionOutput) -> Self {
243        let covenant = output.covenant.map(SerializableCovenantBinding::from);
244        Self { value: output.value, script_public_key: output.script_public_key, covenant }
245    }
246}
247
248impl From<&cctx::TransactionOutput> for SerializableTransactionOutput {
249    fn from(output: &cctx::TransactionOutput) -> Self {
250        let covenant = output.covenant.map(SerializableCovenantBinding::from);
251        Self { value: output.value, script_public_key: output.script_public_key.clone(), covenant }
252    }
253}
254
255impl TryFrom<SerializableTransactionOutput> for cctx::TransactionOutput {
256    type Error = Error;
257    fn try_from(output: SerializableTransactionOutput) -> Result<Self> {
258        let covenant = match output.covenant {
259            Some(covenant) => Some(cctx::CovenantBinding::try_from(covenant)?),
260            None => None,
261        };
262
263        Ok(Self { value: output.value, script_public_key: output.script_public_key, covenant })
264    }
265}
266
267impl TryFrom<&SerializableTransactionOutput> for TransactionOutput {
268    type Error = Error;
269    fn try_from(output: &SerializableTransactionOutput) -> Result<Self> {
270        let covenant = output.covenant.as_ref().map(|covenant| CovenantBinding::new(covenant.authorizing_input, covenant.covenant_id));
271
272        Ok(TransactionOutput::new(output.value, output.script_public_key.clone(), covenant))
273    }
274}
275
276impl TryFrom<&TransactionOutput> for SerializableTransactionOutput {
277    type Error = Error;
278    fn try_from(output: &TransactionOutput) -> Result<Self> {
279        let inner = output.inner();
280        let covenant = inner.covenant.clone().map(SerializableCovenantBinding::from);
281        Ok(Self { value: inner.value, script_public_key: inner.script_public_key.clone(), covenant })
282    }
283}
284
285#[derive(Debug, Serialize, Deserialize, Clone)]
286#[serde(rename_all = "camelCase")]
287pub struct SerializableTransaction {
288    // pub version: u32,
289    pub id: TransactionId,
290    pub version: u16,
291    pub inputs: Vec<SerializableTransactionInput>,
292    pub outputs: Vec<SerializableTransactionOutput>,
293    pub lock_time: u64,
294    pub gas: u64,
295    #[serde(default, alias = "mass")]
296    pub storage_mass: u64,
297    pub subnetwork_id: SubnetworkId,
298    #[serde(with = "hex::serde")]
299    pub payload: Vec<u8>,
300}
301
302impl SerializableTransaction {
303    pub fn serialize_to_object(&self) -> Result<JsValue> {
304        Ok(to_value(self)?)
305    }
306
307    pub fn deserialize_from_object(object: JsValue) -> Result<Self> {
308        Ok(from_value(object)?)
309    }
310
311    pub fn serialize_to_json(&self) -> Result<String> {
312        Ok(serde_json::to_string(self)?)
313    }
314
315    pub fn deserialize_from_json(json: &str) -> Result<Self> {
316        Ok(serde_json::from_str(json)?)
317    }
318
319    pub fn from_signable_transaction(tx: &cctx::SignableTransaction) -> Result<Self> {
320        let verifiable_tx = tx.as_verifiable();
321        let mut inputs = vec![];
322        let transaction = tx.as_ref();
323        for index in 0..transaction.inputs.len() {
324            let (input, utxo) = verifiable_tx.populated_input(index);
325            let input = SerializableTransactionInput::new(input, utxo);
326            inputs.push(input);
327        }
328
329        let outputs = transaction.outputs.clone();
330
331        Ok(Self {
332            // version: 2,
333            inputs,
334            outputs: outputs.into_iter().map(Into::into).collect(),
335            version: transaction.version,
336            lock_time: transaction.lock_time,
337            subnetwork_id: transaction.subnetwork_id,
338            gas: transaction.gas,
339            storage_mass: transaction.storage_mass(),
340            payload: transaction.payload.clone(),
341            id: transaction.id(),
342        })
343    }
344
345    pub fn from_client_transaction(transaction: &Transaction) -> Result<Self> {
346        let inner = transaction.inner();
347
348        let inputs = inner.inputs.iter().map(TryFrom::try_from).collect::<Result<Vec<SerializableTransactionInput>>>()?;
349        let outputs = inner.outputs.iter().map(TryFrom::try_from).collect::<Result<Vec<SerializableTransactionOutput>>>()?;
350
351        Ok(Self {
352            inputs,
353            outputs,
354            version: inner.version,
355            lock_time: inner.lock_time,
356            subnetwork_id: inner.subnetwork_id,
357            gas: inner.gas,
358            payload: inner.payload.clone(),
359            storage_mass: inner.storage_mass,
360            id: inner.id,
361        })
362    }
363
364    pub fn from_cctx_transaction(transaction: &cctx::Transaction, utxos: &AHashMap<UtxoEntryId, UtxoEntryReference>) -> Result<Self> {
365        let inputs = transaction
366            .inputs
367            .iter()
368            .map(|input| {
369                let id = TransactionOutpointInner::new(input.previous_outpoint.transaction_id, input.previous_outpoint.index);
370                let utxo = utxos.get(&id).ok_or(Error::MissingUtxoEntry)?;
371                let utxo = cctx::UtxoEntry::from(utxo);
372                let input = SerializableTransactionInput::new(input, &utxo);
373                Ok(input)
374            })
375            .collect::<Result<Vec<SerializableTransactionInput>>>()?;
376
377        let outputs = transaction.outputs.iter().map(Into::into).collect::<Vec<SerializableTransactionOutput>>();
378
379        Ok(Self {
380            id: transaction.id(),
381            version: transaction.version,
382            inputs,
383            outputs,
384            lock_time: transaction.lock_time,
385            subnetwork_id: transaction.subnetwork_id,
386            gas: transaction.gas,
387            storage_mass: transaction.storage_mass(),
388            payload: transaction.payload.clone(),
389        })
390    }
391}
392
393impl TryFrom<SerializableTransaction> for cctx::SignableTransaction {
394    type Error = Error;
395    fn try_from(serializable: SerializableTransaction) -> Result<Self> {
396        let version = serializable.version;
397        let mut entries = vec![];
398        let mut inputs = vec![];
399        for input in serializable.inputs {
400            entries.push(input.utxo.as_ref().try_into()?);
401            inputs.push(SerializableInputWithVersion { version, input }.try_into()?);
402        }
403
404        let outputs = serializable.outputs.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>>>()?;
405
406        let tx = cctx::Transaction::new(
407            version,
408            inputs,
409            outputs,
410            serializable.lock_time,
411            serializable.subnetwork_id,
412            serializable.gas,
413            serializable.payload,
414        )
415        .with_storage_mass(serializable.storage_mass);
416
417        Ok(Self::with_entries(tx, entries))
418    }
419}
420
421impl TryFrom<SerializableTransaction> for Transaction {
422    type Error = Error;
423    fn try_from(tx: SerializableTransaction) -> Result<Self> {
424        let id = tx.id;
425        let inputs: Vec<TransactionInput> = tx.inputs.iter().map(TryInto::try_into).collect::<Result<Vec<_>>>()?;
426        let outputs: Vec<TransactionOutput> = tx.outputs.iter().map(TryInto::try_into).collect::<Result<Vec<_>>>()?;
427
428        Transaction::new(Some(id), tx.version, inputs, outputs, tx.lock_time, tx.subnetwork_id, tx.gas, tx.payload, tx.storage_mass)
429    }
430}