Просмотр исходного кода

src/sdk/python: create python bindings for transaction, contract, contract function call parameters with the aim of decoding darkfid txs from python scripts

oars 11 месяцев назад
Родитель
Сommit
073b83320c

+ 1 - 0
src/sdk/python/pyproject.toml

@@ -4,6 +4,7 @@ build-backend = "maturin"
 
 [project]
 name = "darkfi-sdk"
+version = "0.5.0"
 requires-python = ">=3.9"
 classifiers = [
     "Programming Language :: Rust",

+ 58 - 0
src/sdk/python/src/contract/dao/auth_xfer.rs

@@ -0,0 +1,58 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_dao_contract::model as dao_model;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`dao_model::DaoAuthMoneyTransferParams`] python binding.
+#[pyclass]
+pub struct DaoAuthMoneyTransferParams(dao_model::DaoAuthMoneyTransferParams);
+impl_py_methods!(DaoAuthMoneyTransferParams);
+
+impl FunctionParams for dao_model::DaoAuthMoneyTransferParams {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item(
+            "enc_attrs",
+            self.enc_attrs
+                .iter()
+                .map(|e| e.to_pydict(py))
+                .collect::<PyResult<Vec<Py<PyDict>>>>()?,
+        )?;
+        dict.set_item("dao_change_attrs", self.dao_change_attrs.to_pydict(py)?)?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}dao_change_attrs:").unwrap();
+        self.dao_change_attrs.fmt_pretty(out, depth + 2)?;
+
+        writeln!(out, "{prefix}enc_attrs:").unwrap();
+
+        for attr in &self.enc_attrs {
+            attr.fmt_pretty(out, depth + 2)?;
+            writeln!(out).unwrap();
+        }
+        Ok(())
+    }
+}

+ 192 - 0
src/sdk/python/src/contract/dao/exec.rs

@@ -0,0 +1,192 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi::Result;
+use darkfi_dao_contract::{model as dao_model, DaoFunction};
+use darkfi_money_contract::{model as money_model, MoneyFunction};
+use darkfi_sdk::crypto::{ContractId, DAO_CONTRACT_ID, MONEY_CONTRACT_ID};
+use darkfi_serial::deserialize;
+use pyo3::{
+    exceptions::PyValueError, prelude::PyDictMethods, pyclass, pymethods, types::PyDict, Py,
+    PyResult, Python,
+};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`dao_model::DaoExecParams`] python binding.
+#[pyclass]
+pub struct DaoExecParams(dao_model::DaoExecParams);
+impl_py_methods!(DaoExecParams);
+
+impl FunctionParams for dao_model::DaoExecParams {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("proposal_bulla", self.proposal_bulla.to_string())?;
+        dict.set_item(
+            "proposal_auth_calls",
+            self.proposal_auth_calls
+                .iter()
+                .map(|auth_call| {
+                    DaoAuthCallDecoded::new(auth_call)
+                        .map_err(|e| PyValueError::new_err(e.to_string()))?
+                        .to_pydict(py)
+                })
+                .collect::<PyResult<Vec<Py<PyDict>>>>()?,
+        )?;
+        dict.set_item("blind_total_vote", self.blind_total_vote.to_pydict(py)?)?;
+        dict.set_item("early_exec", self.early_exec)?;
+        dict.set_item("signature_public", self.signature_public.to_string())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}proposal_bulla: {}", self.proposal_bulla).unwrap();
+        writeln!(out, "{prefix}early_exec: {}", self.early_exec).unwrap();
+        writeln!(out, "{prefix}signature_public: {}", self.signature_public).unwrap();
+        writeln!(out, "{prefix}blind_total_vote:").unwrap();
+        self.blind_total_vote.fmt_pretty(out, depth + 2)?;
+
+        writeln!(out, "{prefix}proposal_auth_calls:").unwrap();
+        for auth_call in &self.proposal_auth_calls {
+            DaoAuthCallDecoded::new(auth_call)
+                .map_err(|e| PyValueError::new_err(e.to_string()))?
+                .fmt_pretty(out, depth + 2)?;
+            writeln!(out).unwrap();
+        }
+        Ok(())
+    }
+}
+
+/// [`dao_model::DaoBlindAggregateVote`] python binding.
+#[pyclass]
+pub struct DaoBlindAggregateVote(dao_model::DaoBlindAggregateVote);
+impl_py_methods!(DaoBlindAggregateVote);
+
+impl FunctionParams for dao_model::DaoBlindAggregateVote {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("yes_vote_commit", format!("{:?}", self.yes_vote_commit))?;
+        dict.set_item("all_vote_commit", format!("{:?}", self.all_vote_commit))?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}yes_vote_commit: {:?}", self.yes_vote_commit).unwrap();
+        writeln!(out, "{prefix}all_vote_commit: {:?}", self.all_vote_commit).unwrap();
+        Ok(())
+    }
+}
+
+/// [`dao_model::DaoAuthCall`] python binding.
+#[pyclass]
+pub struct DaoAuthCall(dao_model::DaoAuthCall);
+
+#[pymethods]
+impl DaoAuthCall {
+    #[getter]
+    pub fn __dict__(&self, py: Python) -> PyResult<Py<PyDict>> {
+        DaoAuthCallDecoded::new(&self.0)
+            .map_err(|e| PyValueError::new_err(e.to_string()))?
+            .to_pydict(py)
+    }
+
+    pub fn __str__(&self) -> PyResult<String> {
+        let mut out = String::new();
+        DaoAuthCallDecoded::new(&self.0)
+            .map_err(|e| PyValueError::new_err(e.to_string()))?
+            .fmt_pretty(&mut out, 0)?;
+        Ok(out)
+    }
+}
+
+/// Decoded representation of [`dao_model::DaoAuthCall`].
+pub struct DaoAuthCallDecoded {
+    contract_id: ContractId,
+    contract_name: String,
+    function_code: u8,
+    function_name: String,
+    auth_data: Vec<money_model::Coin>,
+}
+
+impl DaoAuthCallDecoded {
+    fn new(call: &dao_model::DaoAuthCall) -> Result<Self> {
+        let (contract_name, function_name) = if call.contract_id == *MONEY_CONTRACT_ID {
+            (
+                "Money".to_string(),
+                MoneyFunction::try_from(call.function_code).map(|f| format!("{f:?}"))?,
+            )
+        } else if call.contract_id == *DAO_CONTRACT_ID {
+            (
+                "Dao".to_string(),
+                DaoFunction::try_from(call.function_code).map(|f| format!("{f:?}"))?,
+            )
+        } else {
+            // TODO: Add support for decoding custom contract calls
+            ("Unknown".to_string(), "Unknown".to_string())
+        };
+
+        let proposal_coins =
+            if !call.auth_data.is_empty() { deserialize(&call.auth_data[..])? } else { vec![] };
+
+        Ok(Self {
+            contract_id: call.contract_id,
+            contract_name,
+            function_code: call.function_code,
+            function_name,
+            auth_data: proposal_coins,
+        })
+    }
+}
+
+impl FunctionParams for DaoAuthCallDecoded {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+
+        dict.set_item("contract_id", self.contract_id.to_string())?;
+        dict.set_item("contract_name", &self.contract_name)?;
+        dict.set_item("function_code", self.function_code)?;
+        dict.set_item("function_name", &self.function_name)?;
+        dict.set_item(
+            "auth_data",
+            self.auth_data.iter().map(|c| c.to_string()).collect::<Vec<_>>(),
+        )?;
+
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}contract_id: {}", self.contract_id).unwrap();
+        writeln!(out, "{prefix}contract_name: {}", self.contract_name).unwrap();
+        writeln!(out, "{prefix}function_code: {}", self.function_code).unwrap();
+        writeln!(out, "{prefix}function_name: {}", self.function_name).unwrap();
+
+        if !self.auth_data.is_empty() {
+            writeln!(out, "{prefix}auth_data:").unwrap();
+
+            for coin in &self.auth_data {
+                writeln!(out, "   {prefix}{coin}").unwrap();
+            }
+        }
+        Ok(())
+    }
+}

