Skip to content

Commit

Permalink
ScriptBuilder (#108)
Browse files Browse the repository at this point in the history
* Python ScriptBuilder

* address_from_script_public_key py util fn

* remove unused

* tx examples

* .pyi file

* script builder comment

* lint
  • Loading branch information
smartgoo authored Oct 15, 2024
1 parent c41b17c commit a1ab06f
Show file tree
Hide file tree
Showing 15 changed files with 346 additions and 8 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions consensus/client/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ pub fn address_from_script_public_key(script_public_key: &ScriptPublicKeyT, netw
}
}

#[cfg(feature = "py-sdk")]
#[pyfunction]
#[pyo3(name = "address_from_script_public_key")]
pub fn address_from_script_public_key_py(script_public_key: &ScriptPublicKey, network: &str) -> PyResult<Address> {
match standard::extract_script_pub_key_address(script_public_key, network.try_into()?) {
Ok(address) => Ok(address),
Err(err) => Err(pyo3::exceptions::PyException::new_err(format!("{}", err))),
}
}

/// Returns true if the script passed is a pay-to-pubkey.
/// @param script - The script ({@link HexString} or Uint8Array).
/// @category Wallet SDK
Expand Down
3 changes: 3 additions & 0 deletions crypto/txscript/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ license.workspace = true
repository.workspace = true

[features]
py-sdk = ["pyo3"]
wasm32-core = []
wasm32-sdk = []

[dependencies]
blake2b_simd.workspace = true
borsh.workspace = true
cfg-if.workspace = true
faster-hex.workspace = true
hexplay.workspace = true
indexmap.workspace = true
itertools.workspace = true
Expand All @@ -28,6 +30,7 @@ kaspa-utils.workspace = true
kaspa-wasm-core.workspace = true
log.workspace = true
parking_lot.workspace = true
pyo3 = { workspace = true, optional = true }
rand.workspace = true
secp256k1.workspace = true
serde_json.workspace = true
Expand Down
9 changes: 9 additions & 0 deletions crypto/txscript/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::script_builder;
#[cfg(feature = "py-sdk")]
use pyo3::{exceptions::PyException, prelude::PyErr};
use thiserror::Error;
use wasm_bindgen::{JsError, JsValue};
use workflow_wasm::jserror::JsErrorData;
Expand Down Expand Up @@ -87,3 +89,10 @@ impl From<serde_wasm_bindgen::Error> for Error {
Self::SerdeWasmBindgen(JsValue::from(err).into())
}
}

#[cfg(feature = "py-sdk")]
impl From<Error> for PyErr {
fn from(value: Error) -> Self {
PyException::new_err(value.to_string())
}
}
4 changes: 3 additions & 1 deletion crypto/txscript/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ pub mod caches;
mod data_stack;
pub mod error;
pub mod opcodes;
#[cfg(feature = "py-sdk")]
pub mod python;
pub mod result;
pub mod script_builder;
pub mod script_class;
pub mod standard;
#[cfg(feature = "wasm32-sdk")]
#[cfg(any(feature = "wasm32-sdk", feature = "py-sdk"))]
pub mod wasm;

use crate::caches::Cache;
Expand Down
184 changes: 184 additions & 0 deletions crypto/txscript/src/python/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
use crate::result::Result;
use crate::wasm::opcodes::Opcodes;
use crate::{script_builder as native, standard};
use faster_hex::hex_decode;
use kaspa_consensus_core::tx::ScriptPublicKey;
use kaspa_utils::hex::ToHex;
use pyo3::prelude::*;
use std::sync::{Arc, Mutex, MutexGuard};

#[derive(Clone)]
#[pyclass]
pub struct ScriptBuilder {
script_builder: Arc<Mutex<native::ScriptBuilder>>,
}

impl ScriptBuilder {
#[inline]
pub fn inner(&self) -> MutexGuard<native::ScriptBuilder> {
self.script_builder.lock().unwrap()
}
}

impl Default for ScriptBuilder {
fn default() -> Self {
Self { script_builder: Arc::new(Mutex::new(native::ScriptBuilder::new())) }
}
}

