#![allow(non_snake_case)]
use crate::covenant::GenesisCovenantGroupArrayT;
use crate::imports::*;
use crate::input::{TransactionInput, TransactionInputArrayAsArgT, TransactionInputArrayAsResultT};
use crate::outpoint::TransactionOutpoint;
use crate::output::{TransactionOutput, TransactionOutputArrayAsArgT, TransactionOutputArrayAsResultT};
use crate::result::Result;
use crate::serializable::{SerializableTransactionT, numeric, string};
use crate::utxo::{UtxoEntryId, UtxoEntryReference};
use ahash::AHashMap;
use kaspa_consensus_core::network::NetworkType;
use kaspa_consensus_core::network::NetworkTypeT;
use kaspa_consensus_core::subnets::{self, SubnetworkId};
use kaspa_consensus_core::tx::{GenesisCovenantGroup, UtxoEntry};
use kaspa_txscript::extract_script_pub_key_address;
use kaspa_utils::hex::*;
#[wasm_bindgen(typescript_custom_section)]
const TS_TRANSACTION: &'static str = r#"
/**
* Interface defining the structure of a transaction.
*
* @category Consensus
*/
export interface ITransaction {
version: number;
inputs: ITransactionInput[];
outputs: ITransactionOutput[];
lockTime: bigint;
subnetworkId: HexString;
gas: bigint;
payload: HexString;
/**
* @deprecated since version 1.3.0, use `storageMass`
*/
mass?: bigint;
/** The mass of the transaction (the mass is undefined or zero unless explicitly set or obtained from the node) */
storageMass?: bigint;
/** Optional verbose data provided by RPC */
verboseData?: ITransactionVerboseData;
}
/**
* Optional transaction verbose data.
*
* @category Node RPC
*/
export interface ITransactionVerboseData {
transactionId : HexString;
hash : HexString;
computeMass : bigint;
blockHash : HexString;
blockTime : bigint;
}
"#;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "ITransaction | Transaction")]
pub type TransactionT;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionInner {
pub version: u16,
pub inputs: Vec<TransactionInput>,
pub outputs: Vec<TransactionOutput>,
pub lock_time: u64,
pub subnetwork_id: SubnetworkId,
pub gas: u64,
pub payload: Vec<u8>,
pub storage_mass: u64,
pub id: TransactionId,
}
#[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)]
#[wasm_bindgen(inspectable)]
pub struct Transaction {
inner: Arc<Mutex<TransactionInner>>,
}
impl Transaction {
pub fn new(
id: Option<TransactionId>,
version: u16,
inputs: Vec<TransactionInput>,
outputs: Vec<TransactionOutput>,
lock_time: u64,
subnetwork_id: SubnetworkId,
gas: u64,
payload: Vec<u8>,
storage_mass: u64,
) -> Result<Self> {
let finalize = id.is_none();
let tx = Self {
inner: Arc::new(Mutex::new(TransactionInner {
id: id.unwrap_or_default(),
version,
inputs,
outputs,
lock_time,
subnetwork_id,
gas,
payload,
storage_mass,
})),
};
if finalize {
tx.finalize()?;
}
Ok(tx)
}
pub fn new_with_inner(inner: TransactionInner) -> Self {
Self { inner: Arc::new(Mutex::new(inner)) }
}
pub fn inner(&self) -> MutexGuard<'_, TransactionInner> {
self.inner.lock().unwrap()
}
pub fn id(&self) -> TransactionId {
self.inner().id
}
}
#[wasm_bindgen]
impl Transaction {
pub fn is_coinbase(&self) -> bool {
self.inner().subnetwork_id == subnets::SUBNETWORK_ID_COINBASE
}
pub fn finalize(&self) -> Result<TransactionId> {
let tx: cctx::Transaction = self.into();
self.inner().id = tx.id();
Ok(self.inner().id)
}
#[wasm_bindgen(getter, js_name = id)]
pub fn id_string(&self) -> String {
self.inner().id.to_string()
}
#[wasm_bindgen(constructor)]
pub fn constructor(js_value: &TransactionT) -> std::result::Result<Transaction, JsError> {
Ok(js_value.try_into_owned()?)
}
#[wasm_bindgen(getter = inputs)]
pub fn get_inputs_as_js_array(&self) -> TransactionInputArrayAsResultT {
let inputs = self.inner.lock().unwrap().inputs.clone().into_iter().map(JsValue::from);
Array::from_iter(inputs).unchecked_into()
}
pub fn addresses(&self, network_type: &NetworkTypeT) -> Result<kaspa_addresses::AddressArrayT> {
let mut list = std::collections::HashSet::new();
for input in &self.inner.lock().unwrap().inputs {
if let Some(utxo) = input.get_utxo() {
if let Some(address) = &utxo.utxo.address {
list.insert(address.clone());
} else if let Ok(address) =
extract_script_pub_key_address(&utxo.utxo.script_public_key, NetworkType::try_from(network_type)?.into())
{
list.insert(address);
}
}
}
Ok(Array::from_iter(list.into_iter().map(JsValue::from)).unchecked_into())
}
#[wasm_bindgen(setter = inputs)]
pub fn set_inputs_from_js_array(&mut self, js_value: &TransactionInputArrayAsArgT) {
let inputs = Array::from(js_value)
.iter()
.map(|js_value| {
TransactionInput::try_owned_from(&js_value).unwrap_or_else(|err| panic!("invalid transaction input: {err}"))
})
.collect::<Vec<_>>();
self.inner().inputs = inputs;
}
#[wasm_bindgen(getter = outputs)]
pub fn get_outputs_as_js_array(&self) -> TransactionOutputArrayAsResultT {
let outputs = self.inner.lock().unwrap().outputs.clone().into_iter().map(JsValue::from);
Array::from_iter(outputs).unchecked_into()
}
#[wasm_bindgen(setter = outputs)]
pub fn set_outputs_from_js_array(&mut self, js_value: &TransactionOutputArrayAsArgT) {
let outputs = Array::from(js_value)
.iter()
.map(|js_value| TryCastFromJs::try_owned_from(&js_value).unwrap_or_else(|err| panic!("invalid transaction output: {err}")))
.collect::<Vec<_>>();
self.inner().outputs = outputs;
}
#[wasm_bindgen(getter, js_name = version)]
pub fn get_version(&self) -> u16 {
self.inner().version
}
#[wasm_bindgen(setter, js_name = version)]
pub fn set_version(&self, v: u16) {
self.inner().version = v;
}
#[wasm_bindgen(getter, js_name = lockTime)]
pub fn get_lock_time(&self) -> u64 {
self.inner().lock_time
}
#[wasm_bindgen(setter, js_name = lockTime)]
pub fn set_lock_time(&self, v: u64) {
self.inner().lock_time = v;
}
#[wasm_bindgen(getter, js_name = gas)]
pub fn get_gas(&self) -> u64 {
self.inner().gas
}
#[wasm_bindgen(setter, js_name = gas)]
pub fn set_gas(&self, v: u64) {
self.inner().gas = v;
}
#[wasm_bindgen(getter = subnetworkId)]
pub fn get_subnetwork_id_as_hex(&self) -> String {
self.inner().subnetwork_id.to_hex()
}
#[wasm_bindgen(setter = subnetworkId)]
pub fn set_subnetwork_id_from_js_value(&mut self, js_value: JsValue) {
let subnetwork_id = js_value.try_as_vec_u8().unwrap_or_else(|err| panic!("subnetwork id error: {err}"));
self.inner().subnetwork_id = subnetwork_id.as_slice().try_into().unwrap_or_else(|err| panic!("subnetwork id error: {err}"));
}
#[wasm_bindgen(getter = payload)]
pub fn get_payload_as_hex_string(&self) -> String {
self.inner().payload.to_hex()
}
#[wasm_bindgen(setter = payload)]
pub fn set_payload_from_js_value(&mut self, js_value: JsValue) {
self.inner.lock().unwrap().payload = js_value.try_as_vec_u8().unwrap_or_else(|err| panic!("payload value error: {err}"));
}
#[wasm_bindgen(getter = mass)]
pub fn get_mass(&self) -> u64 {
self.inner().storage_mass
}
#[wasm_bindgen(setter = mass)]
pub fn set_mass(&self, v: u64) {
self.inner().storage_mass = v;
}
#[wasm_bindgen(getter = storageMass)]
pub fn get_storage_mass(&self) -> u64 {
self.inner().storage_mass
}
#[wasm_bindgen(setter = storageMass)]
pub fn set_storage_mass(&self, v: u64) {
self.inner().storage_mass = v;
}
#[wasm_bindgen(js_name = populateGenesisCovenants)]
pub fn js_populate_genesis_covenants(&self, groups: &GenesisCovenantGroupArrayT) -> Result<()> {
let groups: Vec<GenesisCovenantGroup> = groups.try_into()?;
self.populate_genesis_covenants(&groups)?;
Ok(())
}
}
impl TryCastFromJs for Transaction {
type Error = Error;
fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result<Cast<'a, Self>, Self::Error>
where
R: AsRef<JsValue> + 'a,
{
Self::resolve_cast(value, || {
if let Some(object) = Object::try_from(value.as_ref()) {
if let Some(tx) = object.try_get_value("tx")? {
Transaction::try_captured_cast_from(tx)
} else {
let id = object.try_cast_into::<TransactionId>("id")?;
let version = object.get_u16("version")?;
let lock_time = object.get_u64("lockTime")?;
let gas = object.get_u64("gas")?;
let payload = object.get_vec_u8("payload")?;
let mass = object.get_u64("mass").unwrap_or_default();
let storage_mass = object.get_u64("storageMass").unwrap_or_default().max(mass);
let subnetwork_id = object.get_vec_u8("subnetworkId")?;
if subnetwork_id.len() != subnets::SUBNETWORK_ID_SIZE {
return Err(Error::Custom("subnetworkId must be 20 bytes long".into()));
}
let subnetwork_id: SubnetworkId = subnetwork_id
.as_slice()
.try_into()
.map_err(|err| Error::Custom(format!("`subnetworkId` property error: `{err}`")))?;
let inputs = object
.get_vec("inputs")?
.iter()
.map(TryCastFromJs::try_owned_from)
.collect::<std::result::Result<Vec<TransactionInput>, Error>>()?;
let outputs: Vec<TransactionOutput> = object
.get_vec("outputs")?
.iter()
.map(TryCastFromJs::try_owned_from)
.collect::<std::result::Result<Vec<TransactionOutput>, Error>>()?;
Transaction::new(id, version, inputs, outputs, lock_time, subnetwork_id, gas, payload, storage_mass)
.map(Into::into)
}
} else {
Err("Transaction must be an object".into())
}
})
}
}
impl From<cctx::Transaction> for Transaction {
fn from(tx: cctx::Transaction) -> Self {
let id = tx.id();
let storage_mass = tx.storage_mass();
let inputs: Vec<TransactionInput> = tx.inputs.into_iter().map(|input| input.into()).collect::<Vec<TransactionInput>>();
let outputs: Vec<TransactionOutput> = tx.outputs.into_iter().map(|output| output.into()).collect::<Vec<TransactionOutput>>();
Self::new_with_inner(TransactionInner {
version: tx.version,
inputs,
outputs,
lock_time: tx.lock_time,
gas: tx.gas,
payload: tx.payload,
storage_mass,
subnetwork_id: tx.subnetwork_id,
id,
})
}
}
impl From<&Transaction> for cctx::Transaction {
fn from(tx: &Transaction) -> Self {
let inner = tx.inner();
let inputs: Vec<cctx::TransactionInput> = inner
.inputs
.clone()
.into_iter()
.map(|input| input.as_ref().with_version(inner.version).into())
.collect::<Vec<cctx::TransactionInput>>();
let outputs: Vec<cctx::TransactionOutput> =
inner.outputs.clone().into_iter().map(|output| output.as_ref().into()).collect::<Vec<cctx::TransactionOutput>>();
cctx::Transaction::new(inner.version, inputs, outputs, inner.lock_time, inner.subnetwork_id, inner.gas, inner.payload.clone())
.with_storage_mass(inner.storage_mass)
}
}
impl Transaction {
pub fn from_cctx_transaction(tx: &cctx::Transaction, utxos: &AHashMap<UtxoEntryId, UtxoEntryReference>) -> Self {
let inputs: Vec<TransactionInput> = tx
.inputs
.iter()
.map(|input| {
let previous_outpoint: TransactionOutpoint = input.previous_outpoint.into();
let utxo = utxos.get(previous_outpoint.id()).cloned();
TransactionInput::new(
previous_outpoint,
Some(input.signature_script.clone()),
input.sequence,
input.compute_commit.sig_op_count().unwrap_or(0),
input.compute_commit.compute_budget().unwrap_or(0),
utxo,
)
})
.collect::<Vec<TransactionInput>>();
let outputs: Vec<TransactionOutput> = tx.outputs.iter().map(|output| output.into()).collect::<Vec<TransactionOutput>>();
Self::new_with_inner(TransactionInner {
id: tx.id(),
version: tx.version,
inputs,
outputs,
lock_time: tx.lock_time,
gas: tx.gas,
payload: tx.payload.clone(),
storage_mass: tx.storage_mass(),
subnetwork_id: tx.subnetwork_id,
})
}
pub fn tx_and_utxos(&self) -> Result<(cctx::Transaction, Vec<UtxoEntry>)> {
let mut inputs = vec![];
let inner = self.inner();
let utxos: Vec<cctx::UtxoEntry> = inner
.inputs
.clone()
.into_iter()
.map(|input| {
inputs.push(input.as_ref().with_version(inner.version).into());
Ok(input.get_utxo().ok_or(Error::MissingUtxoEntry)?.entry().as_ref().into())
})
.collect::<Result<Vec<_>>>()?;
let outputs: Vec<cctx::TransactionOutput> =
inner.outputs.clone().into_iter().map(|output| output.as_ref().into()).collect::<Vec<cctx::TransactionOutput>>();
let tx = cctx::Transaction::new(
inner.version,
inputs,
outputs,
inner.lock_time,
inner.subnetwork_id,
inner.gas,
inner.payload.clone(),
)
.with_storage_mass(inner.storage_mass);
Ok((tx, utxos))
}
pub fn utxo_entry_references(&self) -> Result<Vec<UtxoEntryReference>> {
let inner = self.inner();
let utxo_entry_references = inner
.inputs
.clone()
.into_iter()
.map(|input| input.get_utxo().ok_or(Error::MissingUtxoEntry))
.collect::<Result<Vec<UtxoEntryReference>>>()?;
Ok(utxo_entry_references)
}
pub fn outputs(&self) -> Vec<cctx::TransactionOutput> {
let inner = self.inner();
inner.outputs.iter().map(|output| output.into()).collect::<Vec<cctx::TransactionOutput>>()
}
pub fn inputs(&self) -> Vec<cctx::TransactionInput> {
let inner = self.inner();
inner.inputs.iter().map(|input| input.with_version(inner.version).into()).collect::<Vec<cctx::TransactionInput>>()
}
pub fn inputs_outputs(&self) -> (Vec<cctx::TransactionInput>, Vec<cctx::TransactionOutput>) {
let inner = self.inner();
let inputs =
inner.inputs.iter().map(|input| input.with_version(inner.version).into()).collect::<Vec<cctx::TransactionInput>>();
let outputs = inner.outputs.iter().map(Into::into).collect::<Vec<cctx::TransactionOutput>>();
(inputs, outputs)
}
pub fn set_signature_script(&self, input_index: usize, signature_script: Vec<u8>) -> Result<()> {
if self.inner().inputs.len() <= input_index {
return Err(Error::Custom("Input index is invalid".to_string()));
}
self.inner().inputs[input_index].set_signature_script(signature_script);
Ok(())
}
pub fn payload(&self) -> Vec<u8> {
self.inner().payload.clone()
}
pub fn payload_len(&self) -> usize {
self.inner().payload.len()
}
pub fn populate_genesis_covenants(&self, groups: &[GenesisCovenantGroup]) -> Result<()> {
let mut tx: cctx::Transaction = self.into();
tx.populate_genesis_covenants(groups)?;
self.inner().outputs = tx.outputs.iter().map(TransactionOutput::from).collect::<Vec<TransactionOutput>>();
Ok(())
}
}
#[wasm_bindgen]
impl Transaction {
#[wasm_bindgen(js_name = "serializeToObject")]
pub fn serialize_to_object(&self) -> Result<SerializableTransactionT> {
Ok(numeric::SerializableTransaction::from_client_transaction(self)?.serialize_to_object()?.into())
}
#[wasm_bindgen(js_name = "serializeToJSON")]
pub fn serialize_to_json(&self) -> Result<String> {
numeric::SerializableTransaction::from_client_transaction(self)?.serialize_to_json()
}
#[wasm_bindgen(js_name = "serializeToSafeJSON")]
pub fn serialize_to_json_safe(&self) -> Result<String> {
string::SerializableTransaction::from_client_transaction(self)?.serialize_to_json()
}
#[wasm_bindgen(js_name = "deserializeFromObject")]
pub fn deserialize_from_object(js_value: &JsValue) -> Result<Transaction> {
numeric::SerializableTransaction::deserialize_from_object(js_value.clone())?.try_into()
}
#[wasm_bindgen(js_name = "deserializeFromJSON")]
pub fn deserialize_from_json(json: &str) -> Result<Transaction> {
numeric::SerializableTransaction::deserialize_from_json(json)?.try_into()
}
#[wasm_bindgen(js_name = "deserializeFromSafeJSON")]
pub fn deserialize_from_safe_json(json: &str) -> Result<Transaction> {
string::SerializableTransaction::deserialize_from_json(json)?.try_into()
}
}
#[cfg(all(test, target_arch = "wasm32"))]
mod tests {
use super::*;
use crate::input::TransactionInput;
use crate::outpoint::TransactionOutpoint;
use crate::output::TransactionOutput;
use kaspa_consensus_core::subnets::SubnetworkId;
use kaspa_consensus_core::tx::ScriptPublicKey;
use wasm_bindgen::JsValue;
use wasm_bindgen_test::wasm_bindgen_test;
fn construct_spk() -> ScriptPublicKey {
ScriptPublicKey::new(0, vec![0xaa, 0xbb].into())
}
fn construct_tx(num_inputs: u32, num_outputs: u32) -> Transaction {
let fixed_txid = TransactionId::from_slice(&[0u8; 32]);
let spk = construct_spk();
let inputs: Vec<TransactionInput> = (0..num_inputs)
.map(|i| {
let outpoint = TransactionOutpoint::new(fixed_txid, i);
TransactionInput::new(outpoint, None, 0, 0, 0, None)
})
.collect();
let outputs: Vec<TransactionOutput> = (0..num_outputs).map(|_| TransactionOutput::ctor(100, &spk, None)).collect();
Transaction::new(None, 1, inputs, outputs, 0, SubnetworkId::from_bytes([0u8; 20]), 0, vec![], 0)
.expect("transaction construction should succeed")
}
fn construct_groups_array(groups: &[(u16, &[u32])]) -> js_sys::Array {
let arr = js_sys::Array::new();
for &(auth_input, outputs) in groups {
let obj = Object::new();
obj.set("authorizingInput", &JsValue::from(auth_input)).unwrap();
let out_arr = js_sys::Array::new();
for &o in outputs {
out_arr.push(&JsValue::from(o));
}
obj.set("outputs", &out_arr.into()).unwrap();
arr.push(&obj.into());
}
arr
}
#[wasm_bindgen_test]
fn test_populate_multiple_groups() {
let tx = construct_tx(2, 4);
let groups = construct_groups_array(&[(0, &[0, 1]), (1, &[2, 3])]);
tx.js_populate_genesis_covenants(groups.unchecked_ref()).expect("populate should succeed");
let inner = tx.inner();
let cov0 = inner.outputs[0].get_covenant().unwrap();
let cov1 = inner.outputs[1].get_covenant().unwrap();
let cov2 = inner.outputs[2].get_covenant().unwrap();
let cov3 = inner.outputs[3].get_covenant().unwrap();
assert_eq!(cov0.get_authorizing_input(), 0);
assert_eq!(cov1.get_authorizing_input(), 0);
assert_eq!(cov2.get_authorizing_input(), 1);
assert_eq!(cov3.get_authorizing_input(), 1);
assert_eq!(cov0.get_covenant_id(), cov1.get_covenant_id());
assert_eq!(cov2.get_covenant_id(), cov3.get_covenant_id());
assert_ne!(cov0.get_covenant_id(), cov2.get_covenant_id());
}
#[wasm_bindgen_test]
fn test_outputs_preserve_value_and_spk() {
let spk = construct_spk();
let tx = construct_tx(1, 2);
let groups = construct_groups_array(&[(0, &[0, 1])]);
tx.js_populate_genesis_covenants(groups.unchecked_ref()).expect("populate should succeed");
let inner = tx.inner();
for output in &inner.outputs {
assert_eq!(output.value(), 100);
assert_eq!(output.get_script_public_key(), spk);
}
}
#[wasm_bindgen_test]
fn deserialize_from_json_accepts_v0_shape() {
let safe_json = r#"{"id":"0000000000000000000000000000000000000000000000000000000000000000","version":0,"inputs":[{"transactionId":"0101010101010101010101010101010101010101010101010101010101010101","index":0,"sequence":"0","sigOpCount":1,"signatureScript":"01","utxo":{"amount":"1","scriptPublicKey":"000001","blockDaaScore":"0","isCoinbase":false}}],"outputs":[],"subnetworkId":"0000000000000000000000000000000000000000","lockTime":"0","gas":"0","mass":"1","payload":""}"#;
let json = r#"{"id":"0000000000000000000000000000000000000000000000000000000000000000","version":0,"inputs":[{"transactionId":"0101010101010101010101010101010101010101010101010101010101010101","index":0,"sequence":0,"sigOpCount":1,"signatureScript":"01","utxo":{"amount":1,"scriptPublicKey":"000001","blockDaaScore":0,"isCoinbase":false}}],"outputs":[],"subnetworkId":"0000000000000000000000000000000000000000","lockTime":0,"gas":0,"mass":1,"payload":""}"#;
for tx in [
Transaction::deserialize_from_safe_json(safe_json).expect("txv0 safe JSON should deserialize"),
Transaction::deserialize_from_json(json).expect("txv0 JSON should deserialize"),
] {
let inner = tx.inner();
assert_eq!(inner.version, 0);
assert_eq!(inner.storage_mass, 1);
assert_eq!(inner.inputs[0].get_compute_budget(), 0);
assert_eq!(inner.inputs[0].get_sig_op_count(), 1);
}
}
}