diff --git a/Cargo.lock b/Cargo.lock index a0db546302..a2ab775571 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3442,6 +3442,7 @@ dependencies = [ "kaspa-txscript", "kaspa-txscript-errors", "kaspa-utils", + "kaspa-wallet-keys", "secp256k1", "serde", "serde-value", diff --git a/wallet/pskt/Cargo.toml b/wallet/pskt/Cargo.toml index b3fff1bfaf..401d8a9b21 100644 --- a/wallet/pskt/Cargo.toml +++ b/wallet/pskt/Cargo.toml @@ -26,6 +26,7 @@ kaspa-consensus-core.workspace = true kaspa-txscript-errors.workspace = true kaspa-txscript.workspace = true kaspa-utils.workspace = true +kaspa-wallet-keys.workspace = true bincode.workspace = true derive_builder.workspace = true diff --git a/wallet/pskt/src/wasm/error.rs b/wallet/pskt/src/wasm/error.rs index 77fb0d8b16..a125e22503 100644 --- a/wallet/pskt/src/wasm/error.rs +++ b/wallet/pskt/src/wasm/error.rs @@ -33,6 +33,8 @@ pub enum Error { #[error(transparent)] Pskt(#[from] crate::error::Error), + // #[error("Failed to serialize into JSON")] + // SerializationError, } impl Error { diff --git a/wallet/pskt/src/wasm/pskt.rs b/wallet/pskt/src/wasm/pskt.rs index 8ee370a4b9..3275fe5e14 100644 --- a/wallet/pskt/src/wasm/pskt.rs +++ b/wallet/pskt/src/wasm/pskt.rs @@ -1,11 +1,18 @@ -use crate::pskt::PSKT as Native; +use crate::pskt::{SignInputOk, Signature, PSKT as Native}; use crate::role::*; -use kaspa_consensus_core::tx::TransactionId; +use kaspa_consensus_core::hashing::sighash::{calc_schnorr_signature_hash, SigHashReusedValuesUnsync}; +use kaspa_consensus_core::tx::{PopulatedTransaction, TransactionId}; +use kaspa_txscript::get_sig_op_count; +use kaspa_txscript::opcodes::codes::OpData65; +use kaspa_txscript::script_builder::ScriptBuilder; use wasm_bindgen::prelude::*; // use js_sys::Object; use crate::pskt::Inner; +use core::result::Result as CoreResult; use kaspa_consensus_client::{Transaction, TransactionInput, TransactionInputT, TransactionOutput, TransactionOutputT}; +use kaspa_wallet_keys::privatekey::PrivateKey; use serde::{Deserialize, Serialize}; +use std::iter; use std::sync::MutexGuard; use std::sync::{Arc, Mutex}; use workflow_wasm::{ @@ -60,8 +67,8 @@ impl From for PSKT { #[wasm_bindgen] extern "C" { - #[wasm_bindgen(typescript_type = "PSKT | Transaction | string | undefined")] - pub type CtorT; + #[wasm_bindgen(typescript_type = "PSKT | Transaction")] + pub type PayloadT; } #[derive(Clone, Serialize, Deserialize)] @@ -111,36 +118,30 @@ impl TryCastFromJs for PSKT { #[wasm_bindgen] impl PSKT { #[wasm_bindgen(constructor)] - pub fn new(payload: CtorT) -> Result { - PSKT::try_owned_from(payload.unchecked_into::().as_ref()).map_err(|err| Error::Ctor(err.to_string())) + pub fn new(payload: Option) -> Result { + match payload { + Some(payload) => { + PSKT::try_owned_from(payload.unchecked_into::().as_ref()).map_err(|err| Error::Ctor(err.to_string())) + } + None => Ok(PSKT::from(State::NoOp(None))), + } } #[wasm_bindgen(getter, js_name = "role")] - pub fn role_getter(&self) -> String { + pub fn role(&self) -> String { self.state().as_ref().unwrap().display().to_string() } #[wasm_bindgen(getter, js_name = "payload")] - pub fn payload_getter(&self) -> JsValue { + pub fn payload(&self) -> JsValue { + // TODO: correctly typing let state = self.state(); serde_wasm_bindgen::to_value(state.as_ref().unwrap()).unwrap() } - fn state(&self) -> MutexGuard> { - self.state.lock().unwrap() - } - - fn take(&self) -> State { - self.state.lock().unwrap().take().unwrap() - } - - fn replace(&self, state: State) -> Result { - self.state.lock().unwrap().replace(state); - Ok(self.clone()) - } - - /// Change role to `CREATOR` - /// #[wasm_bindgen(js_name = toCreator)] + /// Changes role to `CREATOR`. This initializes a PSKT in the Creator role, + /// which is responsible for generating a new transaction without any signatures. + #[wasm_bindgen(js_name = "toCreator")] pub fn creator(&self) -> Result { let state = match self.take() { State::NoOp(inner) => match inner { @@ -153,8 +154,12 @@ impl PSKT { self.replace(state) } - /// Change role to `CONSTRUCTOR` - #[wasm_bindgen(js_name = toConstructor)] + /// Changes role to `CONSTRUCTOR`. The constructor role is responsible for + /// adding the necessary witness data, scripts, or other PSKT fields required + /// to build the transaction. This role extends the creation phase, filling in + /// additional transaction details. + /// The constructor typically defines the transaction inputs and outputs. + #[wasm_bindgen(js_name = "toConstructor")] pub fn constructor(&self) -> Result { let state = match self.take() { State::NoOp(inner) => State::Constructor(inner.ok_or(Error::NotInitialized)?.into()), @@ -165,8 +170,32 @@ impl PSKT { self.replace(state) } - /// Change role to `UPDATER` - #[wasm_bindgen(js_name = toUpdater)] + #[wasm_bindgen(js_name = "addInput")] + pub fn input(&self, input: &TransactionInputT) -> Result { + let input = TransactionInput::try_owned_from(input)?; + let state = match self.take() { + State::Constructor(pskt) => State::Constructor(pskt.input(input.try_into()?)), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + #[wasm_bindgen(js_name = "addOutput")] + pub fn output(&self, output: &TransactionOutputT) -> Result { + let output = TransactionOutput::try_owned_from(output)?; + let state = match self.take() { + State::Constructor(pskt) => State::Constructor(pskt.output(output.try_into()?)), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Changes role to `UPDATER`. The updater is responsible for filling in more + /// specific information into the PSKT, such as completing any missing fields + /// like sequence, and ensuring inputs are correctly referenced. + #[wasm_bindgen(js_name = "toUpdater")] pub fn updater(&self) -> Result { let state = match self.take() { State::NoOp(inner) => State::Updater(inner.ok_or(Error::NotInitialized)?.into()), @@ -177,8 +206,22 @@ impl PSKT { self.replace(state) } - /// Change role to `SIGNER` - #[wasm_bindgen(js_name = toSigner)] + /// The sequence number determines when the input can be spent. + #[wasm_bindgen(js_name = "setSequence")] + pub fn set_sequence(&self, n: u64, input_index: usize) -> Result { + let state = match self.take() { + State::Updater(pskt) => State::Updater(pskt.set_sequence(n, input_index)?), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Changes role to `SIGNER`. The signer is responsible for providing valid + /// cryptographic signatures on the inputs of the PSKT. This role ensures that + /// the transaction is authenticated and can later be combined with other + /// signatures, if necessary. + #[wasm_bindgen(js_name = "toSigner")] pub fn signer(&self) -> Result { let state = match self.take() { State::NoOp(inner) => State::Signer(inner.ok_or(Error::NotInitialized)?.into()), @@ -191,8 +234,57 @@ impl PSKT { self.replace(state) } - /// Change role to `COMBINER` - #[wasm_bindgen(js_name = toCombiner)] + /// Calculates the current transaction id, + /// can only be executed by signers. + #[wasm_bindgen(js_name = "calculateId")] + pub fn calculate_id(&self) -> Result { + let state = self.state.lock().unwrap(); + match state.as_ref().unwrap() { + State::Signer(pskt) => Ok(pskt.calculate_id()), + state => Err(Error::state(state))?, + } + } + + /// Signs all inputs and adds it to partial signature cache + /// to be used in future, can only be executed by signers. + #[wasm_bindgen(js_name = "sign")] + pub fn sign(&self, private_key: &PrivateKey) -> Result { + let state = match self.take() { + State::Signer(pskt) => { + let keypair = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, &private_key.secret_bytes()).unwrap(); + let mut reused_values = SigHashReusedValuesUnsync::new(); + + let signed_pskt = pskt + .pass_signature_sync(|tx, sighash| -> CoreResult, String> { + tx.tx + .inputs + .iter() + .enumerate() + .map(|(idx, _input)| { + let hash = calc_schnorr_signature_hash(&tx.as_verifiable(), idx, sighash[idx], &mut reused_values); + let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice()).unwrap(); + Ok(SignInputOk { + signature: Signature::Schnorr(keypair.sign_schnorr(msg)), + pub_key: keypair.public_key(), + key_source: None, + }) + }) + .collect() + }) + .unwrap(); + State::Signer(signed_pskt) + } + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Changes role to `COMBINER`. The combiner merges multiple PSKTs from various + /// signers into a single, cohesive PSKT. This role is responsible for ensuring + /// that all necessary signatures are included and the transaction is ready for + /// finalization. + #[wasm_bindgen(js_name = "toCombiner")] pub fn combiner(&self) -> Result { let state = match self.take() { State::NoOp(inner) => State::Combiner(inner.ok_or(Error::NotInitialized)?.into()), @@ -205,11 +297,28 @@ impl PSKT { self.replace(state) } - /// Change role to `FINALIZER` - #[wasm_bindgen(js_name = toFinalizer)] + /// Combine an external signer PSKT into current PSKT by combiner role. + /// The state of both PSKTs will reset. + #[wasm_bindgen(js_name = "combine")] + pub fn combine(&self, target_pskt: PSKT) -> Result { + let state = match self.take() { + State::Combiner(pskt) => match target_pskt.take() { + State::Signer(other_pskt) => State::Combiner((pskt + other_pskt).unwrap()), + state => Err(Error::state(state))?, + }, + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Changes role to `FINALIZER`. The finalizer role is responsible for taking + /// the fully signed PSKT and ensuring that the transaction is complete and + /// ready to be submitted to the network. + #[wasm_bindgen(js_name = "toFinalizer")] pub fn finalizer(&self) -> Result { let state = match self.take() { - State::NoOp(inner) => State::Finalizer(inner.ok_or(Error::NotInitialized)?.into()), + State::NoOp(inner) => State::Finalizer(inner.ok_or(Error::NotInitialized)?.into()), // TODO: possibly allow signer to also be a finalizer -- skipping combiner State::Combiner(pskt) => State::Finalizer(pskt.finalizer()), state => Err(Error::state(state))?, }; @@ -217,8 +326,61 @@ impl PSKT { self.replace(state) } - /// Change role to `EXTRACTOR` - #[wasm_bindgen(js_name = toExtractor)] + /// Finalizes the partial signatures cache and populates the inputs with the + /// appropriate unlock scripts, ensuring that only the required number of + /// signatures (based on the sigOpCount) are included. + #[wasm_bindgen(js_name = "finalize")] + pub fn finalize(&self) -> Result { + let state = match self.take() { + State::Finalizer(pskt) => { + let finalized_pskt = pskt + .finalize_sync(|inner: &Inner| -> CoreResult>, String> { + Ok(inner + .inputs + .iter() + .map(|input| -> Vec { + let required_sig_count = get_sig_op_count::( + input.redeem_script.as_ref().unwrap(), // TODO: a question for aspect -- abt how to properly handle here + &input.utxo_entry.as_ref().unwrap().script_public_key, + ); + + let signatures: Vec<_> = input + .partial_sigs + .iter() + .take(required_sig_count as usize) + .flat_map(|(_, signature)| { + let sig_bytes = signature.into_bytes(); + iter::once(OpData65).chain(sig_bytes).chain([input.sighash_type.to_u8()]) + }) + .collect(); + + signatures + .into_iter() + .chain( + ScriptBuilder::new() + .add_data(input.redeem_script.as_ref().unwrap().as_slice()) + .unwrap() + .drain() + .iter() + .cloned(), + ) + .collect() + }) + .collect()) + }) + .unwrap(); + + State::Finalizer(finalized_pskt) + } + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Changes role to `EXTRACTOR`. The extractor is responsible for taking the + /// final transaction from the PSKT and usually submitting it to network. + #[wasm_bindgen(js_name = "toExtractor")] pub fn extractor(&self) -> Result { let state = match self.take() { State::NoOp(inner) => State::Extractor(inner.ok_or(Error::NotInitialized)?.into()), @@ -229,7 +391,19 @@ impl PSKT { self.replace(state) } - #[wasm_bindgen(js_name = fallbackLockTime)] + /// If the transaction is finalized, this function extracts it. + /// The state will reset after the transaction is extracted. + #[wasm_bindgen(js_name = "extractTransaction")] + pub fn extract_tx(&self) -> Result { + match self.take() { + State::Extractor(pskt) => Ok(pskt.extract_tx().unwrap()(0).0.into()), + state => Err(Error::state(state))?, + } + } + + /// This allows specifying a lock time that will be used if no other lock time requirement + /// is set in the final transactions inputs. + #[wasm_bindgen(js_name = "setFallbackLockTime")] pub fn fallback_lock_time(&self, lock_time: u64) -> Result { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.fallback_lock_time(lock_time)), @@ -239,7 +413,8 @@ impl PSKT { self.replace(state) } - #[wasm_bindgen(js_name = inputsModifiable)] + /// Marks the inputs as modifiable. + #[wasm_bindgen(js_name = "makeInputsModifiable")] pub fn inputs_modifiable(&self) -> Result { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.inputs_modifiable()), @@ -249,7 +424,8 @@ impl PSKT { self.replace(state) } - #[wasm_bindgen(js_name = outputsModifiable)] + /// Marks the outputs as modifiable. + #[wasm_bindgen(js_name = "makeOutputsModifiable")] pub fn outputs_modifiable(&self) -> Result { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.outputs_modifiable()), @@ -259,7 +435,8 @@ impl PSKT { self.replace(state) } - #[wasm_bindgen(js_name = noMoreInputs)] + /// Marks the inputs as finalized, preventing any additional inputs from being added. + #[wasm_bindgen(js_name = "noMoreInputs")] pub fn no_more_inputs(&self) -> Result { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_inputs()), @@ -269,7 +446,8 @@ impl PSKT { self.replace(state) } - #[wasm_bindgen(js_name = noMoreOutputs)] + /// Marks the outputs as finalized, preventing any additional outputs from being added. + #[wasm_bindgen(js_name = "noMoreOutputs")] pub fn no_more_outputs(&self) -> Result { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_outputs()), @@ -279,42 +457,16 @@ impl PSKT { self.replace(state) } - pub fn input(&self, input: &TransactionInputT) -> Result { - let input = TransactionInput::try_owned_from(input)?; - let state = match self.take() { - State::Constructor(pskt) => State::Constructor(pskt.input(input.try_into()?)), - state => Err(Error::state(state))?, - }; - - self.replace(state) - } - - pub fn output(&self, output: &TransactionOutputT) -> Result { - let output = TransactionOutput::try_owned_from(output)?; - let state = match self.take() { - State::Constructor(pskt) => State::Constructor(pskt.output(output.try_into()?)), - state => Err(Error::state(state))?, - }; - - self.replace(state) + fn state(&self) -> MutexGuard> { + self.state.lock().unwrap() } - #[wasm_bindgen(js_name = setSequence)] - pub fn set_sequence(&self, n: u64, input_index: usize) -> Result { - let state = match self.take() { - State::Updater(pskt) => State::Updater(pskt.set_sequence(n, input_index)?), - state => Err(Error::state(state))?, - }; - - self.replace(state) + fn take(&self) -> State { + self.state.lock().unwrap().take().unwrap() } - #[wasm_bindgen(js_name = calculateId)] - pub fn calculate_id(&self) -> Result { - let state = self.state(); - match state.as_ref().unwrap() { - State::Signer(pskt) => Ok(pskt.calculate_id()), - state => Err(Error::state(state))?, - } + fn replace(&self, state: State) -> Result { + self.state.lock().unwrap().replace(state); + Ok(self.clone()) } }