+ 45 - 0
src/sdk/python/src/contract/dao/mint.rs

@@ -0,0 +1,45 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_dao_contract::model as dao_model;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`dao_model::DaoMintParams`] python binding.
+#[pyclass]
+pub struct DaoMintParams(dao_model::DaoMintParams);
+impl_py_methods!(DaoMintParams);
+
+impl FunctionParams for dao_model::DaoMintParams {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("dao_bulla", self.dao_bulla.to_string())?;
+        dict.set_item("dao_pubkey", self.dao_pubkey.to_string())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}dao_bulla: {}", self.dao_bulla).unwrap();
+        writeln!(out, "{prefix}dao_pubkey: {}", self.dao_pubkey).unwrap();
+        Ok(())
+    }
+}

+ 97 - 0
src/sdk/python/src/contract/dao/mod.rs

@@ -0,0 +1,97 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi::Result;
+use darkfi_dao_contract::{model as dao_model, DaoFunction};
+use darkfi_serial::deserialize;
+use pyo3::{
+    prelude::{PyAnyMethods, PyModule, PyModuleMethods},
+    Bound, PyResult, Python,
+};
+
+use crate::crypto::{ElGamalEncryptedNote3, ElGamalEncryptedNote4, ElGamalEncryptedNote5};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`DaoFunction::Mint`] function call parameter's python bindings.
+pub mod mint;
+
+/// [`DaoFunction::Propose`] function call parameter's python bindings.
+pub mod propose;
+
+/// [`DaoFunction::Vote`] function call parameter's python bindings.
+pub mod vote;
+
+/// [`DaoFunction::Exec`] function call parameter's python bindings.
+pub mod exec;
+
+/// [`DaoFunction::AuthMoneyTransfer`] function call parameter's python bindings.
+pub mod auth_xfer;
+
+/// Decodes the parameters of a DAO contract function call.
+pub fn decode_dao_function_params(
+    function_index: u8,
+    data: &[u8],
+) -> Result<Box<dyn FunctionParams>> {
+    let res: Box<dyn FunctionParams> = match DaoFunction::try_from(function_index)? {
+        DaoFunction::Mint => {
+            let params: dao_model::DaoMintParams = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+        DaoFunction::Propose => {
+            let params: dao_model::DaoProposeParams = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+        DaoFunction::Vote => {
+            let params: dao_model::DaoVoteParams = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+        DaoFunction::Exec => {
+            let params: dao_model::DaoExecParams = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+        DaoFunction::AuthMoneyTransfer => {
+            let params: dao_model::DaoAuthMoneyTransferParams = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+    };
+
+    Ok(res)
+}
+
+/// Create dao module and provide the python bindings.
+pub fn create_module(py: Python) -> PyResult<Bound<PyModule>> {
+    let submod = PyModule::new(py, "dao")?;
+
+    submod.add_class::<mint::DaoMintParams>()?;
+    submod.add_class::<propose::DaoProposeParams>()?;
+    submod.add_class::<propose::DaoProposeParamsInput>()?;
+    submod.add_class::<vote::DaoVoteParams>()?;
+    submod.add_class::<vote::DaoVoteParamsInput>()?;
+    submod.add_class::<exec::DaoExecParams>()?;
+    submod.add_class::<exec::DaoAuthCall>()?;
+    submod.add_class::<exec::DaoBlindAggregateVote>()?;
+    submod.add_class::<auth_xfer::DaoAuthMoneyTransferParams>()?;
+    submod.add_class::<ElGamalEncryptedNote3>()?;
+    submod.add_class::<ElGamalEncryptedNote4>()?;
+    submod.add_class::<ElGamalEncryptedNote5>()?;
+
+    py.import("sys")?.getattr("modules")?.set_item("darkfi_sdk.contract.dao", &submod)?;
+
+    Ok(submod)
+}

+ 90 - 0
src/sdk/python/src/contract/dao/propose.rs

@@ -0,0 +1,90 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_dao_contract::model as dao_model;
+use darkfi_sdk::crypto::util::FieldElemAsStr;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`dao_model::DaoProposeParams`] python binding.
+#[pyclass]
+pub struct DaoProposeParams(dao_model::DaoProposeParams);
+impl_py_methods!(DaoProposeParams);
+
+impl FunctionParams for dao_model::DaoProposeParams {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("dao_merkle_root", self.dao_merkle_root.to_string())?;
+        dict.set_item("token_commit", self.token_commit.to_string())?;
+        dict.set_item("proposal_bulla", self.proposal_bulla.to_string())?;
+        dict.set_item("note", self.note.to_pydict(py)?)?;
+        dict.set_item(
+            "inputs",
+            self.inputs
+                .iter()
+                .map(|input| input.to_pydict(py))
+                .collect::<PyResult<Vec<Py<PyDict>>>>()?,
+        )?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}dao_merkle_root: {}", self.dao_merkle_root).unwrap();
+        writeln!(out, "{prefix}token_commit: {}", self.dao_merkle_root).unwrap();
+        writeln!(out, "{prefix}proposal_bulla: {}", self.dao_merkle_root).unwrap();
+        writeln!(out, "{prefix}note:").unwrap();
+        self.note.fmt_pretty(out, depth + 2)?;
+
+        writeln!(out, "{prefix}inputs:").unwrap();
+
+        for input in &self.inputs {
+            input.fmt_pretty(out, depth + 2)?;
+            writeln!(out).unwrap();
+        }
+        Ok(())
+    }
+}
+
+/// [`dao_model::DaoProposeParamsInput`] python binding.
+#[pyclass]
+pub struct DaoProposeParamsInput(dao_model::DaoProposeParamsInput);
+impl_py_methods!(DaoProposeParamsInput);
+
+impl FunctionParams for dao_model::DaoProposeParamsInput {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("value_commit", format!("{:?}", self.value_commit))?;
+        dict.set_item("merkle_coin_root", self.merkle_coin_root.to_string())?;
+        dict.set_item("smt_null_root", self.smt_null_root.to_string())?;
+        dict.set_item("signature_public", self.signature_public.to_string())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}value_commit: {:?}", self.value_commit).unwrap();
+        writeln!(out, "{prefix}merkle_coin_root: {}", self.merkle_coin_root).unwrap();
+        writeln!(out, "{prefix}smt_null_root: {:?}", self.smt_null_root).unwrap();
+        writeln!(out, "{prefix}signature_public: {:?}", self.signature_public).unwrap();
+        Ok(())
+    }
+}

