1#![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_bindgen(typescript_type = "ITransaction | Transaction")]
63 pub type TransactionT;
64}
65
66#[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 pub id: TransactionId,
82}
83
84#[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 pub fn is_coinbase(&self) -> bool {
147 self.inner().subnetwork_id == subnets::SUBNETWORK_ID_COINBASE
148 }
149
150 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 #[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 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 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 }
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 #[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 #[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 #[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 #[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 #[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 #[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}