Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ScriptBuilder #108

Merged
merged 7 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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