+ 87 - 0
src/sdk/python/src/contract/dao/vote.rs

@@ -0,0 +1,87 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_dao_contract::model as dao_model;
+use darkfi_sdk::crypto::util::FieldElemAsStr;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`dao_model::DaoVoteParams`] python binding.
+#[pyclass]
+pub struct DaoVoteParams(dao_model::DaoVoteParams);
+impl_py_methods!(DaoVoteParams);
+
+impl FunctionParams for dao_model::DaoVoteParams {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("token_commit", self.token_commit.to_string())?;
+        dict.set_item("proposal_bulla", self.proposal_bulla.to_string())?;
+        dict.set_item("yes_vote_commit", format!("{:?}", self.yes_vote_commit))?;
+        dict.set_item("note", self.note.to_pydict(py)?)?;
+        dict.set_item(
+            "inputs",
+            self.inputs
+                .iter()
+                .map(|input| input.to_pydict(py))
+                .collect::<PyResult<Vec<Py<PyDict>>>>()?,
+        )?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}token_commit: {:?}", self.token_commit).unwrap();
+        writeln!(out, "{prefix}proposal_bulla: {:?}", self.proposal_bulla).unwrap();
+        writeln!(out, "{prefix}yes_vote_commit: {:?}", self.yes_vote_commit).unwrap();
+        writeln!(out, "{prefix}note:").unwrap();
+        self.note.fmt_pretty(out, depth + 2)?;
+
+        writeln!(out, "{prefix}inputs:").unwrap();
+        for input in &self.inputs {
+            input.fmt_pretty(out, depth + 2)?;
+            writeln!(out).unwrap();
+        }
+        Ok(())
+    }
+}
+
+/// [`dao_model::DaoVoteParamsInput`] python binding.
+#[pyclass]
+pub struct DaoVoteParamsInput(dao_model::DaoVoteParamsInput);
+impl_py_methods!(DaoVoteParamsInput);
+
+impl FunctionParams for dao_model::DaoVoteParamsInput {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("vote_commit", format!("{:?}", self.vote_commit))?;
+        dict.set_item("vote_nullifier", self.vote_nullifier.to_string())?;
+        dict.set_item("signature_public", self.signature_public.to_string())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}vote_commit: {:?}", self.vote_commit).unwrap();
+        writeln!(out, "{prefix}vote_nullifier: {:?}", self.vote_nullifier).unwrap();
+        writeln!(out, "{prefix}signature_public: {:?}", self.signature_public).unwrap();
+        Ok(())
+    }
+}

+ 47 - 0
src/sdk/python/src/contract/deployooor/deploy_v1.rs

@@ -0,0 +1,47 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_sdk::{deploy, hex::AsHex};
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`deploy::DeployParamsV1`] python binding.
+#[pyclass]
+pub struct DeployParamsV1(deploy::DeployParamsV1);
+impl_py_methods!(DeployParamsV1);
+
+impl FunctionParams for deploy::DeployParamsV1 {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("public_key", self.public_key.to_string())?;
+        dict.set_item("wasm_bindcode", self.wasm_bincode.hex())?;
+        dict.set_item("ix", self.ix.hex())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}public_key: {}", self.public_key).unwrap();
+        writeln!(out, "{prefix}wasm_bincode: [{} bytes]", &self.wasm_bincode.len()).unwrap();
+        writeln!(out, "{prefix}ix: [{} bytes]", &self.ix.len()).unwrap();
+        Ok(())
+    }
+}

+ 43 - 0
src/sdk/python/src/contract/deployooor/lock_v1.rs

@@ -0,0 +1,43 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_deployooor_contract::model as deployooor_model;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`deployooor_model::LockParamsV1`] python binding.
+#[pyclass]
+pub struct LockParamsV1(deployooor_model::LockParamsV1);
+impl_py_methods!(LockParamsV1);
+
+impl FunctionParams for deployooor_model::LockParamsV1 {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("public_key", self.public_key.to_string())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}public_key: {}", self.public_key).unwrap();
+        Ok(())
+    }
+}

+ 67 - 0
src/sdk/python/src/contract/deployooor/mod.rs

