Skip to content

Commit

Permalink
Expose kaspa-wallet-keys structs to Python (kaspanet#84)
Browse files Browse the repository at this point in the history
* Expose Keypair struct to Python

* PublicKeyGenerator

* XPub and XPrv

* DerivationPath

* python method names

* PrivateKey to_hex method impl

* Address Python method restructure

* PY-NOTE comment clean up

* addresses example usage from Python

* pyi file

* clippy failures due to 1.80
  • Loading branch information
smartgoo committed Sep 17, 2024
1 parent 9baa7c0 commit 8ae7131
Show file tree
Hide file tree
Showing 19 changed files with 649 additions and 95 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions consensus/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ repository.workspace = true
devnet-prealloc = []
wasm32-sdk = []
default = []
py-sdk = ["pyo3"]

[dependencies]
async-trait.workspace = true
Expand All @@ -31,6 +32,7 @@ kaspa-merkle.workspace = true
kaspa-muhash.workspace = true
kaspa-txscript-errors.workspace = true
kaspa-utils.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 consensus/core/src/network.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use borsh::{BorshDeserialize, BorshSerialize};
use kaspa_addresses::Prefix;
#[cfg(feature = "py-sdk")]
use pyo3::{exceptions::PyException, PyErr};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::{Debug, Display, Formatter};
use std::ops::Deref;
Expand Down Expand Up @@ -99,6 +101,13 @@ impl FromStr for NetworkType {
}
}

#[cfg(feature = "py-sdk")]
impl From<NetworkTypeError> for PyErr {
fn from(value: NetworkTypeError) -> PyErr {
PyException::new_err(value.to_string())
}
}

