Skip to content

Commit

Permalink
Python dict type for RPC method arg/return values (kaspanet#60)
Browse files Browse the repository at this point in the history
* Python dictionary for request and response

* readme update

* to_keypair block moves
  • Loading branch information
smartgoo committed Sep 17, 2024
1 parent 816fc35 commit c9217ed
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 94 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

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

22 changes: 22 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Python bindings for Rusty Kaspa
Rusty-Kaspa/Rust bindings for Python, using [PyO3](https://pyo3.rs/v0.20.0/) and [Maturin](https://www.maturin.rs). The result is a Python package that exposes rusty-kaspa/rust source for use in Python programs.

# Building from Source
1. Ensure Python 3.8 or higher (`python --version`) is installed. [*TODO validate 3.8 or higher is correct*]. Python installers can be found on [python.org](https://www.python.org).
2. `cd ./python`
3. Create Python virtual environment: `python -m venv env`
4. Activate Python virtual env:
- Unix-based systems: `source env/bin/activate`
- Windows: `env/scripts/activate.bat`
5. Install `maturin` build tool: `pip install maturin`
6. Build Python package with Maturin:
- For local development, build and install in active Python virtual env: `maturin develop --release --features py-sdk`
- To build source and built (wheel) distributions: `maturin build --release --strip --sdist --features py-sdk`

# Usage from Python
See Python files in `./python/examples`.

# Project Layout
The Python package `kaspapy` is built from the `kaspa-python` crate, which is located at `./python`.

As such, the `kaspapy` function in `./python/src/lib.rs` is a good starting point. This function uses PyO3 to add functionality to the package.
File renamed without changes.
29 changes: 29 additions & 0 deletions python/examples/rpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import asyncio
import json
import time

from kaspapy import RpcClient


async def main():
client = await RpcClient.connect(url = "ws://localhost:17110")
print(f'Client is connected: {client.is_connected()}')

get_server_info_response = await client.get_server_info()
print(get_server_info_response)

block_dag_info_response = await client.get_block_dag_info()
print(block_dag_info_response)

tip_hash = block_dag_info_response['tipHashes'][0]
get_block_request = {'hash': tip_hash, 'includeTransactions': True}
get_block_response = await client.get_block_call(get_block_request)
print(get_block_response)

get_balances_by_addresses_request = {'addresses': ['kaspa:qqxn4k5dchwk3m207cmh9ewagzlwwvfesngkc8l90tj44mufcgmujpav8hakt', 'kaspa:qr5ekyld6j4zn0ngennj9nx5gpt3254fzs77ygh6zzkvyy8scmp97de4ln8v5']}
get_balances_by_addresses_response = await client.get_balances_by_addresses_call(get_balances_by_addresses_request)
print(get_balances_by_addresses_response)


if __name__ == "__main__":
asyncio.run(main())
File renamed without changes.
18 changes: 12 additions & 6 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@ build-backend = "maturin"

[project]
name = "kaspapy"
version = "0.0.1"
description = "Kaspa Python Bindings"
version = "0.1.0"
requires-python = ">=3.8"
license = "ISC"
# readme = "README.md"
# documentation = "https://your_documentation_link"
classifiers = [
"Programming Language :: Python",
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = []

[project.urls]
# homepage = ""
# documentation = ""
# repository = ""
# issues = ""
# changelog = ""

[package.metadata.maturin]
name = "kaspapy"
description = "Kaspa Python Bindings"

[tool.maturin]
name = "kaspapy"
# python-source = "python"
name = "kaspapy"
35 changes: 0 additions & 35 deletions python/tests/rpc.py

This file was deleted.

20 changes: 7 additions & 13 deletions rpc/macros/src/wrpc/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,17 @@ impl ToTokens for RpcTable {

#[pymethods]
impl RpcClient {
fn #fn_call(&self, py: Python) -> PyResult<Py<PyAny>> {
// Returns result as JSON string
fn #fn_call(&self, py: Python, request: Py<PyDict>) -> PyResult<Py<PyAny>> {
let client = self.client.clone();

// TODO - receive argument from Python and deserialize it
// explore https://docs.rs/serde-pyobject/latest/serde_pyobject/ for arg intake / return
let request : #request_type = serde_pyobject::from_pyobject(request.into_bound(py)).unwrap();

// TODO replace serde_json with serde_pyobject
let request : #request_type = serde_json::from_str("{}").map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;

let fut = async move {
let py_fut = pyo3_asyncio_0_21::tokio::future_into_py(py, async move {
let response : #response_type = client.#fn_call(request).await?;
// TODO - replace serde_json with serde_pyobject
serde_json::to_string(&response).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
};

let py_fut = pyo3_asyncio_0_21::tokio::future_into_py(py, fut)?;
Python::with_gil(|py| {
Ok(serde_pyobject::to_pyobject(py, &response).unwrap().to_object(py))
})
})?;

Python::with_gil(|py| Ok(py_fut.into_py(py)))
}
Expand Down
3 changes: 2 additions & 1 deletion rpc/wrpc/python/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "kaspa-wrpc-python"
description = "Kaspa gRPC Python client"
description = "Kaspa wRPC Python client"
rust-version.workspace = true
version.workspace = true
edition.workspace = true
Expand All @@ -26,6 +26,7 @@ kaspa-python-macros.workspace = true
pyo3.workspace = true
pyo3-asyncio-0-21.workspace = true
serde_json.workspace = true
serde-pyobject = "0.3.0"
thiserror.workspace = true
workflow-core.workspace = true
workflow-rpc.workspace = true
37 changes: 10 additions & 27 deletions rpc/wrpc/python/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ use kaspa_wrpc_client::{
client::{ConnectOptions, ConnectStrategy},
KaspaRpcClient, WrpcEncoding,
};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::{prelude::*, types::PyDict};
use std::time::Duration;

#[pyclass]
Expand Down Expand Up @@ -48,38 +47,22 @@ impl RpcClient {
}

fn get_server_info(&self, py: Python) -> PyResult<Py<PyAny>> {
// Returns result as JSON string
let client = self.client.clone();

let fut = async move {
let r = client.get_server_info().await?;
serde_json::to_string(&r).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
};

let py_fut = pyo3_asyncio_0_21::tokio::future_into_py(py, fut)?;

Python::with_gil(|py| Ok(py_fut.into_py(py)))
py_async! {py, async move {
let response = client.get_server_info_call(GetServerInfoRequest { }).await?;
Python::with_gil(|py| {
Ok(serde_pyobject::to_pyobject(py, &response).unwrap().to_object(py))
})
}}
}

// fn get_block_dag_info(&self, py: Python) -> PyResult<Py<PyAny>> {
// // Returns result as JSON string
// let client = self.client.clone();

// let fut = async move {
// let r = client.get_block_dag_info().await?;
// serde_json::to_string(&r).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
// };

// let py_fut = pyo3_asyncio_0_21::tokio::future_into_py(py, fut)?;

// Python::with_gil(|py| Ok(py_fut.into_py(py)))
// }

fn get_block_dag_info(&self, py: Python) -> PyResult<Py<PyAny>> {
let client = self.client.clone();
py_async! {py, async move {
let response = client.get_block_dag_info_call(GetBlockDagInfoRequest { }).await?;
serde_json::to_string(&response).map_err(|err| PyValueError::new_err(err.to_string()))
Python::with_gil(|py| {
Ok(serde_pyobject::to_pyobject(py, &response).unwrap().to_object(py))
})
}}
}
}
Expand Down
24 changes: 12 additions & 12 deletions wallet/keys/src/privatekey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ impl PrivateKey {
let address = Address::new(network.try_into()?, AddressVersion::PubKeyECDSA, &payload);
Ok(address)
}

/// Generate a [`Keypair`] from this [`PrivateKey`].
#[wasm_bindgen(js_name = toKeypair)]
pub fn to_keypair(&self) -> Result<Keypair, JsError> {
Keypair::from_private_key(self)
}
}

// PY-NOTE: fns exposed to both WASM and Python
Expand All @@ -91,13 +97,6 @@ impl PrivateKey {
use kaspa_utils::hex::ToHex;
self.secret_bytes().to_vec().to_hex()
}

// TODO-PY
// Generate a [`Keypair`] from this [`PrivateKey`].
// #[wasm_bindgen(js_name = toKeypair)]
// pub fn to_keypair(&self) -> Result<Keypair, JsError> {
// Keypair::from_private_key(self)
// }
}

// PY-NOTE: Python specific fn implementations
Expand All @@ -109,11 +108,6 @@ impl PrivateKey {
Ok(Self { inner: secp256k1::SecretKey::from_str(key)? })
}

// #[wasm_bindgen(js_name = toKeypair)]
// pub fn to_keypair(&self) -> Result<Keypair, JsError> {
// Keypair::from_private_key(self)
// }

// PY-NOTE: #[pyo3()] can only be used in block that has #[pymethods] applied directly. applying via #[cfg_attr()] does not work (PyO3 limitation).
#[pyo3(name = "to_public_key")]
pub fn to_public_key_py(&self) -> PyResult<PublicKey> {
Expand All @@ -140,6 +134,12 @@ impl PrivateKey {
let address = Address::new(network.try_into()?, AddressVersion::PubKeyECDSA, &payload);
Ok(address)
}

// TODO
// #[wasm_bindgen(js_name = toKeypair)]
// pub fn to_keypair(&self) -> Result<Keypair, JsError> {
// Keypair::from_private_key(self)
// }
}

impl TryCastFromJs for PrivateKey {
Expand Down

0 comments on commit c9217ed

Please sign in to comment.