@@ -0,0 +1,67 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi::error::Result;
+use darkfi_deployooor_contract::{model as deployooor_model, DeployFunction};
+use darkfi_sdk::deploy;
+use darkfi_serial::deserialize;
+use pyo3::{
+    prelude::{PyAnyMethods, PyModule, PyModuleMethods},
+    Bound, PyResult, Python,
+};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`DeployFunction::DeployV1`] function call parameter's python bindings.
+pub mod deploy_v1;
+pub use deploy_v1::DeployParamsV1;
+
+/// [`DeployFunction::LockV1`] function call parameter's python bindings.
+pub mod lock_v1;
+pub use lock_v1::LockParamsV1;
+
+/// Decodes the parameters of a Deployooor contract function call.
+pub fn decode_deployooor_function_params(
+    function_index: u8,
+    data: &[u8],
+) -> Result<Box<dyn FunctionParams>> {
+    let res: Box<dyn FunctionParams> = match DeployFunction::try_from(function_index)? {
+        DeployFunction::DeployV1 => {
+            let params: deploy::DeployParamsV1 = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+        DeployFunction::LockV1 => {
+            let params: deployooor_model::LockParamsV1 = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+    };
+
+    Ok(res)
+}
+
+/// Create deployooor module and provide the python bindings.
+pub fn create_module(py: Python) -> PyResult<Bound<PyModule>> {
+    let submod = PyModule::new(py, "deployooor")?;
+
+    submod.add_class::<DeployParamsV1>()?;
+    submod.add_class::<LockParamsV1>()?;
+
+    py.import("sys")?.getattr("modules")?.set_item("darkfi_sdk.contract.deployooor", &submod)?;
+
+    Ok(submod)
+}

+ 202 - 0
src/sdk/python/src/contract/mod.rs

@@ -0,0 +1,202 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi_dao_contract::DaoFunction;
+use darkfi_deployooor_contract::DeployFunction;
+use darkfi_money_contract::MoneyFunction;
+use darkfi_sdk::{
+    crypto::{DAO_CONTRACT_ID, DEPLOYOOOR_CONTRACT_ID, MONEY_CONTRACT_ID},
+    dark_tree, tx,
+};
+use pyo3::{
+    exceptions::PyValueError,
+    prelude::{PyModule, PyModuleMethods},
+    pyclass, pymethods,
+    types::PyDict,
+    Bound, Py, PyResult, Python,
+};
+
+/// Money contract definitions
+pub mod money;
+pub use money::decode_money_function_params;
+
+/// Dao contract definitions
+pub mod dao;
+pub use dao::decode_dao_function_params;
+
+/// Deployooor contract definitions
+pub mod deployooor;
+pub use deployooor::decode_deployooor_function_params;
+
+/// Trait for working with contract function call parameters.
+pub trait FunctionParams {
+    /// Converts the parameters to a Python dictionary.
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>>;
+
+    /// Appends a formatted, pretty-printed representation of the parameters
+    /// to the given string buffer.
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()>;
+}
+
+/// Generates boilerplate pymethods shared by contract call function parameters
+#[macro_export]
+macro_rules! impl_py_methods {
+    ($name: ident) => {
+        #[pyo3::pymethods]
+        impl $name {
+            #[getter]
+            pub fn __dict__(&self, py: Python) -> PyResult<Py<PyDict>> {
+                self.0.to_pydict(py)
+            }
+
+            pub fn __str__(&self) -> PyResult<String> {
+                let mut out = String::new();
+                self.0.fmt_pretty(&mut out, 0)?;
+                Ok(out)
+            }
+        }
+    };
+}
+pub use impl_py_methods;
+
+/// A class representing a contract call leaf node within a
+/// transaction's call tree.
+#[pyclass]
+pub struct DarkLeafContractCall(pub dark_tree::DarkLeaf<tx::ContractCall>);
+
+#[pymethods]
+impl DarkLeafContractCall {
+    pub fn data(&self) -> ContractCall {
+        ContractCall(self.0.data.clone())
+    }
+
+    pub fn parent_index(&self) -> Option<usize> {
+        self.0.parent_index
+    }
+
+    pub fn children_indexes(&self) -> Vec<usize> {
+        self.0.children_indexes.clone()
+    }
+}
+
+/// A class representing a contract function call.
+#[pyclass]
+pub struct ContractCall(pub tx::ContractCall);
+
+#[pymethods]
+impl ContractCall {
+    pub fn contract_id(&self) -> String {
+        self.0.contract_id.to_string()
+    }
+
+    /// Name of the contract being invoked.
+    pub fn contract_name(&self) -> Option<String> {
+        if self.0.contract_id == *MONEY_CONTRACT_ID {
+            Some("Money".to_string())
+        } else if self.0.contract_id == *DAO_CONTRACT_ID {
+            Some("Dao".to_string())
+        } else if self.0.contract_id == *DEPLOYOOOR_CONTRACT_ID {
+            Some("Deployooor".to_string())
+        } else {
+            None
+        }
+    }
+
+    pub fn data(&self) -> Vec<u8> {
+        self.0.data.clone()
+    }
+
+    pub fn function_index(&self) -> u8 {
+        self.0.data[0]
+    }
+
+    /// Name of the contract function being invoked.
+    pub fn function_name(&self) -> Option<String> {
+        match self.contract_name().as_deref() {
+            Some("Money") => {
+                MoneyFunction::try_from(self.function_index()).map(|f| format!("{f:?}")).ok()
+            }
+            Some("Dao") => {
+                DaoFunction::try_from(self.function_index()).map(|f| format!("{f:?}")).ok()
+            }
+            Some("Deployooor") => {
+                DeployFunction::try_from(self.function_index()).map(|f| format!("{f:?}")).ok()
+            }
+            _ => None,
+        }
+    }
+
+    /// Represents the parameters of a contract function call as a Python dictionary.
+    pub fn function_params_dict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        match self.contract_name().as_deref() {
+            Some("Money") => decode_money_function_params(self.function_index(), &self.0.data)
+                .map_err(|e| PyValueError::new_err(e.to_string()))?
+                .to_pydict(py),
+            Some("Dao") => decode_dao_function_params(self.function_index(), &self.0.data)
+                .map_err(|e| PyValueError::new_err(e.to_string()))?
+                .to_pydict(py),
+            Some("Deployooor") => {
+                decode_deployooor_function_params(self.function_index(), &self.0.data)
+                    .map_err(|e| PyValueError::new_err(e.to_string()))?
+                    .to_pydict(py)
+            }
+            //TODO: Add support for custom contracts
+            _ => Err(PyValueError::new_err("Unknown Contract")),
+        }
+    }
+
+    /// Formatted string of the parameters passed to a contract function call.
+    pub fn function_params_str(&self, depth: usize) -> PyResult<String> {
+        let mut output = String::new();
+        match self.contract_name().as_deref() {
+            Some("Money") => decode_money_function_params(self.function_index(), &self.0.data)
+                .map_err(|e| PyValueError::new_err(e.to_string()))?
+                .fmt_pretty(&mut output, depth)?,
+            Some("Dao") => decode_dao_function_params(self.function_index(), &self.0.data)
+                .map_err(|e| PyValueError::new_err(e.to_string()))?
+                .fmt_pretty(&mut output, depth)?,
+            Some("Deployooor") => {
+                decode_deployooor_function_params(self.function_index(), &self.0.data)
+                    .map_err(|e| PyValueError::new_err(e.to_string()))?
+                    .fmt_pretty(&mut output, depth)?
+            }
+            //TODO: Add support for custom contracts
+            _ => Err(PyValueError::new_err("Unknown Contract"))?,
+        }
+        Ok(output)
+    }
+}
+
+/// Create contract module and provide the python bindings.
+pub fn create_module(py: Python) -> PyResult<Bound<PyModule>> {
+    let submod = PyModule::new(py, "contract")?;
+
+    submod.add_class::<DarkLeafContractCall>()?;
+    submod.add_class::<ContractCall>()?;
+
+    // money, dao, deployooor submodules will be inside contract submodule
+    let money_submodule = money::create_module(py)?;
+    let dao_submodule = dao::create_module(py)?;
+    let deployooor_submodule = deployooor::create_module(py)?;
+
+    submod.add_submodule(&money_submodule)?;
+    submod.add_submodule(&dao_submodule)?;
+    submod.add_submodule(&deployooor_submodule)?;
+
+    Ok(submod)
+}

+ 45 - 0
src/sdk/python/src/contract/money/auth_token_freeze_v1.rs

@@ -0,0 +1,45 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_money_contract::model as money_model;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`money_model::MoneyAuthTokenFreezeParamsV1`] python binding.
+#[pyclass]
+pub struct MoneyAuthTokenFreezeParamsV1(money_model::MoneyAuthTokenFreezeParamsV1);
+impl_py_methods!(MoneyAuthTokenFreezeParamsV1);
+
+impl FunctionParams for money_model::MoneyAuthTokenFreezeParamsV1 {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("mint_public", self.mint_public.to_string())?;
+        dict.set_item("token_id", self.token_id.to_string())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}mint_public: {}", self.mint_public).unwrap();
+        writeln!(out, "{prefix}token_id: {}", self.token_id).unwrap();
+        Ok(())
+    }
+}