impl Display for NetworkType {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Expand Down
44 changes: 32 additions & 12 deletions crypto/addresses/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,38 +237,28 @@ impl Address {
pub fn validate(address: &str) -> bool {
Self::try_from(address).is_ok()
}
}

// PY-NOTE: fns exposed to both WASM and Python
#[cfg_attr(feature = "py-sdk", pymethods)]
#[wasm_bindgen]
impl Address {
// PY-NOTE: want to use `#[pyo3(name = "to_string")]` for this fn, but cannot use #[pyo3()] in block where pymethods is applied via cfg_attr
/// Convert an address to a string.
#[wasm_bindgen(js_name = toString)]
pub fn address_to_string(&self) -> String {
self.into()
}

// PY-NOTE: want to use `#[pyo3(name = "version")]` for this fn, but cannot use #[pyo3()] in block where pymethods is applied via cfg_attr
#[wasm_bindgen(getter, js_name = "version")]
pub fn version_to_string(&self) -> String {
self.version.to_string()
}

// PY-NOTE: want to use `#[pyo3(name = "prefix")]` for this fn, but cannot use #[pyo3()] in block where pymethods is applied via cfg_attr
#[wasm_bindgen(getter, js_name = "prefix")]
pub fn prefix_to_string(&self) -> String {
self.prefix.to_string()
}

// PY-NOTE: want to use `#[pyo3(name = "set_prefix")]` for this fn, but cannot use #[pyo3()] in block where pymethods is applied via cfg_attr
#[wasm_bindgen(setter, js_name = "setPrefix")]
pub fn set_prefix_from_str(&mut self, prefix: &str) {
self.prefix = Prefix::try_from(prefix).unwrap_or_else(|err| panic!("Address::prefix() - invalid prefix `{prefix}`: {err}"));
}

// PY-NOTE: want to use `#[pyo3(name = "payload")]` for this fn, but cannot use #[pyo3()] in block where pymethods is applied via cfg_attr
#[wasm_bindgen(getter, js_name = "payload")]
pub fn payload_to_string(&self) -> String {
self.encode_payload()
Expand All @@ -285,18 +275,48 @@ impl Address {
#[cfg(feature = "py-sdk")]
#[pymethods]
impl Address {
// PY-NOTE: #[new] can only be used in block that has #[pymethods] applied directly. applying via #[cfg_attr()] does not work (PyO3 limitation).
#[new]
pub fn constructor_py(address: &str) -> Address {
address.try_into().unwrap_or_else(|err| panic!("Address::constructor() - address error `{}`: {err}", address))
}

// PY-NOTE: #[pyo3()] and #[staticmethod] can only be used in block that has #[pymethods] applied directly. applying via #[cfg_attr()] does not work (PyO3 limitation).
#[pyo3(name = "validate")]
#[staticmethod]
pub fn validate_py(address: &str) -> bool {
Self::try_from(address).is_ok()
}

#[pyo3(name = "to_string")]
pub fn address_to_string_py(&self) -> String {
self.into()
}

#[pyo3(name = "version")]
pub fn version_to_string_py(&self) -> String {
self.version.to_string()
}

#[pyo3(name = "prefix")]
pub fn prefix_to_string_py(&self) -> String {
self.prefix.to_string()
}

#[pyo3(name = "set_prefix")]
pub fn set_prefix_from_str_py(&mut self, prefix: &str) {
self.prefix = Prefix::try_from(prefix).unwrap_or_else(|err| panic!("Address::prefix() - invalid prefix `{prefix}`: {err}"));
}

#[pyo3(name = "payload")]
pub fn payload_to_string_py(&self) -> String {
self.encode_payload()
}

#[pyo3(name = "short")]
pub fn short_py(&self, n: usize) -> String {
let payload = self.encode_payload();
let n = std::cmp::min(n, payload.len() / 4);
format!("{}:{}....{}", self.prefix, &payload[0..n], &payload[payload.len() - n..])
}
}

impl Display for Address {
Expand Down
68 changes: 60 additions & 8 deletions python/examples/addresses.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,64 @@
from kaspa import PrivateKey
from kaspa import (
PublicKey,
PublicKeyGenerator,
PrivateKey,
Keypair,
# create_address
)

def demo_generate_address_from_public_key_hex_string():
# Compressed public key "02dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659"
public_key = PublicKey("02dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659")
print("\nGiven compressed public key: 02dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659")
print(public_key.to_string())
print(public_key.to_address("mainnet").to_string())

# x-only public key: "dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659"
x_only_public_key = PublicKey("dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659")
print("\nGiven x-only public key: dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659")
print(x_only_public_key.to_string())
print(x_only_public_key.to_address("mainnet").to_string())

# EDR public key
full_der_public_key = PublicKey("0421eb0c4270128b16c93c5f0dac48d56051a6237dae997b58912695052818e348b0a895cbd0c93a11ee7afac745929d96a4642a71831f54a7377893af71a2e2ae")
print("\nGiven x-only public key: 0421eb0c4270128b16c93c5f0dac48d56051a6237dae997b58912695052818e348b0a895cbd0c93a11ee7afac745929d96a4642a71831f54a7377893af71a2e2ae")
print(full_der_public_key.to_string())
print(full_der_public_key.to_address("mainnet").to_string())

def demo_generate_address_from_private_key_hex_string():
private_key = PrivateKey("b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef")
print("\nGiven private key b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef")
print(private_key.to_keypair().to_address("kaspa").to_string())

def demo_generate_random():
keypair = Keypair.random()
print("\nRandom Generation")
print(keypair.private_key())
print(keypair.public_key())
print(keypair.to_address("kaspa").to_string())

if __name__ == "__main__":
private_key = PrivateKey(
'b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef')
print(f'Private Key: {private_key.to_hex()}')
demo_generate_address_from_public_key_hex_string()
demo_generate_address_from_private_key_hex_string()
demo_generate_random()

# HD Wallet style pub key gen
xpub = PublicKeyGenerator.from_master_xprv(
"kprv5y2qurMHCsXYrNfU3GCihuwG3vMqFji7PZXajMEqyBkNh9UZUJgoHYBLTKu1eM4MvUtomcXPQ3Sw9HZ5ebbM4byoUciHo1zrPJBQfqpLorQ",
False,
0
)
print(xpub.to_string())

# Generates the first 10 Receive Public Keys and their addresses
compressed_public_keys = xpub.receive_pubkeys(0, 10)
print("\nreceive address compressed_public_keys")
for key in compressed_public_keys:
print(key.to_string(), key.to_address("mainnet").to_string())

public_key = private_key.to_public_key()
print(f'Public Key: {public_key.to_string_impl()}')
# Generates the first 10 Change Public Keys and their addresses
compressed_public_keys = xpub.change_pubkeys(0, 10)
print("\nchange address compressed_public_keys")
for key in compressed_public_keys:
print(key.to_string(), key.to_address("mainnet").to_string())

address = public_key.to_address('mainnet')
print(f'Address: {address.address_to_string()}')
Loading

0 comments on commit 8ae7131

Please sign in to comment.