From 990472d799b6dd2cf3b8abbfe9dbe5fe7c05768c Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sun, 17 Nov 2024 12:29:44 -0500 Subject: [PATCH] Python Cleanup (#125) * PaymentDestination::Change in Generator * py-note cleanup * comment clean up * example cleanup * cleanup * lint --- consensus/client/src/utxo.rs | 7 --- crypto/addresses/src/lib.rs | 2 - .../txscript/src/bindings/python/builder.rs | 4 +- python/LICENSE | 15 +++++ python/README.md | 57 ++++++++++++------- python/build-dev | 20 ++++--- python/build-release | 29 ++++++++++ python/examples/transactions/generator.py | 2 +- python/examples/transactions/krc20_deploy.py | 18 +++--- python/kaspa.toml | 30 ---------- python/pyproject.toml | 12 +++- rpc/core/src/bindings/python/messages.rs | 2 - rpc/wrpc/bindings/python/src/client.rs | 16 ++---- .../bindings/python/tx/generator/generator.rs | 18 +++--- wallet/core/src/bindings/python/tx/utils.rs | 12 ++-- wallet/keys/src/derivation_path.rs | 2 - wallet/keys/src/privatekey.rs | 2 - wallet/keys/src/pubkeygen.rs | 2 - wallet/keys/src/publickey.rs | 4 -- 19 files changed, 135 insertions(+), 119 deletions(-) create mode 100644 python/LICENSE create mode 100755 python/build-release delete mode 100644 python/kaspa.toml diff --git a/consensus/client/src/utxo.rs b/consensus/client/src/utxo.rs index da56b9603..13a5f8e78 100644 --- a/consensus/client/src/utxo.rs +++ b/consensus/client/src/utxo.rs @@ -218,9 +218,6 @@ impl UtxoEntryReference { #[cfg(feature = "py-sdk")] #[pymethods] impl UtxoEntryReference { - // PY-TODO - // fn py_to_string - #[getter] #[pyo3(name = "entry")] pub fn entry_py(&self) -> UtxoEntry { @@ -426,10 +423,6 @@ impl UtxoEntries { #[cfg(feature = "py-sdk")] #[pymethods] impl UtxoEntries { - // PY-TODO - // #[new] - // pub fn py_ctor() - #[getter] #[pyo3(name = "items")] pub fn get_items_as_py_list(&self) -> Vec { diff --git a/crypto/addresses/src/lib.rs b/crypto/addresses/src/lib.rs index 1c05951d5..990d71ca5 100644 --- a/crypto/addresses/src/lib.rs +++ b/crypto/addresses/src/lib.rs @@ -225,7 +225,6 @@ impl Address { } } -// PY-NOTE: WASM specific fn implementations #[wasm_bindgen] impl Address { #[wasm_bindgen(constructor)] @@ -271,7 +270,6 @@ impl Address { } } -// PY-NOTE: Python specific fn implementations #[cfg(feature = "py-sdk")] #[pymethods] impl Address { diff --git a/crypto/txscript/src/bindings/python/builder.rs b/crypto/txscript/src/bindings/python/builder.rs index a612fa783..95f9d4eed 100644 --- a/crypto/txscript/src/bindings/python/builder.rs +++ b/crypto/txscript/src/bindings/python/builder.rs @@ -119,11 +119,9 @@ impl ScriptBuilder { Ok(generated_script.to_hex().into()) } - - // pub fn hex_view() } -// PY-TODO change to PyOpcode struct and handle similar to PyBinary +// PY-TODO change to PyOpcode struct and handle similar to PyBinary? fn extract_ops(input: &Bound) -> PyResult> { if let Ok(opcode) = extract_op(&input) { // Single u8 or Opcodes variant diff --git a/python/LICENSE b/python/LICENSE new file mode 100644 index 000000000..b66757abc --- /dev/null +++ b/python/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2022-2024 Kaspa developers + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/python/README.md b/python/README.md index 4b165f2c7..63c5b01d6 100644 --- a/python/README.md +++ b/python/README.md @@ -1,21 +1,38 @@ -# 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.9 or higher (`python --version`) is installed. -2. Clone Python SDK source: `git clone -b python https://github.com/aspectron/rusty-kaspa.git` -3. `cd rusty-kaspa` -4. `cd python` (Python SDK crate) -5. Create Python virtual environment with `python -m venv env` or your preferred env tool. -6. Activate Python virtual environment: -- Unix-like systems: `source env/bin/activate` -- Windows: `env/scripts/activate.bat` -5. Install Maturin build tool: `pip install maturin` -6. Build Python package with Maturin: -- Build & install in current active virtual env: `maturin develop --release --features py-sdk` -- Build source and built (wheel) distributions: `maturin build --release --strip --sdist --features py-sdk`. The resulting wheel (.whl file) location will be printed `Built wheel for CPython 3.x to `. The `.whl` file can be copied to another location or machine and installed there with `pip install <.whl filepath>` - -# Usage from Python +# Kaspa Python SDK +Rusty-Kaspa Python SDK exposes select Rusty-Kaspa source for use in Python applications, allowing Python developers to interact with the Kaspa BlockDAG. + +This package is built from Rusty-Kaspa's Rust source code using [PyO3](https://pyo3.rs/v0.20.0/) and [Maturin](https://www.maturin.rs) to build bindings for Python. + +> [!IMPORTANT] +> Kaspa Python SDK is currently in Beta (maybe even Alpha in some regards) status. Please use accordingly. + +## Features +A goal of this package is to mirror Kaspa's WASM SDK as closely as possible. From both a feature coverage and usage perspective. + +The following main feature categories are currently exposed for use from Python: +- wRPC Client +- Transaction generation +- Key management + +This package does not yet fully mirror WASM SDK, gaps mostly exist around wallet functionality. Future work will bring this as close as possible. The ability to read Rusty-Kaspa's RocksDB database from Python is in progress. + +## Installing from Source +This package can currently be installed from source. + +### Instructions +1. To build the Python SDK from source, you need to have the Rust environment installed. To do that, follow instructions in the [Installation section of Rusty Kaspa README](https://github.com/kaspanet/rusty-kaspa?tab=readme-ov-file#installation). +2. `cd rusty-kaspa/python` to enter Python SDK crate +3. Run `./build-release` script to build source and built (wheel) dists. +4. The resulting wheel (`.whl`) file location will be printed: `Built wheel for CPython 3.x to `. The `.whl` file can be copied to another location or machine and installed there with `pip install <.whl filepath>` + +### `maturin develop` vs. `maturin build` +For full details, please see `build-release` script, `build-dev` script, and [Maturin](https://www.maturin.rs) documentation. + +Build & install in current active virtual env: `maturin develop --release --features py-sdk` + +Build source and built (wheel) distributions: `maturin build --release --strip --sdist --features py-sdk`. + +## Usage from Python The Python SDK module name is `kaspa`. The following example shows how to connect an RPC client to Kaspa's PNN (Public Node Network). @@ -34,7 +51,7 @@ if __name__ == "__main__": More detailed examples can be found in `./examples`. -# Project Layout +## SDK Project Layout The Python package `kaspa` is built from the `kaspa-python` crate, which is located at `./python`. -As such, the `kaspa` function in `./python/src/lib.rs` is a good starting point. This function uses PyO3 to add functionality to the package. +As such, the Rust `kaspa` function in `./python/src/lib.rs` is a good starting point. This function uses PyO3 to add functionality to the package. diff --git a/python/build-dev b/python/build-dev index 4be5fdf62..42612490f 100755 --- a/python/build-dev +++ b/python/build-dev @@ -1,23 +1,29 @@ #!/bin/bash +set -e + VENV_DIR="env" if [ ! -d "$VENV_DIR" ]; then - echo "Creating virtual environment in $VENV_DIR..." + echo "Creating virtual environment in '$VENV_DIR'" python3 -m venv $VENV_DIR +else + echo "Virtual environment already exists, using '$VENV_DIR'" fi -echo "Activating virtual environment..." +echo "Activating virtual environment '$VENV_DIR'" source $VENV_DIR/bin/activate if ! command -v maturin &> /dev/null; then - echo "Maturin not found, installing..." + echo "Maturin not found in '$VENV_DIR', installing Maturin" pip install maturin else - echo "Maturin is already installed." + echo "Maturin is already installed in '$VENV_DIR'" + maturin --version fi -echo "Running 'maturin develop --features py-sdk'..." -maturin develop --release --features py-sdk +BUILD_CMD="maturin develop --target-dir ./target --features py-sdk" +echo "Building with command '$BUILD_CMD'" +$BUILD_CMD -echo "Script execution completed." +echo "Build complete." diff --git a/python/build-release b/python/build-release new file mode 100755 index 000000000..480adcabb --- /dev/null +++ b/python/build-release @@ -0,0 +1,29 @@ +#!/bin/bash + +set -e + +VENV_DIR="env" + +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment in '$VENV_DIR'" + python3 -m venv $VENV_DIR +else + echo "Virtual environment already exists, using '$VENV_DIR'" +fi + +echo "Activating virtual environment '$VENV_DIR'" +source $VENV_DIR/bin/activate + +if ! command -v maturin &> /dev/null; then + echo "Maturin not found in '$VENV_DIR', installing Maturin" + pip install maturin +else + echo "Maturin is already installed in '$VENV_DIR'" + maturin --version +fi + +BUILD_CMD="maturin build --release --strip --sdist --target-dir target --out target/wheels --features py-sdk" +echo "Building with command '$BUILD_CMD'" +$BUILD_CMD + +echo "Build complete." diff --git a/python/examples/transactions/generator.py b/python/examples/transactions/generator.py index 0e6741364..2ec877e88 100644 --- a/python/examples/transactions/generator.py +++ b/python/examples/transactions/generator.py @@ -18,7 +18,7 @@ async def main(): entries = await client.get_utxos_by_addresses({"addresses": [source_address]}) entries = entries["entries"] - + entries = sorted(entries, key=lambda x: x['utxoEntry']['amount'], reverse=True) total = sum(item['utxoEntry']['amount'] for item in entries) diff --git a/python/examples/transactions/krc20_deploy.py b/python/examples/transactions/krc20_deploy.py index 920473153..16fde5766 100644 --- a/python/examples/transactions/krc20_deploy.py +++ b/python/examples/transactions/krc20_deploy.py @@ -33,15 +33,15 @@ async def main(): 'lim': '1000', } - script = ScriptBuilder() - script.add_data(public_key.to_x_only_public_key().to_string()) - script.add_op(Opcodes.OpCheckSig) - script.add_op(Opcodes.OpFalse) - script.add_op(Opcodes.OpIf) - script.add_data(b'kasplex') - script.add_i64(0) - script.add_data(json.dumps(data, separators=(',', ':')).encode('utf-8')) - script.add_op(Opcodes.OpEndIf) + script = ScriptBuilder()\ + .add_data(public_key.to_x_only_public_key().to_string())\ + .add_op(Opcodes.OpCheckSig)\ + .add_op(Opcodes.OpFalse)\ + .add_op(Opcodes.OpIf)\ + .add_data(b'kasplex')\ + .add_i64(0)\ + .add_data(json.dumps(data, separators=(',', ':')).encode('utf-8'))\ + .add_op(Opcodes.OpEndIf) print(f'Script: {script.to_string()}') p2sh_address = address_from_script_public_key(script.create_pay_to_script_hash_script(), 'testnet') diff --git a/python/kaspa.toml b/python/kaspa.toml deleted file mode 100644 index b41e388b6..000000000 --- a/python/kaspa.toml +++ /dev/null @@ -1,30 +0,0 @@ -[build-system] -requires = ["maturin>=1.0,<2.0"] -build-backend = "maturin" - -[project] -name = "kaspa" -description = "Kaspa Python Bindings" -version = "0.1.0" -requires-python = ">=3.8" -license = "ISC" -classifiers = [ - "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 = "kaspa" -description = "Kaspa Python Bindings" - -[tool.maturin] -name = "kaspa" \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml index 1c48184a7..288179e6e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,19 +4,27 @@ build-backend = "maturin" [project] name = "kaspa" -version = "0.0.1" +version = "0.1.0" description = "Kaspa Python SDK" +requires-python = ">=3.8" +readme = "README.md" license = "ISC" classifiers = [ + "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Rust", ] dependencies = [] +[project.urls] +"Source" = "https://github.com/kaspanet/rusty-kaspa/tree/master/python" + [package.metadata.maturin] name = "kaspa" description = "Kaspa Python SDK" [tool.maturin] name = "kaspa" -features = ["pyo3/extension-module"] \ No newline at end of file +bindings = "pyo3" +features = ["pyo3/extension-module"] +strip = true \ No newline at end of file diff --git a/rpc/core/src/bindings/python/messages.rs b/rpc/core/src/bindings/python/messages.rs index feb2d176f..b1522c0c6 100644 --- a/rpc/core/src/bindings/python/messages.rs +++ b/rpc/core/src/bindings/python/messages.rs @@ -245,7 +245,6 @@ try_from_args! ( dict : SubmitTransactionRequest, { verbose_data: None, }; - // PY-TODO transaction.into() Ok(SubmitTransactionRequest { transaction: rpc_transaction, allow_orphan }) }); @@ -254,7 +253,6 @@ try_from_args! ( dict : SubmitTransactionReplacementRequest, { .ok_or_else(|| PyException::new_err("Key `transactions` not present"))? .extract()?; - // PY-TODO transaction.into() Ok(SubmitTransactionReplacementRequest { transaction: transaction.into() }) }); diff --git a/rpc/wrpc/bindings/python/src/client.rs b/rpc/wrpc/bindings/python/src/client.rs index b11b4c557..b905305d4 100644 --- a/rpc/wrpc/bindings/python/src/client.rs +++ b/rpc/wrpc/bindings/python/src/client.rs @@ -230,8 +230,6 @@ impl RpcClient { timeout_duration: Option, retry_interval: Option, ) -> PyResult> { - // TODO expose args to Python similar to WASM wRPC Client IConnectOptions? - let block_async_connect = block_async_connect.unwrap_or(true); let strategy = match strategy { Some(strategy) => ConnectStrategy::from_str(&strategy).map_err(|err| PyException::new_err(format!("{}", err)))?, @@ -335,7 +333,9 @@ impl RpcClient { Ok(()) } - // fn clear_event_listener() PY-TODO + // fn clear_event_listener PY-TODO + // fn default_port PY-TODO + // fn parse_url PY-TODO fn remove_all_event_listeners(&self) -> PyResult<()> { *self.inner.callbacks.lock().unwrap() = Default::default(); @@ -480,12 +480,6 @@ impl RpcClient { } } -#[pymethods] -impl RpcClient { - // PY-TODO default_port - // PY-TODO parse_url -} - #[pymethods] impl RpcClient { fn subscribe_utxos_changed(&self, py: Python, addresses: Vec
) -> PyResult> { @@ -538,8 +532,8 @@ impl RpcClient { } build_wrpc_python_subscriptions!([ - // UtxosChanged - added above due to parameter `addresses: Vec
`` - // VirtualChainChanged - added above due to paramter `include_accepted_transaction_ids: bool` + // UtxosChanged - defined above due to parameter `addresses: Vec
`` + // VirtualChainChanged - defined above due to paramter `include_accepted_transaction_ids: bool` BlockAdded, FinalityConflict, FinalityConflictResolved, diff --git a/wallet/core/src/bindings/python/tx/generator/generator.rs b/wallet/core/src/bindings/python/tx/generator/generator.rs index b6b64bc92..6115e675a 100644 --- a/wallet/core/src/bindings/python/tx/generator/generator.rs +++ b/wallet/core/src/bindings/python/tx/generator/generator.rs @@ -63,12 +63,12 @@ pub struct Generator { #[pymethods] impl Generator { #[new] - #[pyo3(signature = (network_id, entries, outputs, change_address, payload=None, priority_fee=None, priority_entries=None, sig_op_count=None, minimum_signatures=None))] + #[pyo3(signature = (network_id, entries, change_address, outputs=None, payload=None, priority_fee=None, priority_entries=None, sig_op_count=None, minimum_signatures=None))] pub fn ctor( network_id: &str, entries: PyUtxoEntries, - outputs: PyOutputs, change_address: Address, + outputs: Option, payload: Option, priority_fee: Option, priority_entries: Option, @@ -76,7 +76,7 @@ impl Generator { minimum_signatures: Option, ) -> PyResult { let settings = GeneratorSettings::new( - outputs.outputs, + outputs, change_address, priority_fee, entries.entries, @@ -177,7 +177,7 @@ struct GeneratorSettings { impl GeneratorSettings { pub fn new( - outputs: Vec, + outputs: Option, change_address: Address, priority_fee: Option, entries: Vec, @@ -189,17 +189,17 @@ impl GeneratorSettings { ) -> GeneratorSettings { let network_id = NetworkId::from_str(network_id).unwrap(); - // PY-TODO - // let final_transaction_destination: PaymentDestination = - // if outputs.is_empty() { PaymentDestination::Change } else { PaymentOutputs::try_from(outputs).unwrap().into() }; - let final_transaction_destination: PaymentDestination = PaymentOutputs { outputs }.into(); + let final_transaction_destination = match outputs { + Some(py_outputs) => PaymentOutputs { outputs: py_outputs.outputs }.into(), + None => PaymentDestination::Change, + }; let final_priority_fee = match priority_fee { Some(fee) => fee.try_into().unwrap(), None => Fees::None, }; - // PY-TODO support GeneratorSource::UtxoContext and clean up below + // PY-TODO support GeneratorSource::UtxoContext when available let generator_source = GeneratorSource::UtxoEntries(entries.iter().map(|entry| UtxoEntryReference::try_from(entry.clone()).unwrap()).collect()); diff --git a/wallet/core/src/bindings/python/tx/utils.rs b/wallet/core/src/bindings/python/tx/utils.rs index 88930145e..a384e9859 100644 --- a/wallet/core/src/bindings/python/tx/utils.rs +++ b/wallet/core/src/bindings/python/tx/utils.rs @@ -43,13 +43,13 @@ pub fn create_transaction_py( #[pyfunction] #[pyo3(name = "create_transactions")] -#[pyo3(signature = (network_id, entries, outputs, change_address, payload=None, priority_fee=None, priority_entries=None, sig_op_count=None, minimum_signatures=None))] +#[pyo3(signature = (network_id, entries, change_address, outputs=None, payload=None, priority_fee=None, priority_entries=None, sig_op_count=None, minimum_signatures=None))] pub fn create_transactions_py<'a>( py: Python<'a>, network_id: &str, entries: PyUtxoEntries, - outputs: PyOutputs, change_address: Address, + outputs: Option, payload: Option, priority_fee: Option, priority_entries: Option, @@ -59,8 +59,8 @@ pub fn create_transactions_py<'a>( let generator = Generator::ctor( network_id, entries, - outputs, change_address, + outputs, payload.map(Into::into), priority_fee, priority_entries, @@ -79,12 +79,12 @@ pub fn create_transactions_py<'a>( #[pyfunction] #[pyo3(name = "estimate_transactions")] -#[pyo3(signature = (network_id, entries, outputs, change_address, payload=None, priority_fee=None, priority_entries=None, sig_op_count=None, minimum_signatures=None))] +#[pyo3(signature = (network_id, entries, change_address, outputs=None, payload=None, priority_fee=None, priority_entries=None, sig_op_count=None, minimum_signatures=None))] pub fn estimate_transactions_py<'a>( network_id: &str, entries: PyUtxoEntries, - outputs: PyOutputs, change_address: Address, + outputs: Option, payload: Option, priority_fee: Option, priority_entries: Option, @@ -94,8 +94,8 @@ pub fn estimate_transactions_py<'a>( let generator = Generator::ctor( network_id, entries, - outputs, change_address, + outputs, payload.map(Into::into), priority_fee, priority_entries, diff --git a/wallet/keys/src/derivation_path.rs b/wallet/keys/src/derivation_path.rs index 6a0dd8785..9e0bf5088 100644 --- a/wallet/keys/src/derivation_path.rs +++ b/wallet/keys/src/derivation_path.rs @@ -10,7 +10,6 @@ pub struct DerivationPath { inner: kaspa_bip32::DerivationPath, } -// PY-NOTE: methods exposed to only WASM #[wasm_bindgen] impl DerivationPath { #[wasm_bindgen(constructor)] @@ -51,7 +50,6 @@ impl DerivationPath { } } -// PY-NOTE: methods exposed to only Python #[cfg(feature = "py-sdk")] #[pymethods] impl DerivationPath { diff --git a/wallet/keys/src/privatekey.rs b/wallet/keys/src/privatekey.rs index 4fd0afc5e..27731d97a 100644 --- a/wallet/keys/src/privatekey.rs +++ b/wallet/keys/src/privatekey.rs @@ -48,7 +48,6 @@ impl PrivateKey { } } -// PY-NOTE: WASM specific fn implementations #[wasm_bindgen] impl PrivateKey { /// Returns the [`PrivateKey`] key encoded as a hex string. @@ -93,7 +92,6 @@ impl PrivateKey { } } -// PY-NOTE: Python specific fn implementations #[cfg(feature = "py-sdk")] #[pymethods] impl PrivateKey { diff --git a/wallet/keys/src/pubkeygen.rs b/wallet/keys/src/pubkeygen.rs index 2c820f7bd..bcfb79e0e 100644 --- a/wallet/keys/src/pubkeygen.rs +++ b/wallet/keys/src/pubkeygen.rs @@ -20,7 +20,6 @@ pub struct PublicKeyGenerator { hd_wallet: WalletDerivationManager, } -// PY-NOTE: WASM specific method impls #[wasm_bindgen] impl PublicKeyGenerator { #[wasm_bindgen(js_name=fromXPub)] @@ -221,7 +220,6 @@ impl PublicKeyGenerator { } } -// PY-NOTE: Python specific fn implementations #[cfg(feature = "py-sdk")] #[pymethods] impl PublicKeyGenerator { diff --git a/wallet/keys/src/publickey.rs b/wallet/keys/src/publickey.rs index 595d69b9e..b47296ad6 100644 --- a/wallet/keys/src/publickey.rs +++ b/wallet/keys/src/publickey.rs @@ -36,7 +36,6 @@ pub struct PublicKey { pub public_key: Option, } -// PY-NOTE: WASM specific fn implementations #[wasm_bindgen(js_class = PublicKey)] impl PublicKey { /// Create a new [`PublicKey`] from a hex-encoded string. @@ -86,7 +85,6 @@ impl PublicKey { } } -// PY-NOTE: Python specific fn implementations #[cfg(feature = "py-sdk")] #[pymethods] impl PublicKey { @@ -253,7 +251,6 @@ impl XOnlyPublicKey { } } -// PY-NOTE: WASM specific fn implementations #[wasm_bindgen] impl XOnlyPublicKey { #[wasm_bindgen(constructor)] @@ -292,7 +289,6 @@ impl XOnlyPublicKey { } } -// PY-NOTE: Python specific fn implementations #[cfg(feature = "py-sdk")] #[pymethods] impl XOnlyPublicKey {