+ 48 - 0
src/sdk/python/src/contract/money/auth_token_mint_v1.rs

@@ -0,0 +1,48 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_money_contract::model as money_model;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`money_model::MoneyAuthTokenMintParamsV1`] python binding.
+#[pyclass]
+pub struct MoneyAuthTokenMintParamsV1(money_model::MoneyAuthTokenMintParamsV1);
+impl_py_methods!(MoneyAuthTokenMintParamsV1);
+
+impl FunctionParams for money_model::MoneyAuthTokenMintParamsV1 {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("token_id", self.token_id.to_string())?;
+        dict.set_item("enc_note", self.enc_note.to_pydict(py)?)?;
+        dict.set_item("mint_pubkey", self.mint_pubkey.to_string())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}token_id: {}", self.token_id).unwrap();
+        writeln!(out, "{prefix}mint_pubkey: {}", self.mint_pubkey).unwrap();
+        writeln!(out, "{prefix}enc_note:").unwrap();
+        self.enc_note.fmt_pretty(out, depth + 2)?;
+        Ok(())
+    }
+}

+ 53 - 0
src/sdk/python/src/contract/money/fee_v1.rs

@@ -0,0 +1,53 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_money_contract::model as money_model;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`money_model::MoneyFeeParamsV1`] python binding.
+#[pyclass]
+pub struct MoneyFeeParamsV1(money_model::MoneyFeeParamsV1);
+impl_py_methods!(MoneyFeeParamsV1);
+
+impl FunctionParams for money_model::MoneyFeeParamsV1 {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let res = PyDict::new(py);
+        res.set_item("input", self.input.to_pydict(py)?)?;
+        res.set_item("output", self.output.to_pydict(py)?)?;
+        res.set_item("fee_value_blind", self.fee_value_blind.to_string())?;
+        res.set_item("token_blind", self.token_blind.to_string())?;
+        Ok(res.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}input:").unwrap();
+        self.input.fmt_pretty(out, depth + 2)?;
+
+        writeln!(out, "{prefix}output:").unwrap();
+        self.output.fmt_pretty(out, depth + 2)?;
+
+        writeln!(out, "{prefix}fee_value_blind: {}", self.fee_value_blind).unwrap();
+        writeln!(out, "{prefix}token_blind: {}", self.token_blind).unwrap();
+        Ok(())
+    }
+}

+ 57 - 0
src/sdk/python/src/contract/money/genesis_mint_v1.rs

@@ -0,0 +1,57 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_money_contract::model as money_model;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`money_model::MoneyGenesisMintParamsV1`] python binding.
+#[pyclass]
+pub struct MoneyGenesisMintParamsV1(money_model::MoneyGenesisMintParamsV1);
+impl_py_methods!(MoneyGenesisMintParamsV1);
+
+impl FunctionParams for money_model::MoneyGenesisMintParamsV1 {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("input", self.input.to_pydict(py)?)?;
+        dict.set_item(
+            "outputs",
+            self.outputs
+                .iter()
+                .map(|output| output.to_pydict(py))
+                .collect::<PyResult<Vec<Py<PyDict>>>>()?,
+        )?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}input:").unwrap();
+        self.input.fmt_pretty(out, depth + 2)?;
+        writeln!(out, "{prefix}outputs:").unwrap();
+
+        for output in &self.outputs {
+            output.fmt_pretty(out, depth + 2)?;
+            writeln!(out).unwrap();
+        }
+        Ok(())
+    }
+}

+ 203 - 0
src/sdk/python/src/contract/money/mod.rs