#[pymethods]
impl ScriptBuilder {
#[new]
pub fn new() -> Self {
Self::default()
}

#[staticmethod]
pub fn from_script(script: Bound<PyAny>) -> PyResult<ScriptBuilder> {
let builder = ScriptBuilder::default();
let script = PyBinary::try_from(script)?;
builder.inner().extend(&script.data);

Ok(builder)
}

pub fn add_op(&self, op: Bound<PyAny>) -> PyResult<ScriptBuilder> {
let op = extract_ops(op)?;
let mut inner = self.inner();
inner.add_op(op[0]).map_err(|err| pyo3::exceptions::PyException::new_err(format!("{}", err)))?;

Ok(self.clone())
}

pub fn add_ops(&self, opcodes: Bound<PyAny>) -> PyResult<ScriptBuilder> {
let ops = extract_ops(opcodes)?;
self.inner().add_ops(&ops.as_slice()).map_err(|err| pyo3::exceptions::PyException::new_err(format!("{}", err)))?;

Ok(self.clone())
}

pub fn add_data(&self, data: Bound<PyAny>) -> PyResult<ScriptBuilder> {
let data = PyBinary::try_from(data)?;

let mut inner = self.inner();
inner.add_data(&data.data).map_err(|err| pyo3::exceptions::PyException::new_err(format!("{}", err)))?;

Ok(self.clone())
}

pub fn add_i64(&self, value: i64) -> PyResult<ScriptBuilder> {
let mut inner = self.inner();
inner.add_i64(value).map_err(|err| pyo3::exceptions::PyException::new_err(format!("{}", err)))?;

Ok(self.clone())
}

pub fn add_lock_time(&self, lock_time: u64) -> PyResult<ScriptBuilder> {
let mut inner = self.inner();
inner.add_lock_time(lock_time).map_err(|err| pyo3::exceptions::PyException::new_err(format!("{}", err)))?;

Ok(self.clone())
}

pub fn add_sequence(&self, sequence: u64) -> PyResult<ScriptBuilder> {
let mut inner = self.inner();
inner.add_sequence(sequence).map_err(|err| pyo3::exceptions::PyException::new_err(format!("{}", err)))?;

Ok(self.clone())
}

#[staticmethod]
pub fn canonical_data_size(data: Bound<PyAny>) -> PyResult<u32> {
let data = PyBinary::try_from(data)?;
let size = native::ScriptBuilder::canonical_data_size(&data.data.as_slice()) as u32;

Ok(size)
}

pub fn to_string(&self) -> String {
let inner = self.inner();

inner.script().to_vec().iter().map(|b| format!("{:02x}", b)).collect()
}

pub fn drain(&self) -> String {
let mut inner = self.inner();

String::from_utf8(inner.drain()).unwrap()
}

#[pyo3(name = "create_pay_to_script_hash_script")]
pub fn pay_to_script_hash_script(&self) -> ScriptPublicKey {
let inner = self.inner();
let script = inner.script();

standard::pay_to_script_hash_script(script)
}

#[pyo3(name = "encode_pay_to_script_hash_signature_script")]
pub fn pay_to_script_hash_signature_script(&self, signature: String) -> Result<String> {
let inner = self.inner();
let script = inner.script();
let signature = signature.as_bytes();
let generated_script = standard::pay_to_script_hash_signature_script(script.into(), signature.to_vec())?;

Ok(generated_script.to_hex().into())
}

// pub fn hex_view(&self, args: Option<HexViewConfigT>) -> Result<String> {
// let inner = self.inner();
// let script = inner.script();

// let config = args.map(HexViewConfig::try_from).transpose()?.unwrap_or_default();
// Ok(config.build(script).to_string())
// }
}

fn extract_ops(input: Bound<PyAny>) -> PyResult<Vec<u8>> {
if let Ok(opcode) = extract_op(&input) {
// Single u8 or Opcodes variant
Ok(vec![opcode])
} else if let Ok(list) = input.downcast::<pyo3::types::PyList>() {
// List of u8 or Opcodes variants
list.iter().map(|item| extract_op(&item)).collect::<PyResult<Vec<u8>>>()
} else {
Err(pyo3::exceptions::PyTypeError::new_err("Expected an Opcodes enum or an integer."))
}
}

