From 0ffbd155c37505d0c31068992417145ae7c39bc3 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Tue, 20 Jan 2026 12:59:27 -0500 Subject: [PATCH 01/15] pskt wip --- src/wallet/mod.rs | 1 + src/wallet/pskt/mod.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/wallet/pskt/mod.rs diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index d6f842e3..389fb30b 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1,3 +1,4 @@ pub mod bip32; pub mod core; pub mod keys; +pub mod pskt; \ No newline at end of file diff --git a/src/wallet/pskt/mod.rs b/src/wallet/pskt/mod.rs new file mode 100644 index 00000000..8de8fd56 --- /dev/null +++ b/src/wallet/pskt/mod.rs @@ -0,0 +1,33 @@ +use std::sync::{Arc, Mutex}; + +use kaspa_consensus_client::Transaction; +use kaspa_wallet_pskt::{pskt::Inner, wasm::pskt::State}; +use pyo3::prelude::*; +use pyo3_stub_gen::derive::*; + +use crate::consensus::client::transaction::PyTransaction; + +/// Partially Signed Kaspa Transaction +#[gen_stub_pyclass] +#[pyclass(name = "PSKT")] +#[derive(Clone)] +pub struct PyPSKT { + state: Arc>>, +} + +#[gen_stub_pymethods] +#[pymethods] +impl PyPSKT { + #[new] + pub fn new(payload: Bound<'_, PyAny>) -> PyResult { + let payload = if let Ok(p) = payload.cast::() { + Inner::from(s) + } else if let Ok(p) = payload.cast::() { + let tx: Transaction = p.into(); + let inner: Inner = tx.into(); + inner + }; + + Ok(()) + } +} \ No newline at end of file From fc6e8be842c8ec0704dce3ea8a8c5eaeea6ec052 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Wed, 21 Jan 2026 17:50:34 -0500 Subject: [PATCH 02/15] check in pskt progress pre-custom exceptions --- Cargo.lock | 1 + Cargo.toml | 1 + src/wallet/mod.rs | 2 +- src/wallet/pskt/error.rs | 3 + src/wallet/pskt/mod.rs | 298 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 291 insertions(+), 14 deletions(-) create mode 100644 src/wallet/pskt/error.rs diff --git a/Cargo.lock b/Cargo.lock index 1270e673..984f5426 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2170,6 +2170,7 @@ dependencies = [ "kaspa-utils", "kaspa-wallet-core", "kaspa-wallet-keys", + "kaspa-wallet-pskt", "kaspa-wrpc-client", "paste", "pyo3", diff --git a/Cargo.toml b/Cargo.toml index 1d975e77..e75ec541 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ kaspa-txscript = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1 kaspa-utils = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" } kaspa-wallet-core = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" } kaspa-wallet-keys = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" } +kaspa-wallet-pskt = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" } kaspa-wrpc-client = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" } paste = "1.0" pyo3 = { version = "0.27.1", features = ['multiple-pymethods'] } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 389fb30b..e63d6e6b 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1,4 +1,4 @@ pub mod bip32; pub mod core; pub mod keys; -pub mod pskt; \ No newline at end of file +pub mod pskt; diff --git a/src/wallet/pskt/error.rs b/src/wallet/pskt/error.rs new file mode 100644 index 00000000..fa7e0580 --- /dev/null +++ b/src/wallet/pskt/error.rs @@ -0,0 +1,3 @@ +use pyo3::prelude::*; +use pyo3::create_exception; +use pyo3::exceptions::PyException; \ No newline at end of file diff --git a/src/wallet/pskt/mod.rs b/src/wallet/pskt/mod.rs index 8de8fd56..6748daaf 100644 --- a/src/wallet/pskt/mod.rs +++ b/src/wallet/pskt/mod.rs @@ -1,11 +1,15 @@ -use std::sync::{Arc, Mutex}; +pub mod error; +use crate::consensus::client::transaction::PyTransaction; use kaspa_consensus_client::Transaction; -use kaspa_wallet_pskt::{pskt::Inner, wasm::pskt::State}; -use pyo3::prelude::*; +use kaspa_wallet_pskt::{ + pskt::{Inner, PSKT}, + role::*, + wasm::pskt::State, +}; +use pyo3::{exceptions::PyException, prelude::*}; use pyo3_stub_gen::derive::*; - -use crate::consensus::client::transaction::PyTransaction; +use std::sync::{Arc, Mutex}; /// Partially Signed Kaspa Transaction #[gen_stub_pyclass] @@ -20,14 +24,282 @@ pub struct PyPSKT { impl PyPSKT { #[new] pub fn new(payload: Bound<'_, PyAny>) -> PyResult { - let payload = if let Ok(p) = payload.cast::() { - Inner::from(s) - } else if let Ok(p) = payload.cast::() { - let tx: Transaction = p.into(); - let inner: Inner = tx.into(); - inner + let payload = if let Ok(p) = payload.extract::() { + let inner = + serde_json::from_str(&p).map_err(|err| PyException::new_err(err.to_string()))?; + Ok(PyPSKT::from(State::NoOp(Some(inner)))) + } else if let Ok(py_tx) = payload.extract::() { + let tx: Transaction = py_tx.into(); + let inner: Inner = tx + .try_into() + .map_err(|_| PyException::new_err("Transaction to Inner failed"))?; + Ok(PyPSKT::from(State::NoOp(Some(inner)))) + } else if payload.is_none() { + Ok(PyPSKT::from(State::Creator(PSKT::::default()))) + } else { + Err(PyException::new_err("Invalid payload")) + }?; + + Ok(payload) + } + + #[getter] + pub fn get_role(&self) -> String { + self.state().as_ref().unwrap().display().to_string() + } + + #[getter] + pub fn get_payload(&self) -> JsValue { + let state = self.state(); + workflow_wasm::serde::to_value(state.as_ref().unwrap()).unwrap() + } + + pub fn serialize(&self) -> String { + let state = self.state(); + serde_json::to_string(state.as_ref().unwrap()).unwrap() + } + + fn state(&self) -> MutexGuard<'_, Option> { + self.state.lock().unwrap() + } + + fn take(&self) -> State { + self.state.lock().unwrap().take().unwrap() + } + + fn replace(&self, state: State) -> PyResult { + self.state.lock().unwrap().replace(state); + Ok(self.clone()) + } + + /// Change role to `CREATOR` + pub fn creator(&self) -> PyResult { + let state = match self.take() { + State::NoOp(inner) => match inner { + None => State::Creator(PSKT::default()), + Some(_) => Err(Error::CreateNotAllowed)?, + }, + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `CONSTRUCTOR` + pub fn to_constructor(&self) -> PyResult { + let state = match self.take() { + State::NoOp(inner) => State::Constructor(inner.ok_or(Error::NotInitialized)?.into()), + State::Creator(pskt) => State::Constructor(pskt.constructor()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `UPDATER` + pub fn to_updater(&self) -> PyResult { + let state = match self.take() { + State::NoOp(inner) => State::Updater(inner.ok_or(Error::NotInitialized)?.into()), + State::Constructor(constructor) => State::Updater(constructor.updater()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `SIGNER` + pub fn to_signer(&self) -> PyResult { + let state = match self.take() { + State::NoOp(inner) => State::Signer(inner.ok_or(Error::NotInitialized)?.into()), + State::Constructor(pskt) => State::Signer(pskt.signer()), + State::Updater(pskt) => State::Signer(pskt.signer()), + State::Combiner(pskt) => State::Signer(pskt.signer()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `COMBINER` + pub fn to_combiner(&self) -> PyResult { + let state = match self.take() { + State::NoOp(inner) => State::Combiner(inner.ok_or(Error::NotInitialized)?.into()), + State::Constructor(pskt) => State::Combiner(pskt.combiner()), + State::Updater(pskt) => State::Combiner(pskt.combiner()), + State::Signer(pskt) => State::Combiner(pskt.combiner()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `FINALIZER` + pub fn to_finalizer(&self) -> PyResult { + let state = match self.take() { + State::NoOp(inner) => State::Finalizer(inner.ok_or(Error::NotInitialized)?.into()), + State::Combiner(pskt) => State::Finalizer(pskt.finalizer()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `EXTRACTOR` + pub fn to_extractor(&self) -> PyResult { + let state = match self.take() { + State::NoOp(inner) => State::Extractor(inner.ok_or(Error::NotInitialized)?.into()), + State::Finalizer(pskt) => State::Extractor(pskt.extractor()?), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + pub fn fallback_lock_time(&self, lock_time: u64) -> PyResult { + let state = match self.take() { + State::Creator(pskt) => State::Creator(pskt.fallback_lock_time(lock_time)), + _ => Err(Error::expected_state("Creator"))?, }; - Ok(()) + self.replace(state) } -} \ No newline at end of file + + pub fn inputs_modifiable(&self) -> PyResult { + let state = match self.take() { + State::Creator(pskt) => State::Creator(pskt.inputs_modifiable()), + _ => Err(Error::expected_state("Creator"))?, + }; + + self.replace(state) + } + + pub fn outputs_modifiable(&self) -> PyResult { + let state = match self.take() { + State::Creator(pskt) => State::Creator(pskt.outputs_modifiable()), + _ => Err(Error::expected_state("Creator"))?, + }; + + self.replace(state) + } + + pub fn no_more_inputs(&self) -> PyResult { + let state = match self.take() { + State::Constructor(pskt) => State::Constructor(pskt.no_more_inputs()), + _ => Err(Error::expected_state("Constructor"))?, + }; + + self.replace(state) + } + + pub fn no_more_outputs(&self) -> PyResult { + let state = match self.take() { + State::Constructor(pskt) => State::Constructor(pskt.no_more_outputs()), + _ => Err(Error::expected_state("Constructor"))?, + }; + + self.replace(state) + } + + pub fn input_and_redeem_scrtip(&self, input: &TransactionInputT, data: &JsValue) -> PyResult { + let obj = js_sys::Object::from(data.clone()); + + let input = TransactionInput::try_owned_from(input)?; + let mut input: Input = input.try_into()?; + let redeem_script = js_sys::Reflect::get(&obj, &"redeemScript".into()) + .expect("Missing redeemscript field") + .as_string() + .expect("redeemscript must be a string"); + input.redeem_script = + Some(hex::decode(redeem_script).map_err(|e| Error::custom(format!("Redeem script is not a hex string: {}", e)))?); + let state = match self.take() { + State::Constructor(pskt) => State::Constructor(pskt.input(input)), + _ => Err(Error::expected_state("Constructor"))?, + }; + + self.replace(state) + } + + pub fn input(&self, input: &TransactionInputT) -> PyResult { + let input = TransactionInput::try_owned_from(input)?; + let state = match self.take() { + State::Constructor(pskt) => State::Constructor(pskt.input(input.try_into()?)), + _ => Err(Error::expected_state("Constructor"))?, + }; + + self.replace(state) + } + + pub fn output(&self, output: &TransactionOutputT) -> PyResult { + let output = TransactionOutput::try_owned_from(output)?; + let state = match self.take() { + State::Constructor(pskt) => State::Constructor(pskt.output(output.try_into()?)), + _ => Err(Error::expected_state("Constructor"))?, + }; + + self.replace(state) + } + + pub fn set_sequence(&self, n: u64, input_index: usize) -> PyResult { + let state = match self.take() { + State::Updater(pskt) => State::Updater(pskt.set_sequence(n, input_index)?), + _ => Err(Error::expected_state("Updater"))?, + }; + + self.replace(state) + } + + #[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()), + _ => Err(Error::expected_state("Signer"))?, + } + } + + #[wasm_bindgen(js_name = calculateMass)] + pub fn calculate_mass(&self, data: &JsValue) -> Result { + let obj = js_sys::Object::from(data.clone()); + let network_id = js_sys::Reflect::get(&obj, &"networkId".into()) + .map_err(|_| Error::custom("networkId is missing"))? + .as_string() + .ok_or_else(|| Error::custom("networkId must be a string"))?; + + let network_id = NetworkType::from_str(&network_id).map_err(|e| Error::custom(format!("Invalid networkId: {}", e)))?; + + let cloned_pskt = self.clone(); + + let extractor = { + let finalizer = cloned_pskt.finalizer()?; + + let finalizer_state = finalizer.state().clone().unwrap(); + + match finalizer_state { + State::Finalizer(pskt) => { + for input in pskt.inputs.iter() { + if input.redeem_script.is_some() { + return Err(Error::custom("Mass calculation is not supported for inputs with redeem scripts")); + } + } + let pskt = pskt + .finalize_sync(|inner: &Inner| -> Result>> { Ok(vec![vec![0u8, 65]; inner.inputs.len()]) }) + .map_err(|e| Error::custom(format!("Failed to finalize PSKT: {e}")))?; + pskt.extractor()? + } + _ => panic!("Finalizer state is not valid"), + } + }; + let tx = extractor + .extract_tx_unchecked(&network_id.into()) + .map_err(|e| Error::custom(format!("Failed to extract transaction: {e}")))?; + Ok(tx.tx.mass()) + } +} + +impl From for PyPSKT { + fn from(value: State) -> Self { + PyPSKT { + state: Arc::new(Mutex::new(Some(value))), + } + } +} From 905eaf48cd5b65d084a354e4fb30e8cf1e508a25 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 24 Jan 2026 07:59:37 -0500 Subject: [PATCH 03/15] pskt build success --- Cargo.lock | 1 + Cargo.toml | 1 + kaspa.pyi | 81 ++++++++++++--- src/lib.rs | 42 +++++++- src/wallet/pskt/error.rs | 46 ++++++++- src/wallet/pskt/mod.rs | 215 +++++++++++++++++++++++++-------------- 6 files changed, 290 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 984f5426..1edb4ce9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2159,6 +2159,7 @@ dependencies = [ "bincode", "faster-hex", "futures", + "hex", "kaspa-addresses", "kaspa-bip32", "kaspa-consensus-client", diff --git a/Cargo.toml b/Cargo.toml index e75ec541..355474e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ ahash = "0.8.12" bincode = "1.3.3" faster-hex = "0.9.0" futures = "0.3.31" +hex = "0.4.3" kaspa-addresses = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" } kaspa-bip32 = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" } kaspa-consensus-client = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" } diff --git a/kaspa.pyi b/kaspa.pyi index f5abdaf2..f2fe17b5 100644 --- a/kaspa.pyi +++ b/kaspa.pyi @@ -762,6 +762,57 @@ class Outputs: """ ... +@typing.final +class PSKT: + r""" + Partially Signed Kaspa Transaction + """ + @property + def role(self) -> builtins.str: ... + @property + def payload(self) -> builtins.str: ... + def __new__(cls, payload: typing.Any) -> PSKT: ... + def serialize(self) -> builtins.str: ... + def creator(self) -> PSKT: + r""" + Change role to `CREATOR` + """ + def to_constructor(self) -> PSKT: + r""" + Change role to `CONSTRUCTOR` + """ + def to_updater(self) -> PSKT: + r""" + Change role to `UPDATER` + """ + def to_signer(self) -> PSKT: + r""" + Change role to `SIGNER` + """ + def to_combiner(self) -> PSKT: + r""" + Change role to `COMBINER` + """ + def to_finalizer(self) -> PSKT: + r""" + Change role to `FINALIZER` + """ + def to_extractor(self) -> PSKT: + r""" + Change role to `EXTRACTOR` + """ + def fallback_lock_time(self, lock_time: builtins.int) -> PSKT: ... + def inputs_modifiable(self) -> PSKT: ... + def outputs_modifiable(self) -> PSKT: ... + def no_more_inputs(self) -> PSKT: ... + def no_more_outputs(self) -> PSKT: ... + def input_and_redeem_script(self, input: TransactionInput, data: builtins.str) -> PSKT: ... + def input(self, input: TransactionInput) -> PSKT: ... + def output(self, output: TransactionOutput) -> PSKT: ... + def set_sequence(self, n: builtins.int, input_index: builtins.int) -> PSKT: ... + def calculate_id(self) -> Hash: ... + def calculate_mass(self, data: NetworkId) -> builtins.int: ... + @typing.final class PaymentOutput: r""" @@ -2444,6 +2495,21 @@ class UtxoContext: Return a range of mature UTXO entries. """ +@typing.final +class UtxoEntries: + r""" + UTXO entries collection for flexible input handling. + + This type is not intended to be instantiated directly from Python. + It serves as a helper type that allows Rust functions to accept a list + of UTXO entries in multiple convenient forms. + + Accepts: + list[UtxoEntryReference]: A list of UtxoEntryReference objects. + list[dict]: A list of dicts with UtxoEntryReference-compatible keys. + """ + ... + @typing.final class UtxoEntries: r""" @@ -2488,21 +2554,6 @@ class UtxoEntries: """ def __eq__(self, other: UtxoEntries) -> builtins.bool: ... -@typing.final -class UtxoEntries: - r""" - UTXO entries collection for flexible input handling. - - This type is not intended to be instantiated directly from Python. - It serves as a helper type that allows Rust functions to accept a list - of UTXO entries in multiple convenient forms. - - Accepts: - list[UtxoEntryReference]: A list of UtxoEntryReference objects. - list[dict]: A list of dicts with UtxoEntryReference-compatible keys. - """ - ... - @typing.final class UtxoEntry: r""" diff --git a/src/lib.rs b/src/lib.rs index dcef9df5..0936f625 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ use pyo3_stub_gen::define_stub_info_gatherer; define_stub_info_gatherer!(stub_info); #[pymodule] -fn kaspa(m: &Bound<'_, PyModule>) -> PyResult<()> { +fn kaspa(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { pyo3_log::init(); m.add_class::()?; @@ -157,5 +157,45 @@ fn kaspa(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add( + "PsktCustomError", + py.get_type::(), + )?; + m.add( + "PsktStateError", + py.get_type::(), + )?; + m.add( + "PsktExpectedStateError", + py.get_type::(), + )?; + m.add( + "PsktCtorError", + py.get_type::(), + )?; + m.add( + "PsktInvalidPayloadError", + py.get_type::(), + )?; + m.add( + "PsktTxNotFinalizedError", + py.get_type::(), + )?; + m.add( + "PsktCreateNotAllowedError", + py.get_type::(), + )?; + m.add( + "PsktNotInitializedError", + py.get_type::(), + )?; + m.add( + "PsktConsensusClientError", + py.get_type::(), + )?; + m.add("PsktError", py.get_type::())?; + + m.add_class::()?; + Ok(()) } diff --git a/src/wallet/pskt/error.rs b/src/wallet/pskt/error.rs index fa7e0580..33e1ed60 100644 --- a/src/wallet/pskt/error.rs +++ b/src/wallet/pskt/error.rs @@ -1,3 +1,45 @@ -use pyo3::prelude::*; +use kaspa_wallet_pskt::wasm::error::Error; use pyo3::create_exception; -use pyo3::exceptions::PyException; \ No newline at end of file +use pyo3::exceptions::PyException; +use pyo3::prelude::*; + +pub struct PyPsktError(pub Error); + +create_exception!("kaspa", PsktCustomError, PyException); +create_exception!("kaspa", PsktStateError, PyException); +create_exception!("kaspa", PsktExpectedStateError, PyException); +create_exception!("kaspa", PsktCtorError, PyException); +create_exception!("kaspa", PsktInvalidPayloadError, PyException); +create_exception!("kaspa", PsktTxNotFinalizedError, PyException); +create_exception!("kaspa", PsktCreateNotAllowedError, PyException); +create_exception!("kaspa", PsktNotInitializedError, PyException); +create_exception!("kaspa", PsktConsensusClientError, PyException); +create_exception!("kaspa", PsktError, PyException); + +impl From for PyErr { + fn from(value: PyPsktError) -> Self { + match value.0 { + Error::Custom(msg) => PsktCustomError::new_err(msg), + Error::State(msg) => PsktStateError::new_err(msg), + Error::ExpectedState(msg) => PsktExpectedStateError::new_err(msg), + Error::Ctor(msg) => PsktCtorError::new_err(msg), + Error::InvalidPayload => PsktInvalidPayloadError::new_err("Invalid payload"), + Error::TxNotFinalized(inner) => PsktTxNotFinalizedError::new_err(inner.to_string()), + Error::CreateNotAllowed => PsktCreateNotAllowedError::new_err( + "Create state is not allowed for PSKT initialized from transaction or a payload", + ), + Error::NotInitialized => PsktNotInitializedError::new_err( + "PSKT must be initialized with a payload or CREATE role", + ), + Error::ConsensusClient(inner) => PsktConsensusClientError::new_err(inner.to_string()), + Error::Pskt(inner) => PsktError::new_err(inner.to_string()), + _ => PyException::new_err("Unhandled error type"), + } + } +} + +impl From for PyPsktError { + fn from(value: Error) -> Self { + PyPsktError(value) + } +} diff --git a/src/wallet/pskt/mod.rs b/src/wallet/pskt/mod.rs index 6748daaf..830ed387 100644 --- a/src/wallet/pskt/mod.rs +++ b/src/wallet/pskt/mod.rs @@ -1,7 +1,15 @@ pub mod error; +use crate::consensus::client::input::PyTransactionInput; +use crate::consensus::client::output::PyTransactionOutput; use crate::consensus::client::transaction::PyTransaction; -use kaspa_consensus_client::Transaction; +use crate::consensus::core::network::PyNetworkId; +use crate::consensus::core::tx::TransactionId; +use error::PyPsktError; +use kaspa_consensus_client::{Transaction, TransactionInput, TransactionOutput}; +use kaspa_consensus_core::network::NetworkType; +use kaspa_wallet_pskt::pskt::Input; +use kaspa_wallet_pskt::wasm::error::Error; use kaspa_wallet_pskt::{ pskt::{Inner, PSKT}, role::*, @@ -9,7 +17,7 @@ use kaspa_wallet_pskt::{ }; use pyo3::{exceptions::PyException, prelude::*}; use pyo3_stub_gen::derive::*; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, MutexGuard}; /// Partially Signed Kaspa Transaction #[gen_stub_pyclass] @@ -19,6 +27,21 @@ pub struct PyPSKT { state: Arc>>, } +impl PyPSKT { + fn take(&self) -> State { + self.state.lock().unwrap().take().unwrap() + } + + fn replace(&self, state: State) -> PyResult { + self.state.lock().unwrap().replace(state); + Ok(self.clone()) + } + + fn state(&self) -> MutexGuard<'_, Option> { + self.state.lock().unwrap() + } +} + #[gen_stub_pymethods] #[pymethods] impl PyPSKT { @@ -49,9 +72,11 @@ impl PyPSKT { } #[getter] - pub fn get_payload(&self) -> JsValue { + pub fn get_payload(&self) -> PyResult { let state = self.state(); - workflow_wasm::serde::to_value(state.as_ref().unwrap()).unwrap() + serde_json::to_string(state.as_ref().unwrap()) + .map_err(|err| PyException::new_err(err.to_string())) + // workflow_wasm::serde::to_value(state.as_ref().unwrap()).unwrap() } pub fn serialize(&self) -> String { @@ -59,27 +84,14 @@ impl PyPSKT { serde_json::to_string(state.as_ref().unwrap()).unwrap() } - fn state(&self) -> MutexGuard<'_, Option> { - self.state.lock().unwrap() - } - - fn take(&self) -> State { - self.state.lock().unwrap().take().unwrap() - } - - fn replace(&self, state: State) -> PyResult { - self.state.lock().unwrap().replace(state); - Ok(self.clone()) - } - /// Change role to `CREATOR` pub fn creator(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => match inner { None => State::Creator(PSKT::default()), - Some(_) => Err(Error::CreateNotAllowed)?, + Some(_) => Err(PyPsktError(Error::CreateNotAllowed))?, }, - state => Err(Error::state(state))?, + state => Err(PyPsktError(Error::state(state)))?, }; self.replace(state) @@ -88,9 +100,11 @@ impl PyPSKT { /// Change role to `CONSTRUCTOR` pub fn to_constructor(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Constructor(inner.ok_or(Error::NotInitialized)?.into()), + State::NoOp(inner) => { + State::Constructor(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + } State::Creator(pskt) => State::Constructor(pskt.constructor()), - state => Err(Error::state(state))?, + state => Err(PyPsktError(Error::state(state)))?, }; self.replace(state) @@ -99,9 +113,11 @@ impl PyPSKT { /// Change role to `UPDATER` pub fn to_updater(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Updater(inner.ok_or(Error::NotInitialized)?.into()), + State::NoOp(inner) => { + State::Updater(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + } State::Constructor(constructor) => State::Updater(constructor.updater()), - state => Err(Error::state(state))?, + state => Err(PyPsktError(Error::state(state)))?, }; self.replace(state) @@ -110,11 +126,13 @@ impl PyPSKT { /// Change role to `SIGNER` pub fn to_signer(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Signer(inner.ok_or(Error::NotInitialized)?.into()), + State::NoOp(inner) => { + State::Signer(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + } State::Constructor(pskt) => State::Signer(pskt.signer()), State::Updater(pskt) => State::Signer(pskt.signer()), State::Combiner(pskt) => State::Signer(pskt.signer()), - state => Err(Error::state(state))?, + state => Err(PyPsktError(Error::state(state)))?, }; self.replace(state) @@ -123,11 +141,13 @@ impl PyPSKT { /// Change role to `COMBINER` pub fn to_combiner(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Combiner(inner.ok_or(Error::NotInitialized)?.into()), + State::NoOp(inner) => { + State::Combiner(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + } State::Constructor(pskt) => State::Combiner(pskt.combiner()), State::Updater(pskt) => State::Combiner(pskt.combiner()), State::Signer(pskt) => State::Combiner(pskt.combiner()), - state => Err(Error::state(state))?, + state => Err(PyPsktError(Error::state(state)))?, }; self.replace(state) @@ -136,9 +156,11 @@ impl PyPSKT { /// Change role to `FINALIZER` pub fn to_finalizer(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Finalizer(inner.ok_or(Error::NotInitialized)?.into()), + State::NoOp(inner) => { + State::Finalizer(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + } State::Combiner(pskt) => State::Finalizer(pskt.finalizer()), - state => Err(Error::state(state))?, + state => Err(PyPsktError(Error::state(state)))?, }; self.replace(state) @@ -147,9 +169,13 @@ impl PyPSKT { /// Change role to `EXTRACTOR` pub fn to_extractor(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Extractor(inner.ok_or(Error::NotInitialized)?.into()), - State::Finalizer(pskt) => State::Extractor(pskt.extractor()?), - state => Err(Error::state(state))?, + State::NoOp(inner) => { + State::Extractor(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + } + State::Finalizer(pskt) => { + State::Extractor(pskt.extractor().map_err(Error::from).map_err(PyPsktError)?) + } + state => Err(PyPsktError(Error::state(state)))?, }; self.replace(state) @@ -158,7 +184,7 @@ impl PyPSKT { pub fn fallback_lock_time(&self, lock_time: u64) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.fallback_lock_time(lock_time)), - _ => Err(Error::expected_state("Creator"))?, + _ => Err(PyPsktError(Error::expected_state("Creator")))?, }; self.replace(state) @@ -167,7 +193,7 @@ impl PyPSKT { pub fn inputs_modifiable(&self) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.inputs_modifiable()), - _ => Err(Error::expected_state("Creator"))?, + _ => Err(PyPsktError(Error::expected_state("Creator")))?, }; self.replace(state) @@ -176,7 +202,7 @@ impl PyPSKT { pub fn outputs_modifiable(&self) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.outputs_modifiable()), - _ => Err(Error::expected_state("Creator"))?, + _ => Err(PyPsktError(Error::expected_state("Creator")))?, }; self.replace(state) @@ -185,7 +211,7 @@ impl PyPSKT { pub fn no_more_inputs(&self) -> PyResult { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_inputs()), - _ => Err(Error::expected_state("Constructor"))?, + _ => Err(PyPsktError(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -194,46 +220,66 @@ impl PyPSKT { pub fn no_more_outputs(&self) -> PyResult { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_outputs()), - _ => Err(Error::expected_state("Constructor"))?, + _ => Err(PyPsktError(Error::expected_state("Constructor")))?, }; self.replace(state) } - pub fn input_and_redeem_scrtip(&self, input: &TransactionInputT, data: &JsValue) -> PyResult { - let obj = js_sys::Object::from(data.clone()); - - let input = TransactionInput::try_owned_from(input)?; - let mut input: Input = input.try_into()?; - let redeem_script = js_sys::Reflect::get(&obj, &"redeemScript".into()) - .expect("Missing redeemscript field") - .as_string() - .expect("redeemscript must be a string"); - input.redeem_script = - Some(hex::decode(redeem_script).map_err(|e| Error::custom(format!("Redeem script is not a hex string: {}", e)))?); + pub fn input_and_redeem_script( + &self, + input: PyTransactionInput, + data: String, + ) -> PyResult { + let input = TransactionInput::from(input); + let mut input: Input = input + .try_into() + .map_err(|err| PyPsktError(Error::from(err)))?; + // let redeem_script = js_sys::Reflect::get(&obj, &"redeemScript".into()) + // .expect("Missing redeemscript field") + // .as_string() + // .expect("redeemscript must be a string"); + input.redeem_script = Some(hex::decode(data).map_err(|e| { + PyPsktError(Error::custom(format!( + "Redeem script is not a hex string: {}", + e + ))) + })?); let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.input(input)), - _ => Err(Error::expected_state("Constructor"))?, + _ => Err(PyPsktError(Error::expected_state("Constructor")))?, }; self.replace(state) } - pub fn input(&self, input: &TransactionInputT) -> PyResult { - let input = TransactionInput::try_owned_from(input)?; + pub fn input(&self, input: PyTransactionInput) -> PyResult { + let input = TransactionInput::from(input); let state = match self.take() { - State::Constructor(pskt) => State::Constructor(pskt.input(input.try_into()?)), - _ => Err(Error::expected_state("Constructor"))?, + State::Constructor(pskt) => State::Constructor( + pskt.input( + input + .try_into() + .map_err(|err| PyPsktError(Error::from(err)))?, + ), + ), + _ => Err(PyPsktError(Error::expected_state("Constructor")))?, }; self.replace(state) } - pub fn output(&self, output: &TransactionOutputT) -> PyResult { - let output = TransactionOutput::try_owned_from(output)?; + pub fn output(&self, output: PyTransactionOutput) -> PyResult { + let output = TransactionOutput::from(output); let state = match self.take() { - State::Constructor(pskt) => State::Constructor(pskt.output(output.try_into()?)), - _ => Err(Error::expected_state("Constructor"))?, + State::Constructor(pskt) => State::Constructor( + pskt.output( + output + .try_into() + .map_err(|err| PyPsktError(Error::from(err)))?, + ), + ), + _ => Err(PyPsktError(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -241,36 +287,39 @@ impl PyPSKT { pub fn set_sequence(&self, n: u64, input_index: usize) -> PyResult { let state = match self.take() { - State::Updater(pskt) => State::Updater(pskt.set_sequence(n, input_index)?), - _ => Err(Error::expected_state("Updater"))?, + State::Updater(pskt) => State::Updater( + pskt.set_sequence(n, input_index) + .map_err(|err| PyPsktError(Error::from(err)))?, + ), + _ => Err(PyPsktError(Error::expected_state("Updater")))?, }; self.replace(state) } - #[wasm_bindgen(js_name = calculateId)] - pub fn calculate_id(&self) -> Result { + pub fn calculate_id(&self) -> PyResult { let state = self.state(); match state.as_ref().unwrap() { - State::Signer(pskt) => Ok(pskt.calculate_id()), - _ => Err(Error::expected_state("Signer"))?, + State::Signer(pskt) => Ok(pskt.calculate_id().into()), + _ => Err(PyPsktError(Error::expected_state("Signer")))?, } } - #[wasm_bindgen(js_name = calculateMass)] - pub fn calculate_mass(&self, data: &JsValue) -> Result { - let obj = js_sys::Object::from(data.clone()); - let network_id = js_sys::Reflect::get(&obj, &"networkId".into()) - .map_err(|_| Error::custom("networkId is missing"))? - .as_string() - .ok_or_else(|| Error::custom("networkId must be a string"))?; + pub fn calculate_mass(&self, data: PyNetworkId) -> PyResult { + // let obj = js_sys::Object::from(data.clone()); + // let network_id = js_sys::Reflect::get(&obj, &"networkId".into()) + // .map_err(|_| Error::custom("networkId is missing"))? + // .as_string() + // .ok_or_else(|| Error::custom("networkId must be a string"))?; - let network_id = NetworkType::from_str(&network_id).map_err(|e| Error::custom(format!("Invalid networkId: {}", e)))?; + // let network_id = NetworkType::from_str(&network_id) + // .map_err(|e| Error::custom(format!("Invalid networkId: {}", e)))?; + let network_type = data.get_network_type(); let cloned_pskt = self.clone(); let extractor = { - let finalizer = cloned_pskt.finalizer()?; + let finalizer = cloned_pskt.to_finalizer()?; let finalizer_state = finalizer.state().clone().unwrap(); @@ -278,20 +327,30 @@ impl PyPSKT { State::Finalizer(pskt) => { for input in pskt.inputs.iter() { if input.redeem_script.is_some() { - return Err(Error::custom("Mass calculation is not supported for inputs with redeem scripts")); + return Err(PyPsktError(Error::custom( + "Mass calculation is not supported for inputs with redeem scripts", + )) + .into()); } } let pskt = pskt - .finalize_sync(|inner: &Inner| -> Result>> { Ok(vec![vec![0u8, 65]; inner.inputs.len()]) }) - .map_err(|e| Error::custom(format!("Failed to finalize PSKT: {e}")))?; - pskt.extractor()? + .finalize_sync(|inner: &Inner| -> PyResult>> { + Ok(vec![vec![0u8, 65]; inner.inputs.len()]) + }) + .map_err(|e| { + PyPsktError(Error::custom(format!("Failed to finalize PSKT: {e}"))) + })?; + pskt.extractor() + .map_err(|err| PyPsktError(Error::TxNotFinalized(err)))? } _ => panic!("Finalizer state is not valid"), } }; let tx = extractor - .extract_tx_unchecked(&network_id.into()) - .map_err(|e| Error::custom(format!("Failed to extract transaction: {e}")))?; + .extract_tx_unchecked(&NetworkType::from(network_type).into()) + .map_err(|e| { + PyPsktError(Error::custom(format!("Failed to extract transaction: {e}"))) + })?; Ok(tx.tx.mass()) } } From c8424a28bd213c2800cd0a1dc0f634d51721d4da Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 24 Jan 2026 08:46:18 -0500 Subject: [PATCH 04/15] Exceptions submodule --- kaspa.pyi | 4 ++++ src/lib.rs | 9 ++++++++- src/wallet/pskt/error.rs | 39 +++++++++++++++++++++++++++------------ 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/kaspa.pyi b/kaspa.pyi index f2fe17b5..c6497827 100644 --- a/kaspa.pyi +++ b/kaspa.pyi @@ -1112,6 +1112,10 @@ class PrivateKeyGenerator: Exception: If derivation fails. """ +@typing.final +class PsktCustomError(builtins.Exception): + ... + @typing.final class PublicKey: r""" diff --git a/src/lib.rs b/src/lib.rs index 0936f625..9ac2c03c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,8 +13,15 @@ define_stub_info_gatherer!(stub_info); #[pymodule] fn kaspa(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { + // Init logging bridge pyo3_log::init(); + // Create/register exceptions submodule + let exceptions = PyModule::new(py, "exceptions")?; + m.add_submodule(&exceptions)?; + + // Register classes & functions + m.add_class::()?; m.add_class::()?; @@ -159,7 +166,7 @@ fn kaspa(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add( "PsktCustomError", - py.get_type::(), + py.get_type::(), )?; m.add( "PsktStateError", diff --git a/src/wallet/pskt/error.rs b/src/wallet/pskt/error.rs index 33e1ed60..1457a067 100644 --- a/src/wallet/pskt/error.rs +++ b/src/wallet/pskt/error.rs @@ -1,25 +1,40 @@ use kaspa_wallet_pskt::wasm::error::Error; -use pyo3::create_exception; +use pyo3::{PyErrArguments, create_exception}; use pyo3::exceptions::PyException; use pyo3::prelude::*; +use pyo3_stub_gen::derive::gen_stub_pyclass; pub struct PyPsktError(pub Error); -create_exception!("kaspa", PsktCustomError, PyException); -create_exception!("kaspa", PsktStateError, PyException); -create_exception!("kaspa", PsktExpectedStateError, PyException); -create_exception!("kaspa", PsktCtorError, PyException); -create_exception!("kaspa", PsktInvalidPayloadError, PyException); -create_exception!("kaspa", PsktTxNotFinalizedError, PyException); -create_exception!("kaspa", PsktCreateNotAllowedError, PyException); -create_exception!("kaspa", PsktNotInitializedError, PyException); -create_exception!("kaspa", PsktConsensusClientError, PyException); -create_exception!("kaspa", PsktError, PyException); +#[gen_stub_pyclass] +#[pyclass(name = "PsktCustomError", extends = PyException)] +pub struct PyPsktCustomError; + +impl PyPsktCustomError { + pub fn new_err(args: A) -> PyErr + where + A: PyErrArguments + Send + Sync + 'static, + { + PyErr::new::(args) + } +} + +// #[gen_stub_pyclass] +// create_exception!("kaspa.exceptions", PsktCustomError, PyException); +create_exception!("kaspa.exceptions", PsktStateError, PyException); +create_exception!("kaspa.exceptions", PsktExpectedStateError, PyException); +create_exception!("kaspa.exceptions", PsktCtorError, PyException); +create_exception!("kaspa.exceptions", PsktInvalidPayloadError, PyException); +create_exception!("kaspa.exceptions", PsktTxNotFinalizedError, PyException); +create_exception!("kaspa.exceptions", PsktCreateNotAllowedError, PyException); +create_exception!("kaspa.exceptions", PsktNotInitializedError, PyException); +create_exception!("kaspa.exceptions", PsktConsensusClientError, PyException); +create_exception!("kaspa.exceptions", PsktError, PyException); impl From for PyErr { fn from(value: PyPsktError) -> Self { match value.0 { - Error::Custom(msg) => PsktCustomError::new_err(msg), + Error::Custom(msg) => PyPsktCustomError::new_err(msg), Error::State(msg) => PsktStateError::new_err(msg), Error::ExpectedState(msg) => PsktExpectedStateError::new_err(msg), Error::Ctor(msg) => PsktCtorError::new_err(msg), From 67fc95e1f5f28fc34aebb916d6a9fc891a676095 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 24 Jan 2026 09:05:40 -0500 Subject: [PATCH 05/15] Macro for custom exceptions --- kaspa.pyi | 11 +++-- src/lib.rs | 18 ++++---- src/macros.rs | 19 ++++++++ src/wallet/pskt/error.rs | 98 +++++++++++++++++++++++++++------------- 4 files changed, 102 insertions(+), 44 deletions(-) diff --git a/kaspa.pyi b/kaspa.pyi index c6497827..7afc6fe0 100644 --- a/kaspa.pyi +++ b/kaspa.pyi @@ -1112,10 +1112,6 @@ class PrivateKeyGenerator: Exception: If derivation fails. """ -@typing.final -class PsktCustomError(builtins.Exception): - ... - @typing.final class PublicKey: r""" @@ -1458,6 +1454,13 @@ class PublicKeyGenerator: str: The generator info string. """ +@typing.final +class PyPsktCustomError(builtins.Exception): + r""" + Custom PSKT Error + """ + ... + @typing.final class Resolver: r""" diff --git a/src/lib.rs b/src/lib.rs index 9ac2c03c..de9b5c7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -170,37 +170,37 @@ fn kaspa(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { )?; m.add( "PsktStateError", - py.get_type::(), + py.get_type::(), )?; m.add( "PsktExpectedStateError", - py.get_type::(), + py.get_type::(), )?; m.add( "PsktCtorError", - py.get_type::(), + py.get_type::(), )?; m.add( "PsktInvalidPayloadError", - py.get_type::(), + py.get_type::(), )?; m.add( "PsktTxNotFinalizedError", - py.get_type::(), + py.get_type::(), )?; m.add( "PsktCreateNotAllowedError", - py.get_type::(), + py.get_type::(), )?; m.add( "PsktNotInitializedError", - py.get_type::(), + py.get_type::(), )?; m.add( "PsktConsensusClientError", - py.get_type::(), + py.get_type::(), )?; - m.add("PsktError", py.get_type::())?; + m.add("PsktError", py.get_type::())?; m.add_class::()?; diff --git a/src/macros.rs b/src/macros.rs index 87571440..eda4ceac 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -61,3 +61,22 @@ macro_rules! wrap_unit_enum_for_py { // } }; } + +#[macro_export] +macro_rules! create_py_exception { + ($(#[$meta:meta])* $name:ident, $py_name:literal) => { + $(#[$meta])* + #[gen_stub_pyclass] + #[pyclass(name = $py_name, extends = PyException)] + pub struct $name; + + impl $name { + pub fn new_err(args: A) -> PyErr + where + A: PyErrArguments + Send + Sync + 'static, + { + PyErr::new::(args) + } + } + }; +} \ No newline at end of file diff --git a/src/wallet/pskt/error.rs b/src/wallet/pskt/error.rs index 1457a067..c168480f 100644 --- a/src/wallet/pskt/error.rs +++ b/src/wallet/pskt/error.rs @@ -1,53 +1,89 @@ use kaspa_wallet_pskt::wasm::error::Error; -use pyo3::{PyErrArguments, create_exception}; +use pyo3::PyErrArguments; use pyo3::exceptions::PyException; use pyo3::prelude::*; use pyo3_stub_gen::derive::gen_stub_pyclass; pub struct PyPsktError(pub Error); -#[gen_stub_pyclass] -#[pyclass(name = "PsktCustomError", extends = PyException)] -pub struct PyPsktCustomError; +crate::create_py_exception!( + /// Custom PSKT Error + PyPsktCustomError, "PsktCustomError" +); -impl PyPsktCustomError { - pub fn new_err(args: A) -> PyErr - where - A: PyErrArguments + Send + Sync + 'static, - { - PyErr::new::(args) - } -} +crate::create_py_exception!( + /// PSKT State Error + PyPsktStateError, "PsktStateError" +); + +crate::create_py_exception!( + /// PSKT Expected State Error + PyPsktExpectedStateError, "PsktExpectedStateError" +); + +crate::create_py_exception!( + /// PSKT Constructor Error + PyPsktCtorError, "PsktCtorError" +); + +crate::create_py_exception!( + /// PSKT Invalid Payload Error + PyPsktInvalidPayloadError, "PsktInvalidPayloadError" +); + +crate::create_py_exception!( + /// PSKT Tx Not Finalized Error + PyPsktTxNotFinalizedError, "PsktTxNotFinalizedError" +); + +crate::create_py_exception!( + /// PSKT Creation Not Allowed Error + PyPsktCreateNotAllowedError, "PsktCreateNotAllowedError" +); + +crate::create_py_exception!( + /// PSKT Not Initialized Error + PyPsktNotInitializedError, "PsktNotInitializedError" +); + +crate::create_py_exception!( + /// PSKT Consensus Client Error + PyPsktConsensusClientError, "PsktConsensusClientError" +); + +crate::create_py_exception!( + /// PSKT Error + PyPsktError, "PsktError" +); -// #[gen_stub_pyclass] // create_exception!("kaspa.exceptions", PsktCustomError, PyException); -create_exception!("kaspa.exceptions", PsktStateError, PyException); -create_exception!("kaspa.exceptions", PsktExpectedStateError, PyException); -create_exception!("kaspa.exceptions", PsktCtorError, PyException); -create_exception!("kaspa.exceptions", PsktInvalidPayloadError, PyException); -create_exception!("kaspa.exceptions", PsktTxNotFinalizedError, PyException); -create_exception!("kaspa.exceptions", PsktCreateNotAllowedError, PyException); -create_exception!("kaspa.exceptions", PsktNotInitializedError, PyException); -create_exception!("kaspa.exceptions", PsktConsensusClientError, PyException); -create_exception!("kaspa.exceptions", PsktError, PyException); +// create_exception!("kaspa.exceptions", PsktStateError, PyException); +// create_exception!("kaspa.exceptions", PsktExpectedStateError, PyException); +// create_exception!("kaspa.exceptions", PsktCtorError, PyException); +// create_exception!("kaspa.exceptions", PsktInvalidPayloadError, PyException); +// create_exception!("kaspa.exceptions", PsktTxNotFinalizedError, PyException); +// create_exception!("kaspa.exceptions", PsktCreateNotAllowedError, PyException); +// create_exception!("kaspa.exceptions", PsktNotInitializedError, PyException); +// create_exception!("kaspa.exceptions", PsktConsensusClientError, PyException); +// create_exception!("kaspa.exceptions", PsktError, PyException); impl From for PyErr { fn from(value: PyPsktError) -> Self { match value.0 { Error::Custom(msg) => PyPsktCustomError::new_err(msg), - Error::State(msg) => PsktStateError::new_err(msg), - Error::ExpectedState(msg) => PsktExpectedStateError::new_err(msg), - Error::Ctor(msg) => PsktCtorError::new_err(msg), - Error::InvalidPayload => PsktInvalidPayloadError::new_err("Invalid payload"), - Error::TxNotFinalized(inner) => PsktTxNotFinalizedError::new_err(inner.to_string()), - Error::CreateNotAllowed => PsktCreateNotAllowedError::new_err( + Error::State(msg) => PyPsktStateError::new_err(msg), + Error::ExpectedState(msg) => PyPsktExpectedStateError::new_err(msg), + Error::Ctor(msg) => PyPsktCtorError::new_err(msg), + Error::InvalidPayload => PyPsktInvalidPayloadError::new_err("Invalid payload"), + Error::TxNotFinalized(inner) => PyPsktTxNotFinalizedError::new_err(inner.to_string()), + Error::CreateNotAllowed => PyPsktCreateNotAllowedError::new_err( "Create state is not allowed for PSKT initialized from transaction or a payload", ), - Error::NotInitialized => PsktNotInitializedError::new_err( + Error::NotInitialized => PyPsktNotInitializedError::new_err( "PSKT must be initialized with a payload or CREATE role", ), - Error::ConsensusClient(inner) => PsktConsensusClientError::new_err(inner.to_string()), - Error::Pskt(inner) => PsktError::new_err(inner.to_string()), + Error::ConsensusClient(inner) => PyPsktConsensusClientError::new_err(inner.to_string()), + Error::Pskt(inner) => PyPsktError::new_err(inner.to_string()), _ => PyException::new_err("Unhandled error type"), } } From 4aa007ef664446bb0831c4f1b3d1b2a55255cd74 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 24 Jan 2026 09:25:01 -0500 Subject: [PATCH 06/15] correctly add custom exceptions to exceptions submodule --- src/lib.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index de9b5c7c..b30e1f0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,43 +164,46 @@ fn kaspa(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add( + exceptions.add( "PsktCustomError", py.get_type::(), )?; - m.add( + exceptions.add( "PsktStateError", py.get_type::(), )?; - m.add( + exceptions.add( "PsktExpectedStateError", py.get_type::(), )?; - m.add( + exceptions.add( "PsktCtorError", py.get_type::(), )?; - m.add( + exceptions.add( "PsktInvalidPayloadError", py.get_type::(), )?; - m.add( + exceptions.add( "PsktTxNotFinalizedError", py.get_type::(), )?; - m.add( + exceptions.add( "PsktCreateNotAllowedError", py.get_type::(), )?; - m.add( + exceptions.add( "PsktNotInitializedError", py.get_type::(), )?; - m.add( + exceptions.add( "PsktConsensusClientError", py.get_type::(), )?; - m.add("PsktError", py.get_type::())?; + exceptions.add( + "PsktError", + py.get_type::(), + )?; m.add_class::()?; From 4e230ba2a9bf3e9c1f824ffc158bad6152553d5c Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 24 Jan 2026 09:25:59 -0500 Subject: [PATCH 07/15] lints --- kaspa.pyi | 63 +++++++++++++++++++++++++++++++++++ src/macros.rs | 4 +-- src/wallet/pskt/error.rs | 21 +++--------- src/wallet/pskt/mod.rs | 72 +++++++++++++++++++--------------------- 4 files changed, 105 insertions(+), 55 deletions(-) diff --git a/kaspa.pyi b/kaspa.pyi index 7afc6fe0..3a13b66a 100644 --- a/kaspa.pyi +++ b/kaspa.pyi @@ -1454,6 +1454,27 @@ class PublicKeyGenerator: str: The generator info string. """ +@typing.final +class PyPsktConsensusClientError(builtins.Exception): + r""" + PSKT Consensus Client Error + """ + ... + +@typing.final +class PyPsktCreateNotAllowedError(builtins.Exception): + r""" + PSKT Creation Not Allowed Error + """ + ... + +@typing.final +class PyPsktCtorError(builtins.Exception): + r""" + PSKT Constructor Error + """ + ... + @typing.final class PyPsktCustomError(builtins.Exception): r""" @@ -1461,6 +1482,48 @@ class PyPsktCustomError(builtins.Exception): """ ... +@typing.final +class PyPsktError(builtins.Exception): + r""" + PSKT Error + """ + ... + +@typing.final +class PyPsktExpectedStateError(builtins.Exception): + r""" + PSKT Expected State Error + """ + ... + +@typing.final +class PyPsktInvalidPayloadError(builtins.Exception): + r""" + PSKT Invalid Payload Error + """ + ... + +@typing.final +class PyPsktNotInitializedError(builtins.Exception): + r""" + PSKT Not Initialized Error + """ + ... + +@typing.final +class PyPsktStateError(builtins.Exception): + r""" + PSKT State Error + """ + ... + +@typing.final +class PyPsktTxNotFinalizedError(builtins.Exception): + r""" + PSKT Tx Not Finalized Error + """ + ... + @typing.final class Resolver: r""" diff --git a/src/macros.rs b/src/macros.rs index eda4ceac..b800aa5b 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -69,7 +69,7 @@ macro_rules! create_py_exception { #[gen_stub_pyclass] #[pyclass(name = $py_name, extends = PyException)] pub struct $name; - + impl $name { pub fn new_err(args: A) -> PyErr where @@ -79,4 +79,4 @@ macro_rules! create_py_exception { } } }; -} \ No newline at end of file +} diff --git a/src/wallet/pskt/error.rs b/src/wallet/pskt/error.rs index c168480f..ad0fc7c1 100644 --- a/src/wallet/pskt/error.rs +++ b/src/wallet/pskt/error.rs @@ -4,7 +4,7 @@ use pyo3::exceptions::PyException; use pyo3::prelude::*; use pyo3_stub_gen::derive::gen_stub_pyclass; -pub struct PyPsktError(pub Error); +pub struct PsktError(pub Error); crate::create_py_exception!( /// Custom PSKT Error @@ -56,19 +56,8 @@ crate::create_py_exception!( PyPsktError, "PsktError" ); -// create_exception!("kaspa.exceptions", PsktCustomError, PyException); -// create_exception!("kaspa.exceptions", PsktStateError, PyException); -// create_exception!("kaspa.exceptions", PsktExpectedStateError, PyException); -// create_exception!("kaspa.exceptions", PsktCtorError, PyException); -// create_exception!("kaspa.exceptions", PsktInvalidPayloadError, PyException); -// create_exception!("kaspa.exceptions", PsktTxNotFinalizedError, PyException); -// create_exception!("kaspa.exceptions", PsktCreateNotAllowedError, PyException); -// create_exception!("kaspa.exceptions", PsktNotInitializedError, PyException); -// create_exception!("kaspa.exceptions", PsktConsensusClientError, PyException); -// create_exception!("kaspa.exceptions", PsktError, PyException); - -impl From for PyErr { - fn from(value: PyPsktError) -> Self { +impl From for PyErr { + fn from(value: PsktError) -> Self { match value.0 { Error::Custom(msg) => PyPsktCustomError::new_err(msg), Error::State(msg) => PyPsktStateError::new_err(msg), @@ -89,8 +78,8 @@ impl From for PyErr { } } -impl From for PyPsktError { +impl From for PsktError { fn from(value: Error) -> Self { - PyPsktError(value) + PsktError(value) } } diff --git a/src/wallet/pskt/mod.rs b/src/wallet/pskt/mod.rs index 830ed387..8a70aa96 100644 --- a/src/wallet/pskt/mod.rs +++ b/src/wallet/pskt/mod.rs @@ -5,7 +5,7 @@ use crate::consensus::client::output::PyTransactionOutput; use crate::consensus::client::transaction::PyTransaction; use crate::consensus::core::network::PyNetworkId; use crate::consensus::core::tx::TransactionId; -use error::PyPsktError; +use error::PsktError; use kaspa_consensus_client::{Transaction, TransactionInput, TransactionOutput}; use kaspa_consensus_core::network::NetworkType; use kaspa_wallet_pskt::pskt::Input; @@ -89,9 +89,9 @@ impl PyPSKT { let state = match self.take() { State::NoOp(inner) => match inner { None => State::Creator(PSKT::default()), - Some(_) => Err(PyPsktError(Error::CreateNotAllowed))?, + Some(_) => Err(PsktError(Error::CreateNotAllowed))?, }, - state => Err(PyPsktError(Error::state(state)))?, + state => Err(PsktError(Error::state(state)))?, }; self.replace(state) @@ -101,10 +101,10 @@ impl PyPSKT { pub fn to_constructor(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Constructor(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + State::Constructor(inner.ok_or(PsktError(Error::NotInitialized))?.into()) } State::Creator(pskt) => State::Constructor(pskt.constructor()), - state => Err(PyPsktError(Error::state(state)))?, + state => Err(PsktError(Error::state(state)))?, }; self.replace(state) @@ -114,10 +114,10 @@ impl PyPSKT { pub fn to_updater(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Updater(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + State::Updater(inner.ok_or(PsktError(Error::NotInitialized))?.into()) } State::Constructor(constructor) => State::Updater(constructor.updater()), - state => Err(PyPsktError(Error::state(state)))?, + state => Err(PsktError(Error::state(state)))?, }; self.replace(state) @@ -127,12 +127,12 @@ impl PyPSKT { pub fn to_signer(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Signer(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + State::Signer(inner.ok_or(PsktError(Error::NotInitialized))?.into()) } State::Constructor(pskt) => State::Signer(pskt.signer()), State::Updater(pskt) => State::Signer(pskt.signer()), State::Combiner(pskt) => State::Signer(pskt.signer()), - state => Err(PyPsktError(Error::state(state)))?, + state => Err(PsktError(Error::state(state)))?, }; self.replace(state) @@ -142,12 +142,12 @@ impl PyPSKT { pub fn to_combiner(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Combiner(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + State::Combiner(inner.ok_or(PsktError(Error::NotInitialized))?.into()) } State::Constructor(pskt) => State::Combiner(pskt.combiner()), State::Updater(pskt) => State::Combiner(pskt.combiner()), State::Signer(pskt) => State::Combiner(pskt.combiner()), - state => Err(PyPsktError(Error::state(state)))?, + state => Err(PsktError(Error::state(state)))?, }; self.replace(state) @@ -157,10 +157,10 @@ impl PyPSKT { pub fn to_finalizer(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Finalizer(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + State::Finalizer(inner.ok_or(PsktError(Error::NotInitialized))?.into()) } State::Combiner(pskt) => State::Finalizer(pskt.finalizer()), - state => Err(PyPsktError(Error::state(state)))?, + state => Err(PsktError(Error::state(state)))?, }; self.replace(state) @@ -170,12 +170,12 @@ impl PyPSKT { pub fn to_extractor(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Extractor(inner.ok_or(PyPsktError(Error::NotInitialized))?.into()) + State::Extractor(inner.ok_or(PsktError(Error::NotInitialized))?.into()) } State::Finalizer(pskt) => { - State::Extractor(pskt.extractor().map_err(Error::from).map_err(PyPsktError)?) + State::Extractor(pskt.extractor().map_err(Error::from).map_err(PsktError)?) } - state => Err(PyPsktError(Error::state(state)))?, + state => Err(PsktError(Error::state(state)))?, }; self.replace(state) @@ -184,7 +184,7 @@ impl PyPSKT { pub fn fallback_lock_time(&self, lock_time: u64) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.fallback_lock_time(lock_time)), - _ => Err(PyPsktError(Error::expected_state("Creator")))?, + _ => Err(PsktError(Error::expected_state("Creator")))?, }; self.replace(state) @@ -193,7 +193,7 @@ impl PyPSKT { pub fn inputs_modifiable(&self) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.inputs_modifiable()), - _ => Err(PyPsktError(Error::expected_state("Creator")))?, + _ => Err(PsktError(Error::expected_state("Creator")))?, }; self.replace(state) @@ -202,7 +202,7 @@ impl PyPSKT { pub fn outputs_modifiable(&self) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.outputs_modifiable()), - _ => Err(PyPsktError(Error::expected_state("Creator")))?, + _ => Err(PsktError(Error::expected_state("Creator")))?, }; self.replace(state) @@ -211,7 +211,7 @@ impl PyPSKT { pub fn no_more_inputs(&self) -> PyResult { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_inputs()), - _ => Err(PyPsktError(Error::expected_state("Constructor")))?, + _ => Err(PsktError(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -220,7 +220,7 @@ impl PyPSKT { pub fn no_more_outputs(&self) -> PyResult { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_outputs()), - _ => Err(PyPsktError(Error::expected_state("Constructor")))?, + _ => Err(PsktError(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -234,20 +234,20 @@ impl PyPSKT { let input = TransactionInput::from(input); let mut input: Input = input .try_into() - .map_err(|err| PyPsktError(Error::from(err)))?; + .map_err(|err| PsktError(Error::from(err)))?; // let redeem_script = js_sys::Reflect::get(&obj, &"redeemScript".into()) // .expect("Missing redeemscript field") // .as_string() // .expect("redeemscript must be a string"); input.redeem_script = Some(hex::decode(data).map_err(|e| { - PyPsktError(Error::custom(format!( + PsktError(Error::custom(format!( "Redeem script is not a hex string: {}", e ))) })?); let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.input(input)), - _ => Err(PyPsktError(Error::expected_state("Constructor")))?, + _ => Err(PsktError(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -260,10 +260,10 @@ impl PyPSKT { pskt.input( input .try_into() - .map_err(|err| PyPsktError(Error::from(err)))?, + .map_err(|err| PsktError(Error::from(err)))?, ), ), - _ => Err(PyPsktError(Error::expected_state("Constructor")))?, + _ => Err(PsktError(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -276,10 +276,10 @@ impl PyPSKT { pskt.output( output .try_into() - .map_err(|err| PyPsktError(Error::from(err)))?, + .map_err(|err| PsktError(Error::from(err)))?, ), ), - _ => Err(PyPsktError(Error::expected_state("Constructor")))?, + _ => Err(PsktError(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -289,9 +289,9 @@ impl PyPSKT { let state = match self.take() { State::Updater(pskt) => State::Updater( pskt.set_sequence(n, input_index) - .map_err(|err| PyPsktError(Error::from(err)))?, + .map_err(|err| PsktError(Error::from(err)))?, ), - _ => Err(PyPsktError(Error::expected_state("Updater")))?, + _ => Err(PsktError(Error::expected_state("Updater")))?, }; self.replace(state) @@ -301,7 +301,7 @@ impl PyPSKT { let state = self.state(); match state.as_ref().unwrap() { State::Signer(pskt) => Ok(pskt.calculate_id().into()), - _ => Err(PyPsktError(Error::expected_state("Signer")))?, + _ => Err(PsktError(Error::expected_state("Signer")))?, } } @@ -327,7 +327,7 @@ impl PyPSKT { State::Finalizer(pskt) => { for input in pskt.inputs.iter() { if input.redeem_script.is_some() { - return Err(PyPsktError(Error::custom( + return Err(PsktError(Error::custom( "Mass calculation is not supported for inputs with redeem scripts", )) .into()); @@ -338,19 +338,17 @@ impl PyPSKT { Ok(vec![vec![0u8, 65]; inner.inputs.len()]) }) .map_err(|e| { - PyPsktError(Error::custom(format!("Failed to finalize PSKT: {e}"))) + PsktError(Error::custom(format!("Failed to finalize PSKT: {e}"))) })?; pskt.extractor() - .map_err(|err| PyPsktError(Error::TxNotFinalized(err)))? + .map_err(|err| PsktError(Error::TxNotFinalized(err)))? } _ => panic!("Finalizer state is not valid"), } }; let tx = extractor .extract_tx_unchecked(&NetworkType::from(network_type).into()) - .map_err(|e| { - PyPsktError(Error::custom(format!("Failed to extract transaction: {e}"))) - })?; + .map_err(|e| PsktError(Error::custom(format!("Failed to extract transaction: {e}"))))?; Ok(tx.tx.mass()) } } From 7a795a380851a20281020b6da878205682a6e639 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 24 Jan 2026 09:30:32 -0500 Subject: [PATCH 08/15] Move PsktError to use from --- src/wallet/pskt/error.rs | 5 ++- src/wallet/pskt/mod.rs | 76 +++++++++++++++++++++------------------- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/wallet/pskt/error.rs b/src/wallet/pskt/error.rs index ad0fc7c1..581321bd 100644 --- a/src/wallet/pskt/error.rs +++ b/src/wallet/pskt/error.rs @@ -4,7 +4,7 @@ use pyo3::exceptions::PyException; use pyo3::prelude::*; use pyo3_stub_gen::derive::gen_stub_pyclass; -pub struct PsktError(pub Error); +// Custom Python Exceptions crate::create_py_exception!( /// Custom PSKT Error @@ -56,6 +56,9 @@ crate::create_py_exception!( PyPsktError, "PsktError" ); +// Rust error that maps to Python error +pub struct PsktError(Error); + impl From for PyErr { fn from(value: PsktError) -> Self { match value.0 { diff --git a/src/wallet/pskt/mod.rs b/src/wallet/pskt/mod.rs index 8a70aa96..ef572935 100644 --- a/src/wallet/pskt/mod.rs +++ b/src/wallet/pskt/mod.rs @@ -89,9 +89,9 @@ impl PyPSKT { let state = match self.take() { State::NoOp(inner) => match inner { None => State::Creator(PSKT::default()), - Some(_) => Err(PsktError(Error::CreateNotAllowed))?, + Some(_) => Err(PsktError::from(Error::CreateNotAllowed))?, }, - state => Err(PsktError(Error::state(state)))?, + state => Err(PsktError::from(Error::state(state)))?, }; self.replace(state) @@ -101,10 +101,10 @@ impl PyPSKT { pub fn to_constructor(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Constructor(inner.ok_or(PsktError(Error::NotInitialized))?.into()) + State::Constructor(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) } State::Creator(pskt) => State::Constructor(pskt.constructor()), - state => Err(PsktError(Error::state(state)))?, + state => Err(PsktError::from(Error::state(state)))?, }; self.replace(state) @@ -114,10 +114,10 @@ impl PyPSKT { pub fn to_updater(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Updater(inner.ok_or(PsktError(Error::NotInitialized))?.into()) + State::Updater(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) } State::Constructor(constructor) => State::Updater(constructor.updater()), - state => Err(PsktError(Error::state(state)))?, + state => Err(PsktError::from(Error::state(state)))?, }; self.replace(state) @@ -127,12 +127,12 @@ impl PyPSKT { pub fn to_signer(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Signer(inner.ok_or(PsktError(Error::NotInitialized))?.into()) + State::Signer(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) } State::Constructor(pskt) => State::Signer(pskt.signer()), State::Updater(pskt) => State::Signer(pskt.signer()), State::Combiner(pskt) => State::Signer(pskt.signer()), - state => Err(PsktError(Error::state(state)))?, + state => Err(PsktError::from(Error::state(state)))?, }; self.replace(state) @@ -142,12 +142,12 @@ impl PyPSKT { pub fn to_combiner(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Combiner(inner.ok_or(PsktError(Error::NotInitialized))?.into()) + State::Combiner(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) } State::Constructor(pskt) => State::Combiner(pskt.combiner()), State::Updater(pskt) => State::Combiner(pskt.combiner()), State::Signer(pskt) => State::Combiner(pskt.combiner()), - state => Err(PsktError(Error::state(state)))?, + state => Err(PsktError::from(Error::state(state)))?, }; self.replace(state) @@ -157,10 +157,10 @@ impl PyPSKT { pub fn to_finalizer(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Finalizer(inner.ok_or(PsktError(Error::NotInitialized))?.into()) + State::Finalizer(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) } State::Combiner(pskt) => State::Finalizer(pskt.finalizer()), - state => Err(PsktError(Error::state(state)))?, + state => Err(PsktError::from(Error::state(state)))?, }; self.replace(state) @@ -170,12 +170,14 @@ impl PyPSKT { pub fn to_extractor(&self) -> PyResult { let state = match self.take() { State::NoOp(inner) => { - State::Extractor(inner.ok_or(PsktError(Error::NotInitialized))?.into()) + State::Extractor(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) } - State::Finalizer(pskt) => { - State::Extractor(pskt.extractor().map_err(Error::from).map_err(PsktError)?) - } - state => Err(PsktError(Error::state(state)))?, + State::Finalizer(pskt) => State::Extractor( + pskt.extractor() + .map_err(Error::from) + .map_err(PsktError::from)?, + ), + state => Err(PsktError::from(Error::state(state)))?, }; self.replace(state) @@ -184,7 +186,7 @@ impl PyPSKT { pub fn fallback_lock_time(&self, lock_time: u64) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.fallback_lock_time(lock_time)), - _ => Err(PsktError(Error::expected_state("Creator")))?, + _ => Err(PsktError::from(Error::expected_state("Creator")))?, }; self.replace(state) @@ -193,7 +195,7 @@ impl PyPSKT { pub fn inputs_modifiable(&self) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.inputs_modifiable()), - _ => Err(PsktError(Error::expected_state("Creator")))?, + _ => Err(PsktError::from(Error::expected_state("Creator")))?, }; self.replace(state) @@ -202,7 +204,7 @@ impl PyPSKT { pub fn outputs_modifiable(&self) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.outputs_modifiable()), - _ => Err(PsktError(Error::expected_state("Creator")))?, + _ => Err(PsktError::from(Error::expected_state("Creator")))?, }; self.replace(state) @@ -211,7 +213,7 @@ impl PyPSKT { pub fn no_more_inputs(&self) -> PyResult { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_inputs()), - _ => Err(PsktError(Error::expected_state("Constructor")))?, + _ => Err(PsktError::from(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -220,7 +222,7 @@ impl PyPSKT { pub fn no_more_outputs(&self) -> PyResult { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_outputs()), - _ => Err(PsktError(Error::expected_state("Constructor")))?, + _ => Err(PsktError::from(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -234,20 +236,20 @@ impl PyPSKT { let input = TransactionInput::from(input); let mut input: Input = input .try_into() - .map_err(|err| PsktError(Error::from(err)))?; + .map_err(|err| PsktError::from(Error::from(err)))?; // let redeem_script = js_sys::Reflect::get(&obj, &"redeemScript".into()) // .expect("Missing redeemscript field") // .as_string() // .expect("redeemscript must be a string"); input.redeem_script = Some(hex::decode(data).map_err(|e| { - PsktError(Error::custom(format!( + PsktError::from(Error::custom(format!( "Redeem script is not a hex string: {}", e ))) })?); let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.input(input)), - _ => Err(PsktError(Error::expected_state("Constructor")))?, + _ => Err(PsktError::from(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -260,10 +262,10 @@ impl PyPSKT { pskt.input( input .try_into() - .map_err(|err| PsktError(Error::from(err)))?, + .map_err(|err| PsktError::from(Error::from(err)))?, ), ), - _ => Err(PsktError(Error::expected_state("Constructor")))?, + _ => Err(PsktError::from(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -276,10 +278,10 @@ impl PyPSKT { pskt.output( output .try_into() - .map_err(|err| PsktError(Error::from(err)))?, + .map_err(|err| PsktError::from(Error::from(err)))?, ), ), - _ => Err(PsktError(Error::expected_state("Constructor")))?, + _ => Err(PsktError::from(Error::expected_state("Constructor")))?, }; self.replace(state) @@ -289,9 +291,9 @@ impl PyPSKT { let state = match self.take() { State::Updater(pskt) => State::Updater( pskt.set_sequence(n, input_index) - .map_err(|err| PsktError(Error::from(err)))?, + .map_err(|err| PsktError::from(Error::from(err)))?, ), - _ => Err(PsktError(Error::expected_state("Updater")))?, + _ => Err(PsktError::from(Error::expected_state("Updater")))?, }; self.replace(state) @@ -301,7 +303,7 @@ impl PyPSKT { let state = self.state(); match state.as_ref().unwrap() { State::Signer(pskt) => Ok(pskt.calculate_id().into()), - _ => Err(PsktError(Error::expected_state("Signer")))?, + _ => Err(PsktError::from(Error::expected_state("Signer")))?, } } @@ -327,7 +329,7 @@ impl PyPSKT { State::Finalizer(pskt) => { for input in pskt.inputs.iter() { if input.redeem_script.is_some() { - return Err(PsktError(Error::custom( + return Err(PsktError::from(Error::custom( "Mass calculation is not supported for inputs with redeem scripts", )) .into()); @@ -338,17 +340,19 @@ impl PyPSKT { Ok(vec![vec![0u8, 65]; inner.inputs.len()]) }) .map_err(|e| { - PsktError(Error::custom(format!("Failed to finalize PSKT: {e}"))) + PsktError::from(Error::custom(format!("Failed to finalize PSKT: {e}"))) })?; pskt.extractor() - .map_err(|err| PsktError(Error::TxNotFinalized(err)))? + .map_err(|err| PsktError::from(Error::TxNotFinalized(err)))? } _ => panic!("Finalizer state is not valid"), } }; let tx = extractor .extract_tx_unchecked(&NetworkType::from(network_type).into()) - .map_err(|e| PsktError(Error::custom(format!("Failed to extract transaction: {e}"))))?; + .map_err(|e| { + PsktError::from(Error::custom(format!("Failed to extract transaction: {e}"))) + })?; Ok(tx.tx.mass()) } } From fd5d63a1cb61fcc19f707f110e560f5164613169 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 24 Jan 2026 09:34:57 -0500 Subject: [PATCH 09/15] leverage wasm error display trait --- src/wallet/pskt/error.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/wallet/pskt/error.rs b/src/wallet/pskt/error.rs index 581321bd..3bcd3cfe 100644 --- a/src/wallet/pskt/error.rs +++ b/src/wallet/pskt/error.rs @@ -66,14 +66,16 @@ impl From for PyErr { Error::State(msg) => PyPsktStateError::new_err(msg), Error::ExpectedState(msg) => PyPsktExpectedStateError::new_err(msg), Error::Ctor(msg) => PyPsktCtorError::new_err(msg), - Error::InvalidPayload => PyPsktInvalidPayloadError::new_err("Invalid payload"), + Error::InvalidPayload => { + PyPsktInvalidPayloadError::new_err(Error::InvalidPayload.to_string()) + } Error::TxNotFinalized(inner) => PyPsktTxNotFinalizedError::new_err(inner.to_string()), - Error::CreateNotAllowed => PyPsktCreateNotAllowedError::new_err( - "Create state is not allowed for PSKT initialized from transaction or a payload", - ), - Error::NotInitialized => PyPsktNotInitializedError::new_err( - "PSKT must be initialized with a payload or CREATE role", - ), + Error::CreateNotAllowed => { + PyPsktCreateNotAllowedError::new_err(Error::CreateNotAllowed.to_string()) + } + Error::NotInitialized => { + PyPsktNotInitializedError::new_err(Error::NotInitialized.to_string()) + } Error::ConsensusClient(inner) => PyPsktConsensusClientError::new_err(inner.to_string()), Error::Pskt(inner) => PyPsktError::new_err(inner.to_string()), _ => PyException::new_err("Unhandled error type"), From 20c42343ba742c77a34e8c5645835e4844b74ad6 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 24 Jan 2026 09:42:28 -0500 Subject: [PATCH 10/15] rename pskt error and wrapped error --- src/wallet/pskt/error.rs | 46 ++++++++-------- src/wallet/pskt/mod.rs | 114 ++++++++++++++++++++++----------------- 2 files changed, 90 insertions(+), 70 deletions(-) diff --git a/src/wallet/pskt/error.rs b/src/wallet/pskt/error.rs index 3bcd3cfe..c0a777f4 100644 --- a/src/wallet/pskt/error.rs +++ b/src/wallet/pskt/error.rs @@ -1,4 +1,4 @@ -use kaspa_wallet_pskt::wasm::error::Error; +use kaspa_wallet_pskt::wasm::error::Error as NativeError; use pyo3::PyErrArguments; use pyo3::exceptions::PyException; use pyo3::prelude::*; @@ -56,35 +56,39 @@ crate::create_py_exception!( PyPsktError, "PsktError" ); -// Rust error that maps to Python error -pub struct PsktError(Error); +// Rust error internal custom Python exception over Rust/Python interface +pub struct Error(NativeError); -impl From for PyErr { - fn from(value: PsktError) -> Self { +impl From for PyErr { + fn from(value: Error) -> Self { match value.0 { - Error::Custom(msg) => PyPsktCustomError::new_err(msg), - Error::State(msg) => PyPsktStateError::new_err(msg), - Error::ExpectedState(msg) => PyPsktExpectedStateError::new_err(msg), - Error::Ctor(msg) => PyPsktCtorError::new_err(msg), - Error::InvalidPayload => { - PyPsktInvalidPayloadError::new_err(Error::InvalidPayload.to_string()) + NativeError::Custom(msg) => PyPsktCustomError::new_err(msg), + NativeError::State(msg) => PyPsktStateError::new_err(msg), + NativeError::ExpectedState(msg) => PyPsktExpectedStateError::new_err(msg), + NativeError::Ctor(msg) => PyPsktCtorError::new_err(msg), + NativeError::InvalidPayload => { + PyPsktInvalidPayloadError::new_err(NativeError::InvalidPayload.to_string()) + } + NativeError::TxNotFinalized(inner) => { + PyPsktTxNotFinalizedError::new_err(inner.to_string()) } - Error::TxNotFinalized(inner) => PyPsktTxNotFinalizedError::new_err(inner.to_string()), - Error::CreateNotAllowed => { - PyPsktCreateNotAllowedError::new_err(Error::CreateNotAllowed.to_string()) + NativeError::CreateNotAllowed => { + PyPsktCreateNotAllowedError::new_err(NativeError::CreateNotAllowed.to_string()) } - Error::NotInitialized => { - PyPsktNotInitializedError::new_err(Error::NotInitialized.to_string()) + NativeError::NotInitialized => { + PyPsktNotInitializedError::new_err(NativeError::NotInitialized.to_string()) } - Error::ConsensusClient(inner) => PyPsktConsensusClientError::new_err(inner.to_string()), - Error::Pskt(inner) => PyPsktError::new_err(inner.to_string()), + NativeError::ConsensusClient(inner) => { + PyPsktConsensusClientError::new_err(inner.to_string()) + } + NativeError::Pskt(inner) => PyPsktError::new_err(inner.to_string()), _ => PyException::new_err("Unhandled error type"), } } } -impl From for PsktError { - fn from(value: Error) -> Self { - PsktError(value) +impl From for Error { + fn from(value: NativeError) -> Self { + Error(value) } } diff --git a/src/wallet/pskt/mod.rs b/src/wallet/pskt/mod.rs index ef572935..391ac7bf 100644 --- a/src/wallet/pskt/mod.rs +++ b/src/wallet/pskt/mod.rs @@ -5,11 +5,11 @@ use crate::consensus::client::output::PyTransactionOutput; use crate::consensus::client::transaction::PyTransaction; use crate::consensus::core::network::PyNetworkId; use crate::consensus::core::tx::TransactionId; -use error::PsktError; +use error::Error; use kaspa_consensus_client::{Transaction, TransactionInput, TransactionOutput}; use kaspa_consensus_core::network::NetworkType; use kaspa_wallet_pskt::pskt::Input; -use kaspa_wallet_pskt::wasm::error::Error; +use kaspa_wallet_pskt::wasm::error::Error as NativeError; use kaspa_wallet_pskt::{ pskt::{Inner, PSKT}, role::*, @@ -89,9 +89,9 @@ impl PyPSKT { let state = match self.take() { State::NoOp(inner) => match inner { None => State::Creator(PSKT::default()), - Some(_) => Err(PsktError::from(Error::CreateNotAllowed))?, + Some(_) => Err(Error::from(NativeError::CreateNotAllowed))?, }, - state => Err(PsktError::from(Error::state(state)))?, + state => Err(Error::from(NativeError::state(state)))?, }; self.replace(state) @@ -100,11 +100,13 @@ impl PyPSKT { /// Change role to `CONSTRUCTOR` pub fn to_constructor(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => { - State::Constructor(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) - } + State::NoOp(inner) => State::Constructor( + inner + .ok_or(Error::from(NativeError::NotInitialized))? + .into(), + ), State::Creator(pskt) => State::Constructor(pskt.constructor()), - state => Err(PsktError::from(Error::state(state)))?, + state => Err(Error::from(NativeError::state(state)))?, }; self.replace(state) @@ -113,11 +115,13 @@ impl PyPSKT { /// Change role to `UPDATER` pub fn to_updater(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => { - State::Updater(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) - } + State::NoOp(inner) => State::Updater( + inner + .ok_or(Error::from(NativeError::NotInitialized))? + .into(), + ), State::Constructor(constructor) => State::Updater(constructor.updater()), - state => Err(PsktError::from(Error::state(state)))?, + state => Err(Error::from(NativeError::state(state)))?, }; self.replace(state) @@ -126,13 +130,15 @@ impl PyPSKT { /// Change role to `SIGNER` pub fn to_signer(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => { - State::Signer(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) - } + State::NoOp(inner) => State::Signer( + inner + .ok_or(Error::from(NativeError::NotInitialized))? + .into(), + ), State::Constructor(pskt) => State::Signer(pskt.signer()), State::Updater(pskt) => State::Signer(pskt.signer()), State::Combiner(pskt) => State::Signer(pskt.signer()), - state => Err(PsktError::from(Error::state(state)))?, + state => Err(Error::from(NativeError::state(state)))?, }; self.replace(state) @@ -141,13 +147,15 @@ impl PyPSKT { /// Change role to `COMBINER` pub fn to_combiner(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => { - State::Combiner(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) - } + State::NoOp(inner) => State::Combiner( + inner + .ok_or(Error::from(NativeError::NotInitialized))? + .into(), + ), State::Constructor(pskt) => State::Combiner(pskt.combiner()), State::Updater(pskt) => State::Combiner(pskt.combiner()), State::Signer(pskt) => State::Combiner(pskt.combiner()), - state => Err(PsktError::from(Error::state(state)))?, + state => Err(Error::from(NativeError::state(state)))?, }; self.replace(state) @@ -156,11 +164,13 @@ impl PyPSKT { /// Change role to `FINALIZER` pub fn to_finalizer(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => { - State::Finalizer(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) - } + State::NoOp(inner) => State::Finalizer( + inner + .ok_or(Error::from(NativeError::NotInitialized))? + .into(), + ), State::Combiner(pskt) => State::Finalizer(pskt.finalizer()), - state => Err(PsktError::from(Error::state(state)))?, + state => Err(Error::from(NativeError::state(state)))?, }; self.replace(state) @@ -169,15 +179,17 @@ impl PyPSKT { /// Change role to `EXTRACTOR` pub fn to_extractor(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => { - State::Extractor(inner.ok_or(PsktError::from(Error::NotInitialized))?.into()) - } + State::NoOp(inner) => State::Extractor( + inner + .ok_or(Error::from(NativeError::NotInitialized))? + .into(), + ), State::Finalizer(pskt) => State::Extractor( pskt.extractor() - .map_err(Error::from) - .map_err(PsktError::from)?, + .map_err(NativeError::from) + .map_err(Error::from)?, ), - state => Err(PsktError::from(Error::state(state)))?, + state => Err(Error::from(NativeError::state(state)))?, }; self.replace(state) @@ -186,7 +198,7 @@ impl PyPSKT { pub fn fallback_lock_time(&self, lock_time: u64) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.fallback_lock_time(lock_time)), - _ => Err(PsktError::from(Error::expected_state("Creator")))?, + _ => Err(Error::from(NativeError::expected_state("Creator")))?, }; self.replace(state) @@ -195,7 +207,7 @@ impl PyPSKT { pub fn inputs_modifiable(&self) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.inputs_modifiable()), - _ => Err(PsktError::from(Error::expected_state("Creator")))?, + _ => Err(Error::from(NativeError::expected_state("Creator")))?, }; self.replace(state) @@ -204,7 +216,7 @@ impl PyPSKT { pub fn outputs_modifiable(&self) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.outputs_modifiable()), - _ => Err(PsktError::from(Error::expected_state("Creator")))?, + _ => Err(Error::from(NativeError::expected_state("Creator")))?, }; self.replace(state) @@ -213,7 +225,7 @@ impl PyPSKT { pub fn no_more_inputs(&self) -> PyResult { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_inputs()), - _ => Err(PsktError::from(Error::expected_state("Constructor")))?, + _ => Err(Error::from(NativeError::expected_state("Constructor")))?, }; self.replace(state) @@ -222,7 +234,7 @@ impl PyPSKT { pub fn no_more_outputs(&self) -> PyResult { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_outputs()), - _ => Err(PsktError::from(Error::expected_state("Constructor")))?, + _ => Err(Error::from(NativeError::expected_state("Constructor")))?, }; self.replace(state) @@ -236,20 +248,20 @@ impl PyPSKT { let input = TransactionInput::from(input); let mut input: Input = input .try_into() - .map_err(|err| PsktError::from(Error::from(err)))?; + .map_err(|err| Error::from(NativeError::from(err)))?; // let redeem_script = js_sys::Reflect::get(&obj, &"redeemScript".into()) // .expect("Missing redeemscript field") // .as_string() // .expect("redeemscript must be a string"); input.redeem_script = Some(hex::decode(data).map_err(|e| { - PsktError::from(Error::custom(format!( + Error::from(NativeError::custom(format!( "Redeem script is not a hex string: {}", e ))) })?); let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.input(input)), - _ => Err(PsktError::from(Error::expected_state("Constructor")))?, + _ => Err(Error::from(NativeError::expected_state("Constructor")))?, }; self.replace(state) @@ -262,10 +274,10 @@ impl PyPSKT { pskt.input( input .try_into() - .map_err(|err| PsktError::from(Error::from(err)))?, + .map_err(|err| Error::from(NativeError::from(err)))?, ), ), - _ => Err(PsktError::from(Error::expected_state("Constructor")))?, + _ => Err(Error::from(NativeError::expected_state("Constructor")))?, }; self.replace(state) @@ -278,10 +290,10 @@ impl PyPSKT { pskt.output( output .try_into() - .map_err(|err| PsktError::from(Error::from(err)))?, + .map_err(|err| Error::from(NativeError::from(err)))?, ), ), - _ => Err(PsktError::from(Error::expected_state("Constructor")))?, + _ => Err(Error::from(NativeError::expected_state("Constructor")))?, }; self.replace(state) @@ -291,9 +303,9 @@ impl PyPSKT { let state = match self.take() { State::Updater(pskt) => State::Updater( pskt.set_sequence(n, input_index) - .map_err(|err| PsktError::from(Error::from(err)))?, + .map_err(|err| Error::from(NativeError::from(err)))?, ), - _ => Err(PsktError::from(Error::expected_state("Updater")))?, + _ => Err(Error::from(NativeError::expected_state("Updater")))?, }; self.replace(state) @@ -303,7 +315,7 @@ impl PyPSKT { let state = self.state(); match state.as_ref().unwrap() { State::Signer(pskt) => Ok(pskt.calculate_id().into()), - _ => Err(PsktError::from(Error::expected_state("Signer")))?, + _ => Err(Error::from(NativeError::expected_state("Signer")))?, } } @@ -329,7 +341,7 @@ impl PyPSKT { State::Finalizer(pskt) => { for input in pskt.inputs.iter() { if input.redeem_script.is_some() { - return Err(PsktError::from(Error::custom( + return Err(Error::from(NativeError::custom( "Mass calculation is not supported for inputs with redeem scripts", )) .into()); @@ -340,10 +352,12 @@ impl PyPSKT { Ok(vec![vec![0u8, 65]; inner.inputs.len()]) }) .map_err(|e| { - PsktError::from(Error::custom(format!("Failed to finalize PSKT: {e}"))) + Error::from(NativeError::custom(format!( + "Failed to finalize PSKT: {e}" + ))) })?; pskt.extractor() - .map_err(|err| PsktError::from(Error::TxNotFinalized(err)))? + .map_err(|err| Error::from(NativeError::TxNotFinalized(err)))? } _ => panic!("Finalizer state is not valid"), } @@ -351,7 +365,9 @@ impl PyPSKT { let tx = extractor .extract_tx_unchecked(&NetworkType::from(network_type).into()) .map_err(|e| { - PsktError::from(Error::custom(format!("Failed to extract transaction: {e}"))) + Error::from(NativeError::custom(format!( + "Failed to extract transaction: {e}" + ))) })?; Ok(tx.tx.mass()) } From 3877fe6016d3fc1716e35ee0dac005ae48388a6f Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 24 Jan 2026 10:05:55 -0500 Subject: [PATCH 11/15] comment --- src/wallet/pskt/error.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wallet/pskt/error.rs b/src/wallet/pskt/error.rs index c0a777f4..45b92e24 100644 --- a/src/wallet/pskt/error.rs +++ b/src/wallet/pskt/error.rs @@ -56,7 +56,9 @@ crate::create_py_exception!( PyPsktError, "PsktError" ); -// Rust error internal custom Python exception over Rust/Python interface +// Internal error type +// Wraps natively defined WASM Error +// Returns corresponding custom Python exception to python pub struct Error(NativeError); impl From for PyErr { From a060f32b47d08ff74cc259fe1882f13477c3cedb Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 24 Jan 2026 11:01:48 -0500 Subject: [PATCH 12/15] fix issue where custom exception cannot be instatiated in python --- src/macros.rs | 27 +++++++++++++++++++++------ src/wallet/pskt/error.rs | 1 - src/wallet/pskt/mod.rs | 16 ++-------------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index b800aa5b..925f841c 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -62,20 +62,35 @@ macro_rules! wrap_unit_enum_for_py { }; } +// PyO3 provides create_exception! macro. However we cannot use it. +// Because we need to use proc macro #[gen_stub_pyclass] to include the defined +// exception in the stub file. When using create_exception!, we cannot apply +// #[gen_stub_pyclass]. +// When PyO3 is able to generate stub files (currently experimental) +// this could likely be removed in favor of that approach. #[macro_export] macro_rules! create_py_exception { ($(#[$meta:meta])* $name:ident, $py_name:literal) => { $(#[$meta])* + #[allow(dead_code)] #[gen_stub_pyclass] #[pyclass(name = $py_name, extends = PyException)] - pub struct $name; + pub struct $name { + message: String, + } + + // This is required, otherwise PyO3 cannot initialize the Exception on Python side + #[pymethods] + impl $name { + #[new] + pub fn new(message: String) -> Self { + Self { message } + } + } impl $name { - pub fn new_err(args: A) -> PyErr - where - A: PyErrArguments + Send + Sync + 'static, - { - PyErr::new::(args) + pub fn new_err(message: impl Into) -> PyErr { + PyErr::new::(message.into()) } } }; diff --git a/src/wallet/pskt/error.rs b/src/wallet/pskt/error.rs index 45b92e24..d0cb1239 100644 --- a/src/wallet/pskt/error.rs +++ b/src/wallet/pskt/error.rs @@ -1,5 +1,4 @@ use kaspa_wallet_pskt::wasm::error::Error as NativeError; -use pyo3::PyErrArguments; use pyo3::exceptions::PyException; use pyo3::prelude::*; use pyo3_stub_gen::derive::gen_stub_pyclass; diff --git a/src/wallet/pskt/mod.rs b/src/wallet/pskt/mod.rs index 391ac7bf..dbac1aa0 100644 --- a/src/wallet/pskt/mod.rs +++ b/src/wallet/pskt/mod.rs @@ -49,7 +49,7 @@ impl PyPSKT { pub fn new(payload: Bound<'_, PyAny>) -> PyResult { let payload = if let Ok(p) = payload.extract::() { let inner = - serde_json::from_str(&p).map_err(|err| PyException::new_err(err.to_string()))?; + serde_json::from_str(&p).map_err(|_| Error::from(NativeError::InvalidPayload))?; Ok(PyPSKT::from(State::NoOp(Some(inner)))) } else if let Ok(py_tx) = payload.extract::() { let tx: Transaction = py_tx.into(); @@ -60,7 +60,7 @@ impl PyPSKT { } else if payload.is_none() { Ok(PyPSKT::from(State::Creator(PSKT::::default()))) } else { - Err(PyException::new_err("Invalid payload")) + Err(Error::from(NativeError::InvalidPayload)) }?; Ok(payload) @@ -249,10 +249,6 @@ impl PyPSKT { let mut input: Input = input .try_into() .map_err(|err| Error::from(NativeError::from(err)))?; - // let redeem_script = js_sys::Reflect::get(&obj, &"redeemScript".into()) - // .expect("Missing redeemscript field") - // .as_string() - // .expect("redeemscript must be a string"); input.redeem_script = Some(hex::decode(data).map_err(|e| { Error::from(NativeError::custom(format!( "Redeem script is not a hex string: {}", @@ -320,14 +316,6 @@ impl PyPSKT { } pub fn calculate_mass(&self, data: PyNetworkId) -> PyResult { - // let obj = js_sys::Object::from(data.clone()); - // let network_id = js_sys::Reflect::get(&obj, &"networkId".into()) - // .map_err(|_| Error::custom("networkId is missing"))? - // .as_string() - // .ok_or_else(|| Error::custom("networkId must be a string"))?; - - // let network_id = NetworkType::from_str(&network_id) - // .map_err(|e| Error::custom(format!("Invalid networkId: {}", e)))?; let network_type = data.get_network_type(); let cloned_pskt = self.clone(); From 9727fe96d007848f93b9ad5bc69680f345aabe22 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sun, 25 Jan 2026 07:19:18 -0500 Subject: [PATCH 13/15] bring in native error, rename wasm error --- src/wallet/pskt/mod.rs | 111 +++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/src/wallet/pskt/mod.rs b/src/wallet/pskt/mod.rs index dbac1aa0..b3116120 100644 --- a/src/wallet/pskt/mod.rs +++ b/src/wallet/pskt/mod.rs @@ -9,8 +9,9 @@ use error::Error; use kaspa_consensus_client::{Transaction, TransactionInput, TransactionOutput}; use kaspa_consensus_core::network::NetworkType; use kaspa_wallet_pskt::pskt::Input; -use kaspa_wallet_pskt::wasm::error::Error as NativeError; +use kaspa_wallet_pskt::wasm::error::Error as WasmError; use kaspa_wallet_pskt::{ + error::Error as NativeError, pskt::{Inner, PSKT}, role::*, wasm::pskt::State, @@ -46,24 +47,28 @@ impl PyPSKT { #[pymethods] impl PyPSKT { #[new] - pub fn new(payload: Bound<'_, PyAny>) -> PyResult { - let payload = if let Ok(p) = payload.extract::() { - let inner = - serde_json::from_str(&p).map_err(|_| Error::from(NativeError::InvalidPayload))?; - Ok(PyPSKT::from(State::NoOp(Some(inner)))) - } else if let Ok(py_tx) = payload.extract::() { - let tx: Transaction = py_tx.into(); - let inner: Inner = tx - .try_into() - .map_err(|_| PyException::new_err("Transaction to Inner failed"))?; - Ok(PyPSKT::from(State::NoOp(Some(inner)))) - } else if payload.is_none() { - Ok(PyPSKT::from(State::Creator(PSKT::::default()))) - } else { - Err(Error::from(NativeError::InvalidPayload)) - }?; - - Ok(payload) + #[pyo3(signature = (payload=None))] + pub fn new(payload: Option>) -> PyResult { + let pskt = match payload { + None => PyPSKT::from(State::Creator(PSKT::::default())), + Some(p) => { + if let Ok(s) = p.extract::() { + let inner = serde_json::from_str(&s) + .map_err(|_| Error::from(WasmError::InvalidPayload))?; + PyPSKT::from(State::NoOp(Some(inner))) + } else if let Ok(py_tx) = p.extract::() { + let tx: Transaction = py_tx.into(); + let inner: Inner = tx + .try_into() + .map_err(|err: NativeError| PyException::new_err(err.to_string()))?; + PyPSKT::from(State::NoOp(Some(inner))) + } else { + return Err(Error::from(WasmError::InvalidPayload).into()); + } + } + }; + + Ok(pskt) } #[getter] @@ -89,9 +94,9 @@ impl PyPSKT { let state = match self.take() { State::NoOp(inner) => match inner { None => State::Creator(PSKT::default()), - Some(_) => Err(Error::from(NativeError::CreateNotAllowed))?, + Some(_) => Err(Error::from(WasmError::CreateNotAllowed))?, }, - state => Err(Error::from(NativeError::state(state)))?, + state => Err(Error::from(WasmError::state(state)))?, }; self.replace(state) @@ -102,11 +107,11 @@ impl PyPSKT { let state = match self.take() { State::NoOp(inner) => State::Constructor( inner - .ok_or(Error::from(NativeError::NotInitialized))? + .ok_or(Error::from(WasmError::NotInitialized))? .into(), ), State::Creator(pskt) => State::Constructor(pskt.constructor()), - state => Err(Error::from(NativeError::state(state)))?, + state => Err(Error::from(WasmError::state(state)))?, }; self.replace(state) @@ -117,11 +122,11 @@ impl PyPSKT { let state = match self.take() { State::NoOp(inner) => State::Updater( inner - .ok_or(Error::from(NativeError::NotInitialized))? + .ok_or(Error::from(WasmError::NotInitialized))? .into(), ), State::Constructor(constructor) => State::Updater(constructor.updater()), - state => Err(Error::from(NativeError::state(state)))?, + state => Err(Error::from(WasmError::state(state)))?, }; self.replace(state) @@ -132,13 +137,13 @@ impl PyPSKT { let state = match self.take() { State::NoOp(inner) => State::Signer( inner - .ok_or(Error::from(NativeError::NotInitialized))? + .ok_or(Error::from(WasmError::NotInitialized))? .into(), ), State::Constructor(pskt) => State::Signer(pskt.signer()), State::Updater(pskt) => State::Signer(pskt.signer()), State::Combiner(pskt) => State::Signer(pskt.signer()), - state => Err(Error::from(NativeError::state(state)))?, + state => Err(Error::from(WasmError::state(state)))?, }; self.replace(state) @@ -149,13 +154,13 @@ impl PyPSKT { let state = match self.take() { State::NoOp(inner) => State::Combiner( inner - .ok_or(Error::from(NativeError::NotInitialized))? + .ok_or(Error::from(WasmError::NotInitialized))? .into(), ), State::Constructor(pskt) => State::Combiner(pskt.combiner()), State::Updater(pskt) => State::Combiner(pskt.combiner()), State::Signer(pskt) => State::Combiner(pskt.combiner()), - state => Err(Error::from(NativeError::state(state)))?, + state => Err(Error::from(WasmError::state(state)))?, }; self.replace(state) @@ -166,11 +171,11 @@ impl PyPSKT { let state = match self.take() { State::NoOp(inner) => State::Finalizer( inner - .ok_or(Error::from(NativeError::NotInitialized))? + .ok_or(Error::from(WasmError::NotInitialized))? .into(), ), State::Combiner(pskt) => State::Finalizer(pskt.finalizer()), - state => Err(Error::from(NativeError::state(state)))?, + state => Err(Error::from(WasmError::state(state)))?, }; self.replace(state) @@ -181,15 +186,15 @@ impl PyPSKT { let state = match self.take() { State::NoOp(inner) => State::Extractor( inner - .ok_or(Error::from(NativeError::NotInitialized))? + .ok_or(Error::from(WasmError::NotInitialized))? .into(), ), State::Finalizer(pskt) => State::Extractor( pskt.extractor() - .map_err(NativeError::from) + .map_err(WasmError::from) .map_err(Error::from)?, ), - state => Err(Error::from(NativeError::state(state)))?, + state => Err(Error::from(WasmError::state(state)))?, }; self.replace(state) @@ -198,7 +203,7 @@ impl PyPSKT { pub fn fallback_lock_time(&self, lock_time: u64) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.fallback_lock_time(lock_time)), - _ => Err(Error::from(NativeError::expected_state("Creator")))?, + _ => Err(Error::from(WasmError::expected_state("Creator")))?, }; self.replace(state) @@ -207,7 +212,7 @@ impl PyPSKT { pub fn inputs_modifiable(&self) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.inputs_modifiable()), - _ => Err(Error::from(NativeError::expected_state("Creator")))?, + _ => Err(Error::from(WasmError::expected_state("Creator")))?, }; self.replace(state) @@ -216,7 +221,7 @@ impl PyPSKT { pub fn outputs_modifiable(&self) -> PyResult { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.outputs_modifiable()), - _ => Err(Error::from(NativeError::expected_state("Creator")))?, + _ => Err(Error::from(WasmError::expected_state("Creator")))?, }; self.replace(state) @@ -225,7 +230,7 @@ impl PyPSKT { pub fn no_more_inputs(&self) -> PyResult { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_inputs()), - _ => Err(Error::from(NativeError::expected_state("Constructor")))?, + _ => Err(Error::from(WasmError::expected_state("Constructor")))?, }; self.replace(state) @@ -234,7 +239,7 @@ impl PyPSKT { pub fn no_more_outputs(&self) -> PyResult { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_outputs()), - _ => Err(Error::from(NativeError::expected_state("Constructor")))?, + _ => Err(Error::from(WasmError::expected_state("Constructor")))?, }; self.replace(state) @@ -248,16 +253,16 @@ impl PyPSKT { let input = TransactionInput::from(input); let mut input: Input = input .try_into() - .map_err(|err| Error::from(NativeError::from(err)))?; + .map_err(|err| Error::from(WasmError::from(err)))?; input.redeem_script = Some(hex::decode(data).map_err(|e| { - Error::from(NativeError::custom(format!( + Error::from(WasmError::custom(format!( "Redeem script is not a hex string: {}", e ))) })?); let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.input(input)), - _ => Err(Error::from(NativeError::expected_state("Constructor")))?, + _ => Err(Error::from(WasmError::expected_state("Constructor")))?, }; self.replace(state) @@ -270,10 +275,10 @@ impl PyPSKT { pskt.input( input .try_into() - .map_err(|err| Error::from(NativeError::from(err)))?, + .map_err(|err| Error::from(WasmError::from(err)))?, ), ), - _ => Err(Error::from(NativeError::expected_state("Constructor")))?, + _ => Err(Error::from(WasmError::expected_state("Constructor")))?, }; self.replace(state) @@ -286,10 +291,10 @@ impl PyPSKT { pskt.output( output .try_into() - .map_err(|err| Error::from(NativeError::from(err)))?, + .map_err(|err| Error::from(WasmError::from(err)))?, ), ), - _ => Err(Error::from(NativeError::expected_state("Constructor")))?, + _ => Err(Error::from(WasmError::expected_state("Constructor")))?, }; self.replace(state) @@ -299,9 +304,9 @@ impl PyPSKT { let state = match self.take() { State::Updater(pskt) => State::Updater( pskt.set_sequence(n, input_index) - .map_err(|err| Error::from(NativeError::from(err)))?, + .map_err(|err| Error::from(WasmError::from(err)))?, ), - _ => Err(Error::from(NativeError::expected_state("Updater")))?, + _ => Err(Error::from(WasmError::expected_state("Updater")))?, }; self.replace(state) @@ -311,7 +316,7 @@ impl PyPSKT { let state = self.state(); match state.as_ref().unwrap() { State::Signer(pskt) => Ok(pskt.calculate_id().into()), - _ => Err(Error::from(NativeError::expected_state("Signer")))?, + _ => Err(Error::from(WasmError::expected_state("Signer")))?, } } @@ -329,7 +334,7 @@ impl PyPSKT { State::Finalizer(pskt) => { for input in pskt.inputs.iter() { if input.redeem_script.is_some() { - return Err(Error::from(NativeError::custom( + return Err(Error::from(WasmError::custom( "Mass calculation is not supported for inputs with redeem scripts", )) .into()); @@ -340,12 +345,12 @@ impl PyPSKT { Ok(vec![vec![0u8, 65]; inner.inputs.len()]) }) .map_err(|e| { - Error::from(NativeError::custom(format!( + Error::from(WasmError::custom(format!( "Failed to finalize PSKT: {e}" ))) })?; pskt.extractor() - .map_err(|err| Error::from(NativeError::TxNotFinalized(err)))? + .map_err(|err| Error::from(WasmError::TxNotFinalized(err)))? } _ => panic!("Finalizer state is not valid"), } @@ -353,7 +358,7 @@ impl PyPSKT { let tx = extractor .extract_tx_unchecked(&NetworkType::from(network_type).into()) .map_err(|e| { - Error::from(NativeError::custom(format!( + Error::from(WasmError::custom(format!( "Failed to extract transaction: {e}" ))) })?; From c2d7f872ccffbbb4129aea61a07ac2b6a3e4b500 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sun, 25 Jan 2026 08:56:43 -0500 Subject: [PATCH 14/15] pskt example wip --- examples/transactions/pskt.py | 142 ++++++++++++++++++++++++++++++++++ kaspa.pyi | 2 +- src/wallet/pskt/mod.rs | 52 +++++-------- 3 files changed, 162 insertions(+), 34 deletions(-) create mode 100644 examples/transactions/pskt.py diff --git a/examples/transactions/pskt.py b/examples/transactions/pskt.py new file mode 100644 index 00000000..511a6b00 --- /dev/null +++ b/examples/transactions/pskt.py @@ -0,0 +1,142 @@ +import asyncio +from kaspa import ( + Hash, + Mnemonic, + Opcodes, + PSKT, + Resolver, + RpcClient, + ScriptBuilder, + TransactionInput, + TransactionOutpoint, + UtxoEntryReference, + XPrv, + address_from_script_public_key, + calculate_transaction_mass, + create_transaction, + sign_transaction, +) + + +def derive(seed, account_index): + xprv = XPrv(seed).derive_path(f"m/45'/111111'/{account_index}'") + xpub = xprv.to_xpub() + prv = xprv.derive_child(1).to_private_key() + pub = xpub.derive_child(1).to_public_key() + return prv, pub + + +async def main(): + ####################################################### + # Derive 3 accounts to use for Multisig PSKT demo + ####################################################### + seed = Mnemonic(( + 'predict cloud noise economy home stereo tag cancel adult pistol act remove ' + 'equip cricket man summer neutral black art miracle foam world clown say' + )).to_seed() + + prv1, pub1 = derive(seed, 0) + print(f'Account 1:\n - prv: {prv1.to_string()}\n - pub: {pub1.to_string()}\n') + + prv2, pub2 = derive(seed, 1) + print(f'Account 2:\n - prv: {prv2.to_string()}\n - pub: {pub2.to_string()}\n') + + prv3, pub3 = derive(seed, 2) + print(f'Account 3:\n - prv: {prv3.to_string()}\n - pub: {pub3.to_string()}\n') + + ####################################################### + # Create Multisig address + ####################################################### + redeem_script = ScriptBuilder()\ + .add_i64(2)\ + .add_data(pub1.to_x_only_public_key().to_string())\ + .add_data(pub2.to_x_only_public_key().to_string())\ + .add_data(pub3.to_x_only_public_key().to_string())\ + .add_i64(3)\ + .add_op(Opcodes.OpCheckMultiSig) + spk = redeem_script.create_pay_to_script_hash_script() + address = address_from_script_public_key(spk, "testnet") + + print(f"Multisig address: {address}") + + while True: + if input("Send funds to address (y to proceed): ") == "y": + break + + ####################################################### + # Get address's UTXOs + ####################################################### + client = RpcClient(resolver=Resolver(), network_id='testnet-10') + await client.connect(strategy='fallback') + utxos = await client.get_utxos_by_addresses(request={'addresses': [address]}) + utxos = utxos["entries"] + utxos = sorted(utxos, key=lambda x: x['utxoEntry']['amount'], reverse=True) + total = sum(item["utxoEntry"]["amount"] for item in utxos) + print(utxos) + # utxo = utxos["entries"][0] + + ####################################################### + # Placeholder TX for fee calculation + ####################################################### + # outputs = [ + # {"address": address, "amount": int(total)} + # ] + # tx = create_transaction(utxos, outputs, 0, None, 2) + # mass = calculate_transaction_mass("testnet-10", tx) + + ####################################################### + # Get feerates & create actual TX + ####################################################### + # fee_rates = await client.get_fee_estimate() + # fee_rate = int(fee_rates["estimate"]["priorityBucket"]["feerate"]) + + # outputs = [ + # {"address": address, "amount": int(total - (fee_rate * mass)), "scriptPublicKey": ""} + # ] + # tx = create_transaction(utxos, outputs, 0, None, 1) + # tx_signed = sign_transaction(tx, [prv1], True) + + ####################################################### + # Create PSKT + ####################################################### + pskt = PSKT() + pskt_serialized = pskt.serialize() + print(pskt_serialized) + + ####################################################### + # Create input + ####################################################### + input0 = TransactionInput.from_dict({ + 'previousOutpoint': { 'transactionId': 'c38eb7191a2e0df6089b05cf7df9c92dc559db618184b11cbb8c5ba30b024bce', 'index': 1 }, + 'signatureScript': '', + 'sequence': 0, + 'sigOpCount': 1, + 'utxo': { + 'utxo': { + 'address': 'kaspatest:prganzek6uhsn4rv29g6qkeh8rduae6n3ul0xk5fnzjtugqhfaxcx0ee2dn47', + 'outpoint': {'transactionId': 'c38eb7191a2e0df6089b05cf7df9c92dc559db618184b11cbb8c5ba30b024bce', 'index': 1}, + 'amount': 98699920028, + 'scriptPublicKey': '0000aa20d1d98b36d72f09d46c5151a05b3738dbcee7538f3ef35a8998a4be20174f4d8387', + 'blockDaaScore': 354263497, + 'isCoinbase': False + } + } + }) + # pskt = pskt.input(input) + + # previous_outpoint = TransactionOutpoint( + # transaction_id=Hash(utxo["outpoint"]['transactionId']), + # index=utxo["outpoint"]['index'] + # ) + # input_0 = TransactionInput( + # previous_outpoint=previous_outpoint, + # signature_script=b"", + # sequence=0, + # sig_op_count=2, + # utxo=None + # ) + # pskt.to_constructor().input(input_0) + # print(pskt.serialize()) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/kaspa.pyi b/kaspa.pyi index 3a13b66a..c807b2de 100644 --- a/kaspa.pyi +++ b/kaspa.pyi @@ -771,7 +771,7 @@ class PSKT: def role(self) -> builtins.str: ... @property def payload(self) -> builtins.str: ... - def __new__(cls, payload: typing.Any) -> PSKT: ... + def __new__(cls, payload: typing.Optional[typing.Any] = None) -> PSKT: ... def serialize(self) -> builtins.str: ... def creator(self) -> PSKT: r""" diff --git a/src/wallet/pskt/mod.rs b/src/wallet/pskt/mod.rs index b3116120..10a46751 100644 --- a/src/wallet/pskt/mod.rs +++ b/src/wallet/pskt/mod.rs @@ -105,11 +105,9 @@ impl PyPSKT { /// Change role to `CONSTRUCTOR` pub fn to_constructor(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Constructor( - inner - .ok_or(Error::from(WasmError::NotInitialized))? - .into(), - ), + State::NoOp(inner) => { + State::Constructor(inner.ok_or(Error::from(WasmError::NotInitialized))?.into()) + } State::Creator(pskt) => State::Constructor(pskt.constructor()), state => Err(Error::from(WasmError::state(state)))?, }; @@ -120,11 +118,9 @@ impl PyPSKT { /// Change role to `UPDATER` pub fn to_updater(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Updater( - inner - .ok_or(Error::from(WasmError::NotInitialized))? - .into(), - ), + State::NoOp(inner) => { + State::Updater(inner.ok_or(Error::from(WasmError::NotInitialized))?.into()) + } State::Constructor(constructor) => State::Updater(constructor.updater()), state => Err(Error::from(WasmError::state(state)))?, }; @@ -135,11 +131,9 @@ impl PyPSKT { /// Change role to `SIGNER` pub fn to_signer(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Signer( - inner - .ok_or(Error::from(WasmError::NotInitialized))? - .into(), - ), + State::NoOp(inner) => { + State::Signer(inner.ok_or(Error::from(WasmError::NotInitialized))?.into()) + } State::Constructor(pskt) => State::Signer(pskt.signer()), State::Updater(pskt) => State::Signer(pskt.signer()), State::Combiner(pskt) => State::Signer(pskt.signer()), @@ -152,11 +146,9 @@ impl PyPSKT { /// Change role to `COMBINER` pub fn to_combiner(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Combiner( - inner - .ok_or(Error::from(WasmError::NotInitialized))? - .into(), - ), + State::NoOp(inner) => { + State::Combiner(inner.ok_or(Error::from(WasmError::NotInitialized))?.into()) + } State::Constructor(pskt) => State::Combiner(pskt.combiner()), State::Updater(pskt) => State::Combiner(pskt.combiner()), State::Signer(pskt) => State::Combiner(pskt.combiner()), @@ -169,11 +161,9 @@ impl PyPSKT { /// Change role to `FINALIZER` pub fn to_finalizer(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Finalizer( - inner - .ok_or(Error::from(WasmError::NotInitialized))? - .into(), - ), + State::NoOp(inner) => { + State::Finalizer(inner.ok_or(Error::from(WasmError::NotInitialized))?.into()) + } State::Combiner(pskt) => State::Finalizer(pskt.finalizer()), state => Err(Error::from(WasmError::state(state)))?, }; @@ -184,11 +174,9 @@ impl PyPSKT { /// Change role to `EXTRACTOR` pub fn to_extractor(&self) -> PyResult { let state = match self.take() { - State::NoOp(inner) => State::Extractor( - inner - .ok_or(Error::from(WasmError::NotInitialized))? - .into(), - ), + State::NoOp(inner) => { + State::Extractor(inner.ok_or(Error::from(WasmError::NotInitialized))?.into()) + } State::Finalizer(pskt) => State::Extractor( pskt.extractor() .map_err(WasmError::from) @@ -345,9 +333,7 @@ impl PyPSKT { Ok(vec![vec![0u8, 65]; inner.inputs.len()]) }) .map_err(|e| { - Error::from(WasmError::custom(format!( - "Failed to finalize PSKT: {e}" - ))) + Error::from(WasmError::custom(format!("Failed to finalize PSKT: {e}"))) })?; pskt.extractor() .map_err(|err| Error::from(WasmError::TxNotFinalized(err)))? From 76c01bf10d70c44c03d5e07a4596a7b9f7c048f9 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 31 Jan 2026 12:49:52 -0500 Subject: [PATCH 15/15] fix pskt from serialized string --- src/wallet/pskt/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wallet/pskt/mod.rs b/src/wallet/pskt/mod.rs index 10a46751..6e53d0ba 100644 --- a/src/wallet/pskt/mod.rs +++ b/src/wallet/pskt/mod.rs @@ -53,9 +53,9 @@ impl PyPSKT { None => PyPSKT::from(State::Creator(PSKT::::default())), Some(p) => { if let Ok(s) = p.extract::() { - let inner = serde_json::from_str(&s) - .map_err(|_| Error::from(WasmError::InvalidPayload))?; - PyPSKT::from(State::NoOp(Some(inner))) + let inner: State = serde_json::from_str(&s) + .map_err(|err| Error::from(WasmError::Ctor(err.to_string())))?; + PyPSKT::from(inner) } else if let Ok(py_tx) = p.extract::() { let tx: Transaction = py_tx.into(); let inner: Inner = tx