@@ -0,0 +1,203 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_money_contract::{model as money_model, MoneyFunction};
+use darkfi_sdk::crypto::util::FieldElemAsStr;
+use darkfi_serial::deserialize;
+use pyo3::{
+    prelude::{PyAnyMethods, PyDictMethods, PyModule, PyModuleMethods},
+    pyclass,
+    types::PyDict,
+    Bound, Py, PyResult, Python,
+};
+
+use crate::crypto::AeadEncryptedNote;
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`MoneyFunction::AuthTokenFreezeV1`] function call parameter's python bindings.
+pub mod auth_token_freeze_v1;
+pub use auth_token_freeze_v1::MoneyAuthTokenFreezeParamsV1;
+
+/// [`MoneyFunction::AuthTokenMintV1`] function call parameter's python bindings.
+pub mod auth_token_mint_v1;
+pub use auth_token_mint_v1::MoneyAuthTokenMintParamsV1;
+
+/// [`MoneyFunction::FeeV1`] function call parameter's python bindings.
+pub mod fee_v1;
+pub use fee_v1::MoneyFeeParamsV1;
+
+/// [`MoneyFunction::GenesisMintV1`] function call parameter's python bindings.
+pub mod genesis_mint_v1;
+pub use genesis_mint_v1::MoneyGenesisMintParamsV1;
+
+/// [`MoneyFunction::PoWRewardV1`] function call parameter's python bindings.
+pub mod pow_reward_v1;
+pub use pow_reward_v1::MoneyPoWRewardParamsV1;
+
+/// [`MoneyFunction::TokenMintV1`] function call parameter's bindings.
+pub mod token_mint_v1;
+pub use token_mint_v1::MoneyTokenMintParamsV1;
+
+/// [`MoneyFunction::TransferV1`] function call parameter's bindings.
+pub mod transfer_v1;
+pub use transfer_v1::MoneyTransferParamsV1;
+
+/// Decodes the parameters of a Money contract function call.
+pub fn decode_money_function_params(
+    function_index: u8,
+    data: &[u8],
+) -> darkfi::Result<Box<dyn FunctionParams>> {
+    let res: Box<dyn FunctionParams> = match MoneyFunction::try_from(function_index)? {
+        MoneyFunction::FeeV1 => {
+            let params: money_model::MoneyFeeParamsV1 = deserialize(&data[9..])?;
+            Box::new(params)
+        }
+        MoneyFunction::GenesisMintV1 => {
+            let params: money_model::MoneyGenesisMintParamsV1 = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+        MoneyFunction::PoWRewardV1 => {
+            let params: money_model::MoneyPoWRewardParamsV1 = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+        MoneyFunction::TransferV1 | MoneyFunction::OtcSwapV1 => {
+            let params: money_model::MoneyTransferParamsV1 = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+        MoneyFunction::AuthTokenMintV1 => {
+            let params: money_model::MoneyAuthTokenMintParamsV1 = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+        MoneyFunction::AuthTokenFreezeV1 => {
+            let params: money_model::MoneyAuthTokenFreezeParamsV1 = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+        MoneyFunction::TokenMintV1 => {
+            let params: money_model::MoneyTokenMintParamsV1 = deserialize(&data[1..])?;
+            Box::new(params)
+        }
+    };
+
+    Ok(res)
+}
+
+/// [`money_model::Input`] python binding
+#[pyclass]
+pub struct Input(money_model::Input);
+impl_py_methods!(Input);
+
+impl FunctionParams for money_model::Input {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("value_commit", format!("{:?}", self.value_commit))?;
+        dict.set_item("token_commit", self.token_commit.to_string())?;
+        dict.set_item("nullifier", self.nullifier.to_string())?;
+        dict.set_item("merkle_root", self.merkle_root.to_string())?;
+        dict.set_item("user_data_enc", self.user_data_enc.to_string())?;
+        dict.set_item("signature_public", self.signature_public.to_string())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}value_commit: {:?}", self.value_commit).unwrap();
+        writeln!(out, "{prefix}token_commit: {}", self.token_commit.to_string()).unwrap();
+        writeln!(out, "{prefix}nullifier: {}", self.nullifier).unwrap();
+        writeln!(out, "{prefix}merkle_root: {}", self.merkle_root).unwrap();
+        writeln!(out, "{prefix}user_data_enc: {}", self.user_data_enc.to_string()).unwrap();
+        writeln!(out, "{prefix}signature_public: {}", self.signature_public).unwrap();
+        Ok(())
+    }
+}
+
+/// [`money_model::Output`] python binding
+#[pyclass]
+pub struct Output(money_model::Output);
+impl_py_methods!(Output);
+
+impl FunctionParams for money_model::Output {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("value_commit", format!("{:?}", self.value_commit))?;
+        dict.set_item("token_commit", self.token_commit.to_string())?;
+        dict.set_item("coin", self.coin.to_string())?;
+        dict.set_item("note", self.note.to_pydict(py)?)?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}value_commit: {:?}", self.value_commit).unwrap();
+        writeln!(out, "{prefix}token_commit: {}", self.token_commit.to_string()).unwrap();
+        writeln!(out, "{prefix}coin: {}", self.coin).unwrap();
+        writeln!(out, "{prefix}note:").unwrap();
+        self.note.fmt_pretty(out, depth + 2)?;
+        Ok(())
+    }
+}
+
+/// [`money_model::ClearInput`] python binding
+#[pyclass]
+pub struct ClearInput(money_model::ClearInput);
+impl_py_methods!(ClearInput);
+
+impl FunctionParams for money_model::ClearInput {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("value", self.value)?;
+        dict.set_item("token_id", self.token_id.to_string())?;
+        dict.set_item("value_blind", self.value_blind.to_string())?;
+        dict.set_item("token_blind", self.token_blind.to_string())?;
+        dict.set_item("signature_public", self.signature_public.to_string())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}value: {}", self.value).unwrap();
+        writeln!(out, "{prefix}token_id: {}", self.token_id).unwrap();
+        writeln!(out, "{prefix}value_blind: {}", self.value_blind).unwrap();
+        writeln!(out, "{prefix}token_blind: {}", self.token_blind).unwrap();
+        writeln!(out, "{prefix}signature_public: {}", self.signature_public).unwrap();
+        Ok(())
+    }
+}
+
+/// Create money module and provide the python bindings.
+pub fn create_module(py: Python) -> PyResult<Bound<PyModule>> {
+    let submod = PyModule::new(py, "money")?;
+
+    submod.add_class::<MoneyAuthTokenFreezeParamsV1>()?;
+    submod.add_class::<MoneyAuthTokenMintParamsV1>()?;
+    submod.add_class::<MoneyFeeParamsV1>()?;
+    submod.add_class::<MoneyGenesisMintParamsV1>()?;
+    submod.add_class::<MoneyPoWRewardParamsV1>()?;
+    submod.add_class::<MoneyTokenMintParamsV1>()?;
+    submod.add_class::<MoneyTransferParamsV1>()?;
+    submod.add_class::<Input>()?;
+    submod.add_class::<Output>()?;
+    submod.add_class::<ClearInput>()?;
+    submod.add_class::<AeadEncryptedNote>()?;
+
+    py.import("sys")?.getattr("modules")?.set_item("darkfi_sdk.contract.money", &submod)?;
+
+    Ok(submod)
+}

+ 47 - 0
src/sdk/python/src/contract/money/pow_reward_v1.rs

@@ -0,0 +1,47 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_money_contract::model as money_model;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`money_model::MoneyPoWRewardParamsV1`] python binding.
+#[pyclass]
+pub struct MoneyPoWRewardParamsV1(money_model::MoneyPoWRewardParamsV1);
+impl_py_methods!(MoneyPoWRewardParamsV1);
+
+impl FunctionParams for money_model::MoneyPoWRewardParamsV1 {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("input", self.input.to_pydict(py)?)?;
+        dict.set_item("output", self.output.to_pydict(py)?)?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}input:").unwrap();
+        self.input.fmt_pretty(out, depth + 2)?;
+        writeln!(out, "{prefix}output:").unwrap();
+        self.output.fmt_pretty(out, depth + 2)?;
+        Ok(())
+    }
+}

+ 43 - 0
src/sdk/python/src/contract/money/token_mint_v1.rs

@@ -0,0 +1,43 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_money_contract::model as money_model;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`money_model::MoneyTokenMintParamsV1`] python binding.
+#[pyclass]
+pub struct MoneyTokenMintParamsV1(money_model::MoneyTokenMintParamsV1);
+impl_py_methods!(MoneyTokenMintParamsV1);
+
+impl FunctionParams for money_model::MoneyTokenMintParamsV1 {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("coin", self.coin.to_string())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}coin: {}", self.coin).unwrap();
+        Ok(())
+    }
+}

+ 67 - 0
src/sdk/python/src/contract/money/transfer_v1.rs

@@ -0,0 +1,67 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the impl FunctionParams foried warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_money_contract::model as money_model;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`money_model::MoneyTransferParamsV1`] python binding.
+#[pyclass]
+pub struct MoneyTransferParamsV1(money_model::MoneyTransferParamsV1);
+impl_py_methods!(MoneyTransferParamsV1);
+
+impl FunctionParams for money_model::MoneyTransferParamsV1 {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item(
+            "inputs",
+            self.inputs
+                .iter()
+                .map(|input| input.to_pydict(py))
+                .collect::<PyResult<Vec<Py<PyDict>>>>()?,
+        )?;
+        dict.set_item(
+            "outputs",
+            self.outputs
+                .iter()
+                .map(|output| output.to_pydict(py))
+                .collect::<PyResult<Vec<Py<PyDict>>>>()?,
+        )?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}inputs:").unwrap();
+        for input in &self.inputs {
+            input.fmt_pretty(out, depth + 2)?;
+            writeln!(out).unwrap();
+        }
+
+        writeln!(out, "{prefix}outputs:").unwrap();
+
+        for output in &self.outputs {
+            output.fmt_pretty(out, depth + 2)?;
+            writeln!(out).unwrap();
+        }
+        Ok(())
+    }
+}

+ 76 - 4
src/sdk/python/src/crypto.rs

@@ -16,14 +16,18 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::ops::Deref;
+use std::{fmt::Write, ops::Deref};
 
-use darkfi_sdk::{crypto, pasta::pallas};
+use darkfi_sdk::{crypto, crypto::util::FieldElemAsStr, hex::AsHex, pasta::pallas};
 use pyo3::{
-    prelude::{PyModule, PyModuleMethods},
-    pyfunction, wrap_pyfunction, Bound, PyResult, Python,
+    prelude::{PyDictMethods, PyModule, PyModuleMethods},
+    pyclass, pyfunction, pymethods,
+    types::PyDict,
+    wrap_pyfunction, Bound, Py, PyResult, Python,
 };
 
+use crate::contract::{impl_py_methods, FunctionParams};
+
 use super::pasta::{Ep, Fp, Fq};
 
 /// Calculate the Poseidon hash of given `Fp` elements.
@@ -66,6 +70,74 @@ pub fn pedersen_commitment_base(value: &Bound<Fp>, blind: &Bound<Fq>) -> Ep {
     ))
 }
 