fn extract_op(item: &Bound<PyAny>) -> PyResult<u8> {
if let Ok(op) = item.extract::<u8>() {
Ok(op)
} else if let Ok(op) = item.extract::<Opcodes>() {
Ok(op.value())
} else {
Err(pyo3::exceptions::PyTypeError::new_err("Expected Opcodes variant or u8"))
}
}

struct PyBinary {
pub data: Vec<u8>,
}

impl TryFrom<Bound<'_, PyAny>> for PyBinary {
type Error = PyErr;
fn try_from(value: Bound<PyAny>) -> Result<Self, Self::Error> {
if let Ok(str) = value.extract::<String>() {
// Python `str` (of valid hex)
let mut data = vec![0u8; str.len() / 2];
match hex_decode(str.as_bytes(), &mut data) {
Ok(()) => Ok(PyBinary { data }), // Hex string
Err(_) => Err(pyo3::exceptions::PyValueError::new_err("Invalid hex string")),
}
} else if let Ok(py_bytes) = value.downcast::<pyo3::types::PyBytes>() {
// Python `bytes` type
Ok(PyBinary { data: py_bytes.as_bytes().to_vec() })
} else if let Ok(op_list) = value.downcast::<pyo3::types::PyList>() {
// Python `[int]` (list of bytes)
let data = op_list.iter().map(|item| item.extract::<u8>().unwrap()).collect();
Ok(PyBinary { data })
} else {
Err(pyo3::exceptions::PyTypeError::new_err("Expected `str` (of valid hex), `bytes`, `int` (u8), or `[int]` (u8)"))
}
}
}
9 changes: 9 additions & 0 deletions crypto/txscript/src/python/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use cfg_if::cfg_if;

cfg_if! {
if #[cfg(feature = "py-sdk")] {
pub mod builder;

pub use self::builder::*;
}
}
2 changes: 1 addition & 1 deletion crypto/txscript/src/script_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl ScriptBuilder {
&self.script
}

#[cfg(any(test, target_arch = "wasm32"))]
#[cfg(any(test, target_arch = "wasm32", feature = "py-sdk"))]
pub fn extend(&mut self, data: &[u8]) {
self.script.extend(data);
}
Expand Down
2 changes: 2 additions & 0 deletions crypto/txscript/src/wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ cfg_if! {

pub use self::opcodes::*;
pub use self::builder::*;
} else if #[cfg(feature = "py-sdk")] {
pub mod opcodes;
}
}
13 changes: 13 additions & 0 deletions crypto/txscript/src/wasm/opcodes.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#[cfg(feature = "py-sdk")]
use pyo3::prelude::*;
pub use wasm_bindgen::prelude::*;

/// Kaspa Transaction Script Opcodes
/// @see {@link ScriptBuilder}
/// @category Consensus
#[derive(Clone)]
#[cfg_attr(feature = "py-sdk", pyclass)]
#[wasm_bindgen]
pub enum Opcodes {
OpFalse = 0x00,
Expand Down Expand Up @@ -294,3 +298,12 @@ pub enum Opcodes {
OpPubKey = 0xfe,
OpInvalidOpCode = 0xff,
}

#[cfg(feature = "py-sdk")]
#[pymethods]
impl Opcodes {
#[getter]
pub fn value(&self) -> u8 {
self.clone() as u8
}
}
2 changes: 2 additions & 0 deletions python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ kaspa-bip32.workspace = true
kaspa-consensus-core.workspace = true
kaspa-consensus-client.workspace = true
kaspa-hashes.workspace = true
kaspa-txscript.workspace = true
kaspa-wallet-core.workspace = true
kaspa-wallet-keys.workspace = true
kaspa-wrpc-python.workspace = true
Expand All @@ -28,6 +29,7 @@ py-sdk = [
"pyo3/extension-module",
"kaspa-addresses/py-sdk",
"kaspa-consensus-client/py-sdk",
"kaspa-txscript/py-sdk",
"kaspa-wallet-keys/py-sdk",
"kaspa-wallet-core/py-sdk",
"kaspa-wrpc-python/py-sdk",
Expand Down
Loading

0 comments on commit a1ab06f

Please sign in to comment.