Skip to main content

kaspa_consensus_client/serializable/
string.rs

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