+/// [`crypto::schnorr::Signature`] python binding
+#[pyclass]
+pub struct Signature(pub crypto::schnorr::Signature);
+
+#[pymethods]
+impl Signature {
+    pub fn __str__(&self) -> String {
+        format!("{:?}", self.0)
+    }
+}
+
+/// [`crypto::note::AeadEncryptedNote`] python binding
+#[pyclass]
+pub struct AeadEncryptedNote(crypto::note::AeadEncryptedNote);
+impl_py_methods!(AeadEncryptedNote);
+
+impl FunctionParams for crypto::note::AeadEncryptedNote {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("ephem_public", self.ephem_public.to_string())?;
+        dict.set_item("ciphertext", self.ciphertext.hex())?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}ephem_public: {}", self.ephem_public).unwrap();
+        writeln!(out, "{prefix}ciphertext: [{} bytes]", self.ciphertext.len()).unwrap();
+        Ok(())
+    }
+}
+
+macro_rules! el_gamal_encrypted_note_binding {
+    ($name: ident, $typ:literal) => {
+        #[pyo3::pyclass]
+        pub struct $name(crypto::note::ElGamalEncryptedNote<$typ>);
+
+        crate::impl_py_methods!($name);
+    };
+}
+
+el_gamal_encrypted_note_binding!(ElGamalEncryptedNote3, 3);
+el_gamal_encrypted_note_binding!(ElGamalEncryptedNote4, 4);
+el_gamal_encrypted_note_binding!(ElGamalEncryptedNote5, 5);
+
+impl<const N: usize> FunctionParams for crypto::note::ElGamalEncryptedNote<N> {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item("ephem_public", self.ephem_public.to_string())?;
+        dict.set_item(
+            "encrypted_values",
+            self.encrypted_values.iter().map(|b| b.to_string()).collect::<Vec<_>>(),
+        )?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}ephem_public: {}", self.ephem_public).unwrap();
+        writeln!(out, "{prefix}encrypted_values:").unwrap();
+
+        for value in &self.encrypted_values {
+            writeln!(out, "   {prefix}{}", value.to_string()).unwrap();
+        }
+        Ok(())
+    }
+}
+
 /// Wrapper function for creating this Python module.
 pub(crate) fn create_module(py: Python<'_>) -> PyResult<Bound<PyModule>> {
     let submod = PyModule::new(py, "crypto")?;

+ 20 - 4
src/sdk/python/src/lib.rs

@@ -16,6 +16,8 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use pyo3::prelude::PyAnyMethods;
+
 /// Pallas and Vesta curves
 mod pasta;
 
@@ -28,25 +30,39 @@ mod crypto;
 /// zkas definitions
 mod zkas;
 
+/// Contract definitions
+mod contract;
+
+/// Transaction definitions
+mod tx;
+
 #[pyo3::prelude::pymodule]
 fn darkfi_sdk(
     py: pyo3::Python<'_>,
     m: &pyo3::Bound<'_, pyo3::prelude::PyModule>,
 ) -> pyo3::PyResult<()> {
     let submodule = pasta::create_module(py)?;
-    pyo3::py_run!(py, submodule, "import sys; sys.modules['darkfi_sdk.pasta'] = submodule");
+    py.import("sys")?.getattr("modules")?.set_item("darkfi_sdk.pasta", &submodule)?;
     pyo3::types::PyModuleMethods::add_submodule(m, &submodule)?;
 
     let submodule = merkle::create_module(py)?;
-    pyo3::py_run!(py, submodule, "import sys; sys.modules['darkfi_sdk.merkle'] = submodule");
+    py.import("sys")?.getattr("modules")?.set_item("darkfi_sdk.merkle", &submodule)?;
     pyo3::types::PyModuleMethods::add_submodule(m, &submodule)?;
 
     let submodule = crypto::create_module(py)?;
-    pyo3::py_run!(py, submodule, "import sys; sys.modules['darkfi_sdk.crypto'] = submodule");
+    py.import("sys")?.getattr("modules")?.set_item("darkfi_sdk.crypto", &submodule)?;
     pyo3::types::PyModuleMethods::add_submodule(m, &submodule)?;
 
     let submodule = zkas::create_module(py)?;
-    pyo3::py_run!(py, submodule, "import sys; sys.modules['darkfi_sdk.zkas'] = submodule");
+    py.import("sys")?.getattr("modules")?.set_item("darkfi_sdk.zkas", &submodule)?;
+    pyo3::types::PyModuleMethods::add_submodule(m, &submodule)?;
+
+    let submodule = tx::create_module(py)?;
+    py.import("sys")?.getattr("modules")?.set_item("darkfi_sdk.tx", &submodule)?;
+    pyo3::types::PyModuleMethods::add_submodule(m, &submodule)?;
+
+    let submodule = contract::create_module(py)?;
+    py.import("sys")?.getattr("modules")?.set_item("darkfi_sdk.contract", &submodule)?;
     pyo3::types::PyModuleMethods::add_submodule(m, &submodule)?;
 
     Ok(())

+ 177 - 0
src/sdk/python/src/tx.rs

@@ -0,0 +1,177 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi::tx::{MAX_TX_CALLS, MIN_TX_CALLS};
+use darkfi_sdk::dark_tree::dark_forest_leaf_vec_integrity_check;
+use darkfi_serial::deserialize;
+use pyo3::{
+    exceptions::PyValueError,
+    prelude::{PyDictMethods, PyModule, PyModuleMethods},
+    pyclass, pymethods,
+    types::PyDict,
+    Bound, Py, PyResult, Python,
+};
+
+use super::{
+    contract::{ContractCall, DarkLeafContractCall},
+    crypto::Signature,
+    zkas::Proof,
+};
+
+/// Class representing a transaction
+#[pyclass]
+pub struct Transaction(darkfi::tx::Transaction);
+
+#[pymethods]
+impl Transaction {
+    #[staticmethod]
+    pub fn decode(data: Vec<u8>) -> PyResult<Self> {
+        let tx: darkfi::tx::Transaction = deserialize(&data)?;
+        dark_forest_leaf_vec_integrity_check(&tx.calls, Some(MIN_TX_CALLS), Some(MAX_TX_CALLS))
+            .map_err(|e| {
+                PyValueError::new_err(format!(
+                    "Invalid Transaction, contract call integrity check failed: {e}"
+                ))
+            })?;
+        Ok(Self(tx))
+    }
+
+    pub fn hash(&self) -> TransactionHash {
+        TransactionHash(self.0.hash())
+    }
+
+    pub fn proofs(&self) -> Vec<Vec<Proof>> {
+        self.0.proofs.iter().map(|inner| inner.iter().map(|p| Proof(p.clone())).collect()).collect()
+    }
+
+    pub fn signatures(&self) -> Vec<Vec<Signature>> {
+        self.0
+            .signatures
+            .iter()
+            .map(|inner| inner.iter().map(|s| Signature(*s)).collect())
+            .collect()
+    }
+
+    pub fn calls(&self) -> Vec<DarkLeafContractCall> {
+        self.0.calls.iter().map(|leaf| DarkLeafContractCall(leaf.clone())).collect()
+    }
+
+    /// Returns the transaction represented as a Python dictionary.
+    #[getter]
+    pub fn __dict__(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        let mut calls = vec![];
+        for (i, call) in self.calls().iter().enumerate() {
+            let call_dict = PyDict::new(py);
+            call_dict.set_item("parent_index", call.parent_index())?;
+            call_dict.set_item("children_indexes", call.children_indexes())?;
+
+            let call_data = call.data();
+
+            call_dict.set_item("contract_id", call_data.contract_id())?;
+            call_dict.set_item("contract_name", call_data.contract_name())?;
+            call_dict.set_item("function_index", call_data.function_index())?;
+            call_dict.set_item("function_name", call_data.function_name())?;
+            call_dict.set_item("function_params", call_data.function_params_dict(py)?)?;
+            call_dict.set_item("proofs", self.0.proofs.get(i).unwrap().iter().len())?;
+            call_dict.set_item(
+                "signatures",
+                self.0
+                    .signatures
+                    .get(i)
+                    .unwrap()
+                    .iter()
+                    .map(|s| format!("{s:?}"))
+                    .collect::<Vec<String>>(),
+            )?;
+            calls.push(call_dict);
+        }
+
+        dict.set_item("hash", self.0.hash().as_string())?;
+        dict.set_item("calls", calls)?;
+        Ok(dict.unbind())
+    }
+
+    /// A formatted text representation of a transaction, showing
+    /// function calls in their call hierarchy.
+    pub fn __str__(&self) -> PyResult<String> {
+        let mut out = String::new();
+        writeln!(out, "hash: {}\n", self.0.hash()).unwrap();
+
+        let depth = self.compute_depth();
+        for (i, call) in self.0.calls.iter().enumerate().rev() {
+            write!(out, "{}", self.format_call(&ContractCall(call.data.clone()), depth[i])?)
+                .unwrap();
+        }
+
+        Ok(out)
+    }
+}
+
+impl Transaction {
+    /// Calculate the depth of each node in the call tree.
+    fn compute_depth(&self) -> Vec<usize> {
+        let mut depth = vec![0; self.0.calls.len()];
+
+        for (i, call) in self.0.calls.iter().enumerate().rev() {
+            if let Some(parent) = call.parent_index {
+                depth[i] = depth[parent] + 1;
+            }
+        }
+
+        depth
+    }
+
+    fn format_call(&self, call: &ContractCall, depth: usize) -> PyResult<String> {
+        let mut out = String::new();
+        let prefix = format!("{}├─ ", "   ".repeat(depth + 1));
+        writeln!(out, "{}⊟ Contract Call", "    ".repeat(depth)).unwrap();
+        writeln!(out, "{}━━━━━━━━━━━━━━━", "    ".repeat(depth)).unwrap();
+
+        writeln!(out, "{}contract id: {}", prefix, call.contract_id()).unwrap();
+        writeln!(out, "{}contract_name: {}", prefix, call.contract_name().unwrap_or_default())
+            .unwrap();
+        writeln!(out, "{}function_index: {}", prefix, call.function_index()).unwrap();
+        writeln!(out, "{}function_name: {}", prefix, call.function_name().unwrap_or_default())
+            .unwrap();
+        writeln!(out, "{}function_params:\n{}", prefix, call.function_params_str(depth + 2)?)
+            .unwrap();
+
+        Ok(out)
+    }
+}
+
+#[pyclass]
+pub struct TransactionHash(darkfi_sdk::tx::TransactionHash);
+
+#[pymethods]
+impl TransactionHash {
+    pub fn __str__(&self) -> String {
+        self.0.to_string()
+    }
+}
+
+pub fn create_module(py: Python<'_>) -> PyResult<Bound<PyModule>> {
+    let submod = PyModule::new(py, "tx")?;
+
+    submod.add_class::<Transaction>()?;
+
+    Ok(submod)
+}

+ 1 - 1
src/sdk/python/src/zkas.rs

@@ -257,7 +257,7 @@ impl ProvingKey {
 
 #[pyclass]
 /// A zkVM proof
-pub struct Proof(zk::proof::Proof);
+pub struct Proof(pub(crate) zk::proof::Proof);
 
 #[pymethods]
 impl Proof {