From ac4ed022bb0afdcd4068881e3bbe8bd9fd3b3f25 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Wed, 29 Apr 2026 16:58:53 -0400 Subject: [PATCH 1/7] docs learn section wip --- docs/CHANGELOG.md | 3 + docs/getting-started/examples.md | 10 +- docs/getting-started/security.md | 49 +++ docs/guides/addresses.md | 167 --------- docs/guides/custom-derivation.md | 60 ++++ docs/guides/key-derivation.md | 258 -------------- docs/guides/message-signing.md | 207 ++++-------- docs/guides/mnemonics.md | 143 +++----- docs/guides/multisig.md | 99 ++++++ docs/guides/rpc-client.md | 414 ----------------------- docs/guides/transactions.md | 361 -------------------- docs/guides/wallet-recovery.md | 96 ++++++ docs/index.md | 70 ++-- docs/learn/addresses.md | 144 ++++++++ docs/learn/concepts.md | 114 +++++++ docs/learn/index.md | 23 ++ docs/learn/networks.md | 71 ++++ docs/learn/rpc/calls.md | 135 ++++++++ docs/learn/rpc/connecting.md | 91 +++++ docs/learn/rpc/index.md | 43 +++ docs/learn/rpc/resolver.md | 53 +++ docs/learn/rpc/subscriptions.md | 112 ++++++ docs/learn/transactions/index.md | 122 +++++++ docs/learn/transactions/inputs.md | 105 ++++++ docs/learn/transactions/mass-and-fees.md | 130 +++++++ docs/learn/transactions/metadata.md | 104 ++++++ docs/learn/transactions/outputs.md | 121 +++++++ docs/learn/transactions/serialization.md | 64 ++++ docs/learn/transactions/signing.md | 128 +++++++ docs/learn/transactions/submission.md | 98 ++++++ docs/learn/wallet-sdk/derivation.md | 159 +++++++++ docs/learn/wallet-sdk/index.md | 36 ++ docs/learn/wallet-sdk/key-management.md | 117 +++++++ docs/learn/wallet-sdk/tx-generator.md | 185 ++++++++++ docs/learn/wallet-sdk/utxo-context.md | 86 +++++ docs/learn/wallet-sdk/utxo-processor.md | 96 ++++++ docs/learn/wallet/accounts.md | 120 +++++++ docs/learn/wallet/addresses.md | 70 ++++ docs/learn/wallet/architecture.md | 77 +++++ docs/learn/wallet/index.md | 73 ++++ docs/learn/wallet/initialize.md | 62 ++++ docs/learn/wallet/keypair.md | 74 ++++ docs/learn/wallet/lifecycle.md | 64 ++++ docs/learn/wallet/open.md | 136 ++++++++ docs/learn/wallet/private-keys.md | 86 +++++ docs/learn/wallet/send-transaction.md | 128 +++++++ docs/learn/wallet/start.md | 107 ++++++ docs/learn/wallet/sweep.md | 74 ++++ docs/learn/wallet/transaction-history.md | 122 +++++++ 49 files changed, 3977 insertions(+), 1490 deletions(-) create mode 100644 docs/getting-started/security.md delete mode 100644 docs/guides/addresses.md create mode 100644 docs/guides/custom-derivation.md delete mode 100644 docs/guides/key-derivation.md create mode 100644 docs/guides/multisig.md delete mode 100644 docs/guides/rpc-client.md delete mode 100644 docs/guides/transactions.md create mode 100644 docs/guides/wallet-recovery.md create mode 100644 docs/learn/addresses.md create mode 100644 docs/learn/concepts.md create mode 100644 docs/learn/index.md create mode 100644 docs/learn/networks.md create mode 100644 docs/learn/rpc/calls.md create mode 100644 docs/learn/rpc/connecting.md create mode 100644 docs/learn/rpc/index.md create mode 100644 docs/learn/rpc/resolver.md create mode 100644 docs/learn/rpc/subscriptions.md create mode 100644 docs/learn/transactions/index.md create mode 100644 docs/learn/transactions/inputs.md create mode 100644 docs/learn/transactions/mass-and-fees.md create mode 100644 docs/learn/transactions/metadata.md create mode 100644 docs/learn/transactions/outputs.md create mode 100644 docs/learn/transactions/serialization.md create mode 100644 docs/learn/transactions/signing.md create mode 100644 docs/learn/transactions/submission.md create mode 100644 docs/learn/wallet-sdk/derivation.md create mode 100644 docs/learn/wallet-sdk/index.md create mode 100644 docs/learn/wallet-sdk/key-management.md create mode 100644 docs/learn/wallet-sdk/tx-generator.md create mode 100644 docs/learn/wallet-sdk/utxo-context.md create mode 100644 docs/learn/wallet-sdk/utxo-processor.md create mode 100644 docs/learn/wallet/accounts.md create mode 100644 docs/learn/wallet/addresses.md create mode 100644 docs/learn/wallet/architecture.md create mode 100644 docs/learn/wallet/index.md create mode 100644 docs/learn/wallet/initialize.md create mode 100644 docs/learn/wallet/keypair.md create mode 100644 docs/learn/wallet/lifecycle.md create mode 100644 docs/learn/wallet/open.md create mode 100644 docs/learn/wallet/private-keys.md create mode 100644 docs/learn/wallet/send-transaction.md create mode 100644 docs/learn/wallet/start.md create mode 100644 docs/learn/wallet/sweep.md create mode 100644 docs/learn/wallet/transaction-history.md diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b4cd4f58..037cf5b6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -20,6 +20,9 @@ - Wallet-specific exception classes populated into the `kaspa.exceptions` submodule, covering the rusty-kaspa wallet error variants (e.g. `WalletInsufficientFundsError`, `WalletAccountNotFoundError`, `WalletNotSyncedError`, etc.). - Examples under `examples/wallet/` demonstrating wallet usage - Pytest options `--network-id` and `--rpc-url` for targeting integration tests at a specific network / node. +- Documentation site reorganised around [Diataxis](https://diataxis.fr): a sequenced **Learn** section (RPC, Wallet, Wallet SDK, Networks, Addresses, Transactions, Kaspa Concepts) covers the SDK topic by topic, with a focused **Guides** cookbook for cross-cutting recipes (mnemonic restore, message signing, wallet recovery, custom derivation, multisig). +- `docs/getting-started/security.md`: a single canonical page covering secret-handling rules. Other pages link to it instead of duplicating the warning. +- Learn → Transactions split into a dedicated section: Overview, Inputs, Outputs, Mass & Fees, Signing, Submission, Metadata Fields, Serialization. Each page covers one component of a transaction with light Kaspa-protocol context, replacing the single `learn/transactions.md` page. ### Changed - `py_error_map!` macro extended to register wallet exception variants into the `kaspa.exceptions` submodule. diff --git a/docs/getting-started/examples.md b/docs/getting-started/examples.md index 983e3292..c7e4dc8d 100644 --- a/docs/getting-started/examples.md +++ b/docs/getting-started/examples.md @@ -2,12 +2,10 @@ This page contains a handful of brief examples showing core features of the Kaspa Python SDK. -!!! danger "Security Warning" - **Handle Private Keys Securely** - - **These examples do not use proper private key/mnemonic/seed handling.** This is omitted for brevity. - - Never store your private keys in plain text, or directly in source code. Store securely offline. Anyone with access to this phrase has full control over your funds. +!!! warning "Handle secrets carefully" + These snippets pass private-key material as inline strings for + readability. That is not how production code should handle secrets — + see [Security](security.md). ## Examples on Github diff --git a/docs/getting-started/security.md b/docs/getting-started/security.md new file mode 100644 index 00000000..7f2f8502 --- /dev/null +++ b/docs/getting-started/security.md @@ -0,0 +1,49 @@ +# Security + +Working with cryptocurrency means working with secret material. The same key +that lets you spend funds also lets anyone else who obtains it spend them. The +SDK doesn't try to hide this — it gives you direct access to mnemonics, seeds, +private keys, and wallet files. Treat them with care. + +## What counts as secret material + +| Type | Where it appears | Compromise consequence | +| --- | --- | --- | +| **Mnemonic phrase** | `Mnemonic.phrase`, the words in a `Mnemonic` instance | Full recovery of every wallet derived from it | +| **BIP-39 seed (64 bytes)** | `mnemonic.to_seed(...)`, the input to `XPrv(seed)` | Same as the mnemonic | +| **Extended private key (XPrv)** | `XPrv` instance, `xprv.xprv` string | Full control of every account derived under it | +| **Private key** | `PrivateKey`, `private_key.to_string()`, hex export | Full control of every UTXO that pays its address | +| **Wallet secret** | The password passed to `wallet_create`, `prv_key_data_create`, `accounts_send`, etc. | Decrypts the on-disk wallet file | +| **Wallet file** (`.kaspa/`) | The directory the managed `Wallet` writes to | Encrypted, but a weak password is not enough — the file holds every key | + +## Rules + +1. **Never commit secrets to git.** Add the wallet storage directory (`~/.kaspa` or whatever you configured) and any `*.json`, `*.kaspa`, `seed.txt`, `mnemonic.txt` artefacts to `.gitignore` *before* generating real keys. +2. **Never paste a real mnemonic into source code, an issue, a chat message, an LLM prompt, or a screenshot.** The examples throughout these docs use placeholder phrases for a reason. +3. **Don't print or log secret material in production.** The example snippets print mnemonics for clarity; strip those `print()` calls before shipping. +4. **Don't reuse mainnet keys for testing.** Generate a fresh testnet mnemonic and fund it from the testnet faucet. +5. **Use a wallet passphrase ("25th word") for high-value wallets.** A passphrase changes the seed; an attacker with the mnemonic alone gets nothing without it. +6. **Store backups offline.** Paper, hardware-encrypted USB, hardware wallet — not iCloud Notes. +7. **Generate keys on a machine you trust.** Building a release on a shared CI runner and deriving keys there is not the same as deriving them locally. + +## In these docs + +Code samples in **Learn** and **Guides** show how the SDK works — they pass +literal hex strings and short passwords inline so the snippet is readable. +**That is not how to handle real secrets.** When you adapt a snippet to +production, replace the inline strings with secrets sourced from your secret +manager, environment variables, hardware wallet, or interactive prompt. + +If a page documents an operation that touches secret material, it links here +instead of repeating this warning in full. + +## When something does leak + +If a mnemonic, seed, or private key leaves your control: + +1. Move every UTXO out of the affected wallet *immediately* — to a freshly + derived wallet from a *new* mnemonic, not the same one. +2. Stop using the leaked mnemonic. Don't try to "rotate the passphrase" or + "skip account 0" — derive a new wallet from new entropy. +3. Audit any service that accepted that wallet's signed messages or extended + public key. diff --git a/docs/guides/addresses.md b/docs/guides/addresses.md deleted file mode 100644 index 51bc7ac5..00000000 --- a/docs/guides/addresses.md +++ /dev/null @@ -1,167 +0,0 @@ -# Addresses - -## Overview - -Kaspa addresses are encoded representations of public keys or script hashes. They include: - -- A **network prefix** (`kaspa:`, `kaspatest:`, `kaspadev:`, `kaspasim:`) -- A **version** indicating the address type (PubKey, PubKeyECDSA, ScriptHash) -- An **encoded payload** derived from the public key or script - -## Creating Addresses - -### From a String - -```python -from kaspa import Address - -# Parse an address string -address = Address("kaspa:qz0s9f5p7d3e2c4x8n1b6m9k0j2h4g5f3d7a8s9w0e1r2t3y4u5i6o7p8") - -# Get address components -print(f"Prefix: {address.prefix}") -print(f"Version: {address.version}") -print(f"Full: {address.to_string()}") -``` - -### From a Private Key - -```python -from kaspa import PrivateKey, NetworkType - -# Create from private key hex -private_key = PrivateKey("your-private-key-hex") - -# Derive Schnorr address (default) -address = private_key.to_address(NetworkType.Mainnet) -print(f"Schnorr address: {address.to_string()}") - -# Derive ECDSA address -ecdsa_address = private_key.to_address_ecdsa(NetworkType.Mainnet) -print(f"ECDSA address: {ecdsa_address.to_string()}") -``` - -### From a Public Key - -```python -from kaspa import PublicKey, NetworkType - -# Create from public key hex -public_key = PublicKey("02a1b2c3d4e5f6...") - -# Derive address -address = public_key.to_address(NetworkType.Mainnet) -print(f"Address: {address.to_string()}") -``` - -## Validating Addresses - -```python -from kaspa import Address - -address_str = "kaspa:qz..." - -# Static validation (returns bool) -if Address.validate(address_str): - address = Address(address_str) - print(f"Valid address: {address.to_string()}") -else: - print("Invalid address!") -``` - -## Address Types - -Kaspa supports several address versions: - -| Version | Description | -|---------|-------------| -| PubKey | Schnorr signature | -| PubKeyECDSA | ECDSA signature | -| ScriptHash | Pay-to-Script-Hash | - -```python -from kaspa import Address - -address = Address("kaspa:qz...") - -# Check the version -version = address.version -print(f"Address version: {version}") -``` - -## Network Prefixes - -Addresses include a prefix indicating the network: - -| Prefix | Network | Use | -|--------|---------|-----| -| `kaspa:` | Mainnet | Production | -| `kaspatest:` | Testnet | Testing | -| `kaspadev:` | Devnet | Development | -| `kaspasim:` | Simnet | Simulation | - -### Changing the Prefix - -```python -from kaspa import Address - -# Create mainnet address -address = Address("kaspa:qz...") - -# Change to testnet -address.prefix = "kaspatest" -print(f"Testnet address: {address.to_string()}") -``` - -## Address from Script - -Create an address from a script public key: - -```python -from kaspa import ScriptPublicKey, address_from_script_public_key, NetworkType - -# Create script public key -script_pubkey = ScriptPublicKey(0, "20a1b2c3...") - -# Convert to address -address = address_from_script_public_key(script_pubkey, NetworkType.Mainnet) -print(f"Address: {address.to_string()}") -``` - -## Script from Address - -Get the script public key for an address: - -```python -from kaspa import Address, pay_to_address_script - -address = Address("kaspa:qz...") - -# Get the locking script -script_pubkey = pay_to_address_script(address) -print(f"Script: {script_pubkey.script}") -``` - -## Multi-Signature Addresses - -Create a multi-signature address: - -```python -from kaspa import create_multisig_address, PublicKey, NetworkType - -# Gather public keys from all participants -pubkeys = [ - PublicKey("02key1..."), - PublicKey("02key2..."), - PublicKey("02key3..."), -] - -# Create 2-of-3 multisig address -multisig_address = create_multisig_address( - minimum_signatures=2, - keys=pubkeys, - network_type=NetworkType.Mainnet -) - -print(f"Multisig address: {multisig_address.to_string()}") -``` diff --git a/docs/guides/custom-derivation.md b/docs/guides/custom-derivation.md new file mode 100644 index 00000000..0cd3fd64 --- /dev/null +++ b/docs/guides/custom-derivation.md @@ -0,0 +1,60 @@ +# Custom derivation paths + +The default +[`PrivateKeyGenerator`](../learn/wallet-sdk/derivation.md#privatekeygenerator) +walks the BIP-44 path `m/44'/111111'/'//`. +When you need something off-spec — a custom path for migrating from +another wallet, a one-off subkey, a non-BIP-44 layout — derive directly +from the `XPrv`. + +## Recipe + +```python +from kaspa import DerivationPath, Mnemonic, NetworkType, XPrv + +m = Mnemonic("<24 words>") +xprv = XPrv(m.to_seed()) + +# Walk an arbitrary path +custom = xprv.derive_path("m/9999'/7'/0/42") +print(custom.private_key.to_string()) +print(custom.private_key.to_address(NetworkType.Mainnet).to_string()) + +# Build paths programmatically +path = DerivationPath("m/44'/111111'/0'") +path.push(0) # → m/44'/111111'/0'/0 +path.push(0) # → m/44'/111111'/0'/0/0 +leaf = xprv.derive_path(path) + +# Step by step (no path string needed) +account_xprv = xprv.derive_child(0, hardened=True) +chain_xprv = account_xprv.derive_child(0) +addr_xprv = chain_xprv.derive_child(0) +``` + +## Notes + +- **Hardened path components** end in `'`. Pass `hardened=True` to + `derive_child` for the same effect. +- **Re-using `DerivationPath`.** It's mutable — `push`, `parent`, + `to_string`, `length`, `is_empty`. Convenient when walking a chain + in a loop. +- **The result of `derive_path` is itself an `XPrv`**, so you can keep + deriving below it. +- **Watch-only with a custom path.** Take `XPub` from any node in the + tree and derive *below it* with `XPub.derive_path(...)` (unhardened + components only — hardened derivation requires the private side). +- **Address encoding.** `derive_path` gives you a key, not an address. + Call `.to_address(NetworkType.X)` to encode for the right network. + +## When *not* to do this + +- For everyday HD wallets, use + [`PrivateKeyGenerator`](../learn/wallet-sdk/derivation.md#privatekeygenerator) + — it produces the addresses the rest of the ecosystem expects. +- For multisig, use the cosigner-aware path (`is_multisig=True`, + `cosigner_index=...`) — see + [Multi-signature transactions](multisig.md). +- A custom path means *you own the recovery story*. Document the path + alongside the mnemonic; "I derived from `m/9999'/7'/0/42`" is not + recoverable without that note. diff --git a/docs/guides/key-derivation.md b/docs/guides/key-derivation.md deleted file mode 100644 index d4269a56..00000000 --- a/docs/guides/key-derivation.md +++ /dev/null @@ -1,258 +0,0 @@ -# Key Derivation - -This guide covers hierarchical deterministic (HD) wallet key derivation in the Kaspa Python SDK. - -!!! danger "Security Warning" - **Handle Private Keys Securely** - - **These examples do not use proper private key/mnemonic/seed handling.** This is omitted for brevity. - - Never store your private keys in plain text, or directly in source code. Store securely offline. Anyone with access to this phrase has full control over your funds. - -## Derivation Path - -``` -m / purpose' / coin_type' / account' / change / address_index -``` - -See [Kaspa MDBook's page on derivation](https://kaspa-mdbook.aspectron.com/wallets/addresses.html) for more information. - -## Extended Keys - -### Extended Private Key (XPrv) - -```python -from kaspa import Mnemonic, XPrv - -# Generate from mnemonic -mnemonic = Mnemonic.random() -seed = mnemonic.to_seed() -xprv = XPrv(seed) - -# Access properties -print(f"XPrv: {xprv.xprv}") -print(f"Private key: {xprv.private_key}") -print(f"Depth: {xprv.depth}") -print(f"Chain code: {xprv.chain_code}") -``` - -### Extended Public Key (XPub) - -```python -from kaspa import XPrv, XPub - -# Derive XPub from XPrv -xprv = XPrv(seed) -xpub = xprv.to_xpub() - -# Access properties -print(f"XPub: {xpub.xpub}") -print(f"Depth: {xpub.depth}") -print(f"Chain code: {xpub.chain_code}") - -# Get public key -public_key = xpub.to_public_key() -``` - -## Manual Derivation - -### Deriving Child Keys - -```python -from kaspa import XPrv, DerivationPath - -xprv = XPrv(seed) - -# Derive by child number -child = xprv.derive_child(0) # Non-hardened -hardened_child = xprv.derive_child(0, hardened=True) - -# Derive by path string -account_key = xprv.derive_path("m/44'/111111'/0'") - -# Derive using DerivationPath object -path = DerivationPath("m/44'/111111'/0'/0/0") -derived = xprv.derive_path(path) -``` - -### Working with Derivation Paths - -```python -from kaspa import DerivationPath - -# Create a path -path = DerivationPath("m/44'/111111'/0'/0/0") - -# Check properties -print(f"Length: {path.length()}") -print(f"Is empty: {path.is_empty()}") -print(f"String: {path.to_string()}") - -# Get parent path -parent = path.parent() -print(f"Parent: {parent.to_string()}") - -# Extend path -path.push(1) # Add non-hardened child -path.push(0, hardened=True) # Add hardened child -``` - -## Key Generators - -### Private Key Generator - -```python -from kaspa import XPrv, PrivateKeyGenerator, NetworkType - -xprv = XPrv(seed) - -# Create generator for standard (non-multisig) wallet -key_gen = PrivateKeyGenerator( - xprv=xprv, - is_multisig=False, - account_index=0 -) - -# Generate receive addresses -for i in range(5): - private_key = key_gen.receive_key(i) - address = private_key.to_address(NetworkType.Mainnet) - print(f"Receive {i}: {address.to_string()}") - -# Generate change addresses -change_key = key_gen.change_key(0) -change_address = change_key.to_address(NetworkType.Mainnet) -print(f"Change: {change_address.to_string()}") -``` - -### Public Key Generator - -For watch-only wallets or when you only need addresses: - -```python -from kaspa import PublicKeyGenerator, NetworkType - -# Create from XPub string -pub_gen = PublicKeyGenerator.from_xpub("xpub...") - -# Or create from master XPrv -pub_gen = PublicKeyGenerator.from_master_xprv( - xprv=xprv_string, - is_multisig=False, - account_index=0 -) - -# Generate receive addresses -addresses = pub_gen.receive_addresses( - network_type=NetworkType.Mainnet, - start=0, - end=10 -) -for i, addr in enumerate(addresses): - print(f"Address {i}: {addr.to_string()}") - -# Get single address -single_addr = pub_gen.receive_address(NetworkType.Mainnet, 0) - -# Get as strings directly -addr_strings = pub_gen.receive_addresses_as_strings( - network_type=NetworkType.Mainnet, - start=0, - end=10 -) - -# Get public keys -pubkeys = pub_gen.receive_pubkeys(start=0, end=5) -pubkey_strings = pub_gen.receive_pubkeys_as_strings(start=0, end=5) -``` - -### Change Addresses - -Key generators provide both receive and change key paths: - -```python -from kaspa import PrivateKeyGenerator, PublicKeyGenerator, NetworkType - -# Private key generator -priv_gen = PrivateKeyGenerator(xprv_string, False, 0) -receive_key = priv_gen.receive_key(0) # m/.../0/0 -change_key = priv_gen.change_key(0) # m/.../1/0 - -# Public key generator -pub_gen = PublicKeyGenerator.from_xpub(xpub_string) -receive_addrs = pub_gen.receive_addresses(NetworkType.Mainnet, 0, 5) -change_addrs = pub_gen.change_addresses(NetworkType.Mainnet, 0, 5) -``` - -## Multi-Signature Wallets - -```python -from kaspa import PrivateKeyGenerator, PublicKeyGenerator - -# Each cosigner uses their own index -cosigner_0_gen = PrivateKeyGenerator( - xprv=xprv_string, - is_multisig=True, - account_index=0, - cosigner_index=0 -) - -cosigner_1_gen = PrivateKeyGenerator( - xprv=other_xprv_string, - is_multisig=True, - account_index=0, - cosigner_index=1 -) -``` - -## Account Types - -```python -from kaspa import AccountKind - -# Create account kind from string -bip32 = AccountKind("bip32") -legacy = AccountKind("legacy") -multisig = AccountKind("multisig") - -print(f"Account kind: {bip32.to_string()}") -``` - -## Complete Example: HD Wallet - -```python -from kaspa import ( - Mnemonic, XPrv, PrivateKeyGenerator, - PublicKeyGenerator, NetworkType -) - -# Create wallet from mnemonic -mnemonic = Mnemonic.random() -seed = mnemonic.to_seed() -master_xprv = XPrv(seed) - -# Derive account-level key -account_xprv = master_xprv.derive_path("m/44'/111111'/0'") -account_xpub = account_xprv.to_xpub() - -# Export XPub for watch-only wallet -xpub_export = account_xpub.to_str("kpub") -print(f"Watch-only XPub: {xpub_export}") - -# Full wallet with private keys -priv_gen = PrivateKeyGenerator(master_xprv, False, 0) - -# Watch-only wallet -pub_gen = PublicKeyGenerator.from_xpub(xpub_export) - -# Both generate the same addresses -for i in range(3): - # From private key generator - priv_addr = priv_gen.receive_key(i).to_address(NetworkType.Mainnet) - - # From public key generator - pub_addr = pub_gen.receive_address(NetworkType.Mainnet, i) - - assert priv_addr.to_string() == pub_addr.to_string() - print(f"Address {i}: {priv_addr.to_string()}") -``` diff --git a/docs/guides/message-signing.md b/docs/guides/message-signing.md index eb5c08fb..1bd5cb23 100644 --- a/docs/guides/message-signing.md +++ b/docs/guides/message-signing.md @@ -1,193 +1,110 @@ -# Message Signing +# Sign and verify a message -This guide covers signing and verifying arbitrary messages with the Kaspa Python SDK. +Sign arbitrary bytes with a `PrivateKey` and verify with the +corresponding `PublicKey`. Useful for proving address ownership, +authenticating off-chain actions, or stamping structured payloads. -!!! danger "Security Warning" - **Handle Private Keys Securely** +Read [Security](../getting-started/security.md) before signing with real +keys. - **These examples do not use proper private key/mnemonic/seed handling.** This is omitted for brevity. - - Never store your private keys in plain text, or directly in source code. Store securely offline. Anyone with access to this phrase has full control over your funds. - -## Overview - -Message signing allows you to: - -- **Prove ownership** of an address without revealing your private key -- **Sign data** for off-chain verification -- **Authenticate** actions or statements - -## Signing a Message +## Sign ```python -from kaspa import sign_message, PrivateKey - -# Your private key -private_key = PrivateKey("your-private-key-hex") - -# Message to sign -message = "Hello, I own this address!" +from kaspa import PrivateKey, sign_message -# Sign the message -signature = sign_message(message, private_key) -print(f"Signature: {signature}") +key = PrivateKey("<64-char hex>") +signature = sign_message("Hello, I own this address!", key) ``` -### Deterministic Signing - -By default, signatures use auxiliary randomness for additional security. For deterministic signatures: +For deterministic signatures (same message + same key produces the same +signature): ```python -# Deterministic signature (same message + key = same signature) -signature = sign_message(message, private_key, no_aux_rand=True) +signature = sign_message(message, key, no_aux_rand=True) ``` -## Verifying a Signature - -```python -from kaspa import verify_message, PublicKey - -# The public key of the signer -public_key = PublicKey("02a1b2c3...") - -# Or derive from private key -public_key = private_key.to_public_key() - -# Verify the signature -message = "Hello, I own this address!" -signature = "signature-from-signing..." +The default uses fresh auxiliary randomness; that's the right choice +unless a downstream consumer needs determinism. -is_valid = verify_message(message, signature, public_key) +## Verify -if is_valid: - print("Signature is valid!") -else: - print("Invalid signature!") -``` +```python +from kaspa import PublicKey, verify_message -## Complete Example +pub = PublicKey("02a1b2c3...") +# or: pub = key.to_public_key() -```python -from kaspa import ( - Mnemonic, XPrv, PrivateKeyGenerator, - sign_message, verify_message, NetworkType -) - -# Create a wallet -mnemonic = Mnemonic.random() -xprv = XPrv(mnemonic.to_seed()) -key_gen = PrivateKeyGenerator(xprv, False, 0) - -# Get a keypair -private_key = key_gen.receive_key(0) -public_key = private_key.to_public_key() -address = private_key.to_address(NetworkType.Mainnet) - -print(f"Address: {address.to_string()}") - -# Sign a message -message = f"I control address {address.to_string()} on 2024-01-15" -signature = sign_message(message, private_key) -print(f"Signature: {signature}") - -# Verify the signature -is_valid = verify_message(message, signature, public_key) -print(f"Valid: {is_valid}") - -# Try with wrong message -wrong_message = "I control a different address" -is_valid_wrong = verify_message(wrong_message, signature, public_key) -print(f"Wrong message valid: {is_valid_wrong}") # False +ok = verify_message(message, signature, pub) ``` -## Use Cases +`verify_message` returns `bool`. It does not raise on a mismatch. -### Proving Address Ownership +## Recipe: prove address ownership ```python -def prove_ownership(private_key, address, timestamp): - """Generate a proof of address ownership.""" +import time +from kaspa import sign_message, verify_message + +def prove_ownership(key, address): + timestamp = int(time.time()) message = f"I own {address.to_string()} at {timestamp}" - signature = sign_message(message, private_key) return { "address": address.to_string(), + "timestamp": timestamp, "message": message, - "signature": signature, - "timestamp": timestamp + "signature": sign_message(message, key), } -def verify_ownership(proof, public_key): - """Verify a proof of address ownership.""" - return verify_message( - proof["message"], - proof["signature"], - public_key - ) +def verify_ownership(proof, pub): + return verify_message(proof["message"], proof["signature"], pub) ``` -### Signing Structured Data +Include a timestamp so the proof can't be replayed indefinitely; the +verifier should reject anything older than its acceptable window. + +## Recipe: signed JSON payload + +When the thing you're signing is structured, canonicalise first: ```python import json -import hashlib - -def sign_json_data(data, private_key): - """Sign structured JSON data.""" - # Canonical JSON serialization - canonical = json.dumps(data, sort_keys=True, separators=(',', ':')) - - # Sign the serialized data - signature = sign_message(canonical, private_key) - - return { - "data": data, - "signature": signature - } +from kaspa import sign_message, verify_message -def verify_json_data(signed_data, public_key): - """Verify signed JSON data.""" - canonical = json.dumps( - signed_data["data"], - sort_keys=True, - separators=(',', ':') - ) - - return verify_message( - canonical, - signed_data["signature"], - public_key - ) +def sign_json(data, key): + canonical = json.dumps(data, sort_keys=True, separators=(",", ":")) + return {"data": data, "signature": sign_message(canonical, key)} + +def verify_json(envelope, pub): + canonical = json.dumps(envelope["data"], sort_keys=True, separators=(",", ":")) + return verify_message(canonical, envelope["signature"], pub) ``` -### Authentication Token +The canonicalisation matters: any non-deterministic serialisation +(default `json.dumps`, key reordering, whitespace) will produce a +signature that won't verify. + +## Recipe: time-limited auth token ```python import time +from kaspa import sign_message, verify_message -def create_auth_token(private_key, address, validity_seconds=300): - """Create a time-limited authentication token.""" - expires = int(time.time()) + validity_seconds +def create_token(key, address, ttl=300): + expires = int(time.time()) + ttl message = f"auth:{address.to_string()}:{expires}" - signature = sign_message(message, private_key) - return { "address": address.to_string(), "expires": expires, - "signature": signature + "signature": sign_message(message, key), } -def verify_auth_token(token, public_key): - """Verify an authentication token.""" - # Check expiration +def verify_token(token, pub): if int(time.time()) > token["expires"]: - return False, "Token expired" - - # Verify signature + return False, "expired" message = f"auth:{token['address']}:{token['expires']}" - is_valid = verify_message(message, token["signature"], public_key) - - if is_valid: - return True, "Valid" - else: - return False, "Invalid signature" + return verify_message(message, token["signature"], pub), "ok" ``` + +The signature covers the expiry, so a token can't be forged with a later +expiry without re-signing. Use it as a bearer token in HTTP headers, or +embed it in a session payload. diff --git a/docs/guides/mnemonics.md b/docs/guides/mnemonics.md index 6887c3b7..f1ac42d8 100644 --- a/docs/guides/mnemonics.md +++ b/docs/guides/mnemonics.md @@ -1,137 +1,74 @@ -# Mnemonics +# Generate or restore a mnemonic -!!! danger "Security Warning" - **Handle Private Keys Securely** +How to produce, validate, and convert BIP-39 mnemonic phrases. For the +teaching version of this material, see +[Wallet SDK → Key Management](../learn/wallet-sdk/key-management.md). - **These examples do not use proper private key/mnemonic/seed handling.** This is omitted for brevity. +Read [Security](../getting-started/security.md) before generating real +secrets. - Never store your private keys in plain text, or directly in source code. Store securely offline. Anyone with access to this phrase has full control over your funds. - -## Overview - -Mnemonic phrases (also called seed phrases or recovery phrases) are human-readable representations of cryptographic seeds. The Kaspa SDK supports BIP-39 compatible mnemonics. - -## Generating a New Mnemonic +## Generate ```python from kaspa import Mnemonic -# Generate a random 24-word mnemonic (default) -mnemonic = Mnemonic.random() -print(f"Your seed phrase: {mnemonic.phrase}") +# 24 words (default) +m = Mnemonic.random() + +# 12 words +m12 = Mnemonic.random(word_count=12) -# Generate with specific word count -mnemonic_12 = Mnemonic.random(word_count=12) # 12 words -mnemonic_24 = Mnemonic.random(word_count=24) # 24 words (recommended) +print(m.phrase) ``` -## Restoring from a Mnemonic +## Restore from a phrase ```python from kaspa import Mnemonic phrase = "abandon abandon abandon ... about" -# Validate before use -if Mnemonic.validate(phrase): - mnemonic = Mnemonic(phrase) - print("Mnemonic restored successfully") -else: - print("Invalid mnemonic phrase!") -``` - -## Validation - -```python -from kaspa import Mnemonic, Language +if not Mnemonic.validate(phrase): + raise ValueError("invalid mnemonic") -phrase = "word1 word2 word3 ..." - -# Basic validation -is_valid = Mnemonic.validate(phrase) -print(f"Valid: {is_valid}") - -# With specific language -is_valid_english = Mnemonic.validate(phrase, Language.English) +m = Mnemonic(phrase) ``` -## Converting to Seed - -```python -from kaspa import Mnemonic, XPrv +`Mnemonic.validate(phrase)` returns `True` / `False`; it does not raise. +Pass `Language.English` (or another wordlist) as the second argument to +validate against a specific language. -mnemonic = Mnemonic.random() +## Convert to a seed -# Convert to seed (without passphrase) -seed = mnemonic.to_seed() - -# Convert with optional passphrase for extra security -seed_with_passphrase = mnemonic.to_seed("my-secret-passphrase") - -# Use seed to create extended private key -xprv = XPrv(seed) +```python +seed = m.to_seed() # 64-byte BIP-39 seed +seed_passphrased = m.to_seed("25th-word") # different seed; same mnemonic ``` -!!! info "Passphrase" - The passphrase (sometimes called "25th word") provides additional security. The same mnemonic with different passphrases produces different seeds. - -## Working with Entropy +The optional passphrase changes the seed — the same mnemonic with +different passphrases produces different wallets. An attacker who +captures the mnemonic alone gets nothing without the passphrase. -Access the underlying entropy: +## Convert to an `XPrv` ```python -from kaspa import Mnemonic - -mnemonic = Mnemonic.random() - -# Get entropy as hex string -entropy = mnemonic.entropy -print(f"Entropy: {entropy}") - -# Set new entropy (advanced use) -mnemonic.entropy = "new-entropy-hex" +from kaspa import XPrv +xprv = XPrv(seed) ``` -## Language Support +From here, derive child keys with +[`PrivateKeyGenerator`](../learn/wallet-sdk/derivation.md) or load the +mnemonic into a managed [Wallet](../learn/wallet/private-keys.md). -```python -from kaspa import Mnemonic, Language +## Raw entropy -# Currently supported languages -mnemonic = Mnemonic.random() # Uses English by default - -# Specify language explicitly -mnemonic = Mnemonic(phrase, Language.English) -``` - -## Wallet Creation Example +The entropy is exposed as a hex string — useful when round-tripping +through a tool that emits entropy rather than words: ```python -from kaspa import ( - Mnemonic, XPrv, PrivateKeyGenerator, - NetworkType -) - -# Step 1: Generate mnemonic -mnemonic = Mnemonic.random() -print(f"Seed phrase: {mnemonic.phrase}") - -# Step 2: Convert to seed -seed = mnemonic.to_seed() - -# Step 3: Create extended private key -xprv = XPrv(seed) +m = Mnemonic.random() +print(m.entropy) -# Step 4: Create key generator -key_gen = PrivateKeyGenerator( - xprv=xprv, - is_multisig=False, - account_index=0 -) - -# Step 5: Derive addresses -for i in range(3): - private_key = key_gen.receive_key(i) - address = private_key.to_address(NetworkType.Mainnet) - print(f"Address {i}: {address.to_string()}") +m.entropy = "" +print(m.phrase) # rebuilt from the new entropy ``` diff --git a/docs/guides/multisig.md b/docs/guides/multisig.md new file mode 100644 index 00000000..8711bcf7 --- /dev/null +++ b/docs/guides/multisig.md @@ -0,0 +1,99 @@ +# Multi-signature transactions + +You have N cosigners and want a 2-of-3 (or M-of-N) wallet. Each cosigner +holds their own xprv; spending requires M signatures. + +This is a how-to. For the underlying derivation and transaction +generation primitives, see +[Wallet SDK → Derivation](../learn/wallet-sdk/derivation.md) and +[Wallet SDK → Transaction Generator](../learn/wallet-sdk/tx-generator.md). + +## Build a 2-of-3 multisig address + +```python +from kaspa import ( + create_multisig_address, NetworkType, PublicKey, + PrivateKeyGenerator, +) + +# Each cosigner derives the same account-level public key, with their +# own cosigner_index. Public keys (not private!) are exchanged. +gen0 = PrivateKeyGenerator(xprv=cosigner_0_xprv, is_multisig=True, + account_index=0, cosigner_index=0) +gen1 = PrivateKeyGenerator(xprv=cosigner_1_xprv, is_multisig=True, + account_index=0, cosigner_index=1) +gen2 = PrivateKeyGenerator(xprv=cosigner_2_xprv, is_multisig=True, + account_index=0, cosigner_index=2) + +pubkeys = [ + PublicKey(gen0.receive_key(0).to_public_key().to_string()), + PublicKey(gen1.receive_key(0).to_public_key().to_string()), + PublicKey(gen2.receive_key(0).to_public_key().to_string()), +] + +multisig = create_multisig_address( + minimum_signatures=2, + keys=pubkeys, + network_type=NetworkType.Mainnet, +) +print(multisig.to_string()) +``` + +Send funds to `multisig.to_string()` and you've created a 2-of-3 UTXO +set. + +## Spend from the multisig + +```python +from kaspa import Generator, NetworkId, PaymentOutput + +gen = Generator( + network_id=NetworkId("mainnet"), + entries=multisig_utxos, # UTXOs paying to multisig.address + change_address=multisig, # change goes back to the multisig + outputs=[PaymentOutput(recipient, amount)], + minimum_signatures=2, # accurate mass calculation +) + +for pending in gen: + # Two of the three cosigners' keys + pending.sign([cosigner_0_key, cosigner_1_key]) + tx_id = await pending.submit(client) +``` + +`minimum_signatures=2` matters: the `Generator` budgets mass for the +expected number of signatures. Skip it and the resulting transaction is +under-massed and the node will reject it. + +## Coordinating signatures across machines + +For a real multisig, the cosigners aren't co-resident. The wire-format +hand-off is: + +1. **Coordinator** runs `gen` and collects each `pending.transaction` (or + the dict form `pending.transaction.to_dict()`). +2. **Each signer** receives the unsigned transaction, signs only their + inputs with `pending.create_input_signature(...)`, and ships the + signature back. +3. **Coordinator** assembles the signatures into the + `signature_script` for each input via `pending.fill_input(i, ...)`, + then submits. + +See `pending.create_input_signature(input_index, private_key, +sighash_type=SighashType.All)` and `pending.fill_input(i, script_bytes)` — +both are documented in +[Wallet SDK → Transaction Generator](../learn/wallet-sdk/tx-generator.md). + +## Notes + +- All cosigners must use the **same `account_index`** — the multisig + address is a function of the public keys at that level, and any + mismatch produces a different address. +- `cosigner_index` differentiates the cosigners; it's not a "first / + second / third signer" ordering, it's a deterministic position used + during derivation. Pick once and document it alongside the wallet. +- Multisig addresses are `ScriptHash`-version (see + [Addresses](../learn/addresses.md)). +- For derivation across more than one address index, every cosigner + derives in lockstep — `gen0.receive_key(i)`, `gen1.receive_key(i)`, + `gen2.receive_key(i)` for the same `i`. diff --git a/docs/guides/rpc-client.md b/docs/guides/rpc-client.md deleted file mode 100644 index 2e7bac8d..00000000 --- a/docs/guides/rpc-client.md +++ /dev/null @@ -1,414 +0,0 @@ -# RPC Client - -## Overview - -The Kaspa Python SDK provides an asynchronous RPC client for communicating with Kaspa nodes via WebSocket. Features include: - -- **Automatic node discovery (PNN)** via Resolver. -- **Connection management** with reconnection support. -- **Full RPC API** coverage, including event subscriptions for real-time updates. - -## Quick Start - -```python -import asyncio -from kaspa import RpcClient, Resolver - -async def main(): - # Create client with resolver - client = RpcClient( - resolver=Resolver(), - network_id="mainnet" - ) - - # Connect - await client.connect() - print(f"Connected to: {client.url}") - - # Make RPC calls - info = await client.get_info() - print(f"Server info: {info}") - - # Disconnect - await client.disconnect() - -asyncio.run(main()) -``` - -## Connection Options - -### Using a Resolver - -The Resolver automatically finds available PNN nodes: - -```python -from kaspa import RpcClient, Resolver - -# Default resolver (uses public infrastructure) -resolver = Resolver() - -# Custom resolver URLs -resolver = Resolver(urls=["https://resolver1.kaspa.org"]) - -# With TLS configuration -resolver = Resolver(tls=True) - -client = RpcClient(resolver=resolver, network_id="mainnet") -``` - -### Direct Connection - -Connect directly to a known node: - -```python -from kaspa import RpcClient - -client = RpcClient( - url="wss://node.kaspa.org:17110", - network_id="mainnet", - encoding="borsh" # or "json" -) -``` - -### Connection Parameters - -```python -await client.connect( - block_async_connect=True, # Wait for connection - strategy="fallback", # Connection strategy - timeout_duration=30000, # Timeout in ms - retry_interval=1000, # Retry interval in ms -) -``` - -## Client Properties - -```python -# Check connection status -print(f"Connected: {client.is_connected}") - -# Get current URL -print(f"URL: {client.url}") - -# Get encoding -print(f"Encoding: {client.encoding}") - -# Get node ID -print(f"Node ID: {client.node_id}") - -# Get resolver -resolver = client.resolver -``` - -## RPC Methods - -### Network Information - -```python -# Get general info -info = await client.get_info() - -# Get block count -count = await client.get_block_count() -print(f"Blocks: {count['blockCount']}, Headers: {count['headerCount']}") - -# Get block DAG info -dag_info = await client.get_block_dag_info() -print(f"Network: {dag_info['networkName']}") -print(f"Block count: {dag_info['blockCount']}") - -# Get coin supply -supply = await client.get_coin_supply() -print(f"Circulating: {supply['circulatingSompi']}") - -# Get current network -network = await client.get_current_network() - -# Get sync status -sync = await client.get_sync_status() -print(f"Synced: {sync['isSynced']}") -``` - -### Balance and UTXOs - -```python -# Get balance for single address -balance = await client.get_balance_by_address({ - "address": "kaspa:qz..." -}) -print(f"Balance: {balance['balance']} sompi") - -# Get balances for multiple addresses -balances = await client.get_balances_by_addresses({ - "addresses": ["kaspa:qz...", "kaspa:qr..."] -}) - -# Get UTXOs -utxos = await client.get_utxos_by_addresses({ - "addresses": ["kaspa:qz..."] -}) -for entry in utxos.get("entries", []): - print(f"UTXO: {entry['outpoint']} = {entry['utxoEntry']['amount']}") -``` - -### Blocks - -```python -# Get specific block -block = await client.get_block({ - "hash": "block-hash-hex", - "includeTransactions": True -}) - -# Get multiple blocks -blocks = await client.get_blocks({ - "lowHash": "starting-hash", - "includeBlocks": True, - "includeTransactions": False -}) - -# Get block template (for mining) -template = await client.get_block_template({ - "payAddress": "kaspa:mining-address...", - "extraData": [] -}) -``` - -### Transactions - -```python -# Submit transaction -from kaspa import Transaction - -result = await client.submit_transaction({ - "transaction": tx.serialize_to_dict(), - "allowOrphan": False -}) -print(f"Transaction ID: {result['transactionId']}") - -# Get mempool entries -mempool = await client.get_mempool_entries({ - "includeOrphanPool": False, - "filterTransactionPool": True -}) - -# Get mempool entry by transaction ID -entry = await client.get_mempool_entry({ - "transactionId": "tx-id...", - "includeOrphanPool": False, - "filterTransactionPool": True -}) -``` - -### Fees - -```python -# Get fee estimate -fee = await client.get_fee_estimate() -print(f"Priority fee: {fee['estimate']['priorityBucket']}") - -# Experimental fee estimate with more detail -fee_exp = await client.get_fee_estimate_experimental({ - "verbose": True -}) -``` - -### Peer Management - -```python -# Get connected peers -peers = await client.get_connected_peer_info() - -# Get peer addresses -addresses = await client.get_peer_addresses() - -# Add peer -await client.add_peer({ - "peerAddress": "192.168.1.1:16111", - "isPermanent": False -}) - -# Ban/unban peer -await client.ban({"ip": "192.168.1.1"}) -await client.unban({"ip": "192.168.1.1"}) -``` - -### System - -```python -# Ping node -pong = await client.ping() - -# Get server info -server_info = await client.get_server_info() - -# Get system info -system_info = await client.get_system_info() - -# Get metrics -metrics = await client.get_metrics({ - "processMetrics": True, - "connectionMetrics": True, - "bandwidthMetrics": True, - "consensusMetrics": True, - "storageMetrics": False, - "customMetrics": False -}) -``` - -## Event Subscriptions - -Subscribe to real-time events. - -### Available Events - -| Event | Subscription Method | -|-------|---------------------| -| `utxos-changed` | `subscribe_utxos_changed()` | -| `block-added` | `subscribe_block_added()` | -| `virtual-chain-changed` | `subscribe_virtual_chain_changed()` | -| `virtual-daa-score-changed` | `subscribe_virtual_daa_score_changed()` | -| `sink-blue-score-changed` | `subscribe_sink_blue_score_changed()` | -| `finality-conflict` | `subscribe_finality_conflict()` | -| `finality-conflict-resolved` | `subscribe_finality_conflict_resolved()` | -| `new-block-template` | `subscribe_new_block_template()` | -| `pruning-point-utxo-set-override` | `subscribe_pruning_point_utxo_set_override()` | - - -### UTXO Changes - -```python -from kaspa import Address - -# Define callback -def on_utxo_change(event): - print(f"UTXO change: {event}") - -# Add listener -client.add_event_listener("utxos-changed", on_utxo_change) - -# Subscribe to addresses -await client.subscribe_utxos_changed([ - Address("kaspa:qz...") -]) - -# Later: unsubscribe -await client.unsubscribe_utxos_changed([ - Address("kaspa:qz...") -]) -``` - -### Block Events - -```python -def on_block_added(event): - print(f"New block: {event['block']['header']['hash']}") - -client.add_event_listener("block-added", on_block_added) -await client.subscribe_block_added() -``` - -### Virtual Chain Changes - -```python -def on_chain_change(event): - print(f"Chain updated: {event}") - -client.add_event_listener("virtual-chain-changed", on_chain_change) -await client.subscribe_virtual_chain_changed( - include_accepted_transaction_ids=True -) -``` - -### DAA Score Changes - -```python -def on_daa_change(event): - print(f"DAA score: {event['virtualDaaScore']}") - -client.add_event_listener("virtual-daa-score-changed", on_daa_change) -await client.subscribe_virtual_daa_score_changed() -``` - -### Managing Listeners - -```python -# Add listener with extra args -client.add_event_listener("block-added", callback, extra_arg) - -# Remove specific listener -client.remove_event_listener("block-added", callback) - -# Remove all listeners for an event -client.remove_event_listener("block-added") - -# Remove all listeners -client.remove_all_event_listeners() -``` - -## Complete Example: Wallet Monitor - -```python -import asyncio -from kaspa import RpcClient, Resolver, Address, sompi_to_kaspa - -class WalletMonitor: - def __init__(self, addresses): - self.addresses = [Address(a) for a in addresses] - self.client = RpcClient( - resolver=Resolver(), - network_id="mainnet" - ) - - async def start(self): - await self.client.connect() - - # Set up event handler - self.client.add_event_listener( - "utxos-changed", - self.on_utxo_change - ) - - # Subscribe to address changes - await self.client.subscribe_utxos_changed(self.addresses) - - # Get initial balances - await self.check_balances() - - print("Monitoring... Press Ctrl+C to stop") - - # Keep running - while True: - await asyncio.sleep(1) - - async def check_balances(self): - for addr in self.addresses: - result = await self.client.get_balance_by_address({ - "address": addr.to_string() - }) - balance = sompi_to_kaspa(result.get("balance", 0)) - print(f"{addr.to_string()[:20]}...: {balance} KAS") - - def on_utxo_change(self, event): - print(f"UTXO change detected!") - for added in event.get("added", []): - amount = sompi_to_kaspa(added["utxoEntry"]["amount"]) - print(f" + {amount} KAS") - for removed in event.get("removed", []): - amount = sompi_to_kaspa(removed["utxoEntry"]["amount"]) - print(f" - {amount} KAS") - - async def stop(self): - await self.client.disconnect() - -async def main(): - addresses = ["kaspa:qz..."] - monitor = WalletMonitor(addresses) - - try: - await monitor.start() - except KeyboardInterrupt: - await monitor.stop() - -asyncio.run(main()) -``` \ No newline at end of file diff --git a/docs/guides/transactions.md b/docs/guides/transactions.md deleted file mode 100644 index 293e8304..00000000 --- a/docs/guides/transactions.md +++ /dev/null @@ -1,361 +0,0 @@ -# Transactions - -This guide covers building, signing, and broadcasting transactions with the Kaspa Python SDK. - -!!! danger "Security Warning" - **Handle Private Keys Securely** - - **These examples do not use proper private key/mnemonic/seed handling.** This is omitted for brevity. - - Never store your private keys in plain text, or directly in source code. Store securely offline. Anyone with access to this phrase has full control over your funds. - -## Using the Generator - -The `Generator` class handles UTXO selection, fee calculation, and change management: - -```python -import asyncio -from kaspa import ( - RpcClient, Resolver, Generator, PaymentOutput, - Address, PrivateKey, NetworkId -) - -async def send_payment(): - # Connect to network - client = RpcClient(resolver=Resolver(), network_id="mainnet") - await client.connect() - - try: - # Your private key and address - private_key = PrivateKey("your-private-key-hex") - my_address = private_key.to_address("mainnet") - - # Fetch UTXOs - utxos = await client.get_utxos_by_addresses({ - "addresses": [my_address.to_string()] - }) - - # Define payment - recipient = Address("kaspa:recipient-address...") - amount = 500_000_000 # 5 KAS in sompi - - # Create generator - generator = Generator( - network_id=NetworkId("mainnet"), - entries=utxos["entries"], - change_address=my_address, - outputs=[PaymentOutput(recipient, amount)], - ) - - # Process transactions - for pending_tx in generator: - pending_tx.sign([private_key]) - tx_id = await pending_tx.submit(client) - print(f"Submitted: {tx_id}") - - # Get summary - summary = generator.summary() - print(f"Total fees: {summary.fees}") - print(f"Transactions: {summary.transactions}") - - finally: - await client.disconnect() - -asyncio.run(send_payment()) -``` - -## Generator Options - -```python -from kaspa import Generator, NetworkId, PaymentOutput - -generator = Generator( - # Required - network_id=NetworkId("mainnet"), - entries=utxo_entries, # List of UTXOs - change_address=my_address, # Where to send change - - # Optional - outputs=[payment1, payment2], # Payment outputs - payload=b"optional-data", # OP_RETURN data - priority_fee=1000, # Additional fee in sompi - priority_entries=priority_utxos, # UTXOs to use first - sig_op_count=1, # Signature operations per input - minimum_signatures=1, # For multisig estimation -) -``` - -## Estimating Transactions - -Transactions can be estimated prior to submission. - -```python -from kaspa import Generator, estimate_transactions - -# Using Generator.estimate() -generator = Generator( - network_id="mainnet", - entries=utxos, - change_address=my_address, - outputs=[PaymentOutput(recipient, amount)], -) - -summary = generator.estimate() -print(f"Estimated fee: {summary.fees} sompi") -print(f"Number of transactions: {summary.transactions}") -print(f"UTXOs consumed: {summary.utxos}") - -# Using standalone function -summary = estimate_transactions( - network_id="mainnet", - entries=utxos, - change_address=my_address, - outputs=[{"address": recipient, "amount": amount}], -) -``` - -## Pending Transactions - -The `PendingTransaction` represents a transaction ready for signing: - -```python -for pending_tx in generator: - # Transaction properties - print(f"ID: {pending_tx.id}") - print(f"Payment amount: {pending_tx.payment_amount}") - print(f"Change amount: {pending_tx.change_amount}") - print(f"Fee: {pending_tx.fee_amount}") - print(f"Mass: {pending_tx.mass}") - print(f"Type: {pending_tx.transaction_type}") - - # Get UTXOs being spent - utxo_refs = pending_tx.get_utxo_entries() - - # Get addresses involved - addresses = pending_tx.addresses() - - # Access underlying transaction - tx = pending_tx.transaction -``` - -## Signing Transactions - -### Simple Signing - -```python -# Sign with one or more private keys -pending_tx.sign([private_key]) - -# Or sign with multiple keys for multisig -pending_tx.sign([key1, key2, key3]) -``` - -### Per-Input Signing - -For more control, sign each input individually: - -```python -for i, utxo in enumerate(pending_tx.get_utxo_entries()): - pending_tx.sign_input(i, private_key) -``` - -### Custom Signature Scripts - -For advanced use cases (like multisig): - -```python -from kaspa import SighashType - -# Create signature -signature = pending_tx.create_input_signature( - input_index=0, - private_key=private_key, - sighash_type=SighashType.All -) - -# Set custom signature script -pending_tx.fill_input(0, signature_script_bytes) -``` - -## Manual Transaction Building - -Transactions can be built manually: - -```python -from kaspa import ( - Transaction, TransactionInput, TransactionOutput, - TransactionOutpoint, ScriptPublicKey, UtxoEntryReference, - sign_transaction -) - -# Create inputs from UTXOs -inputs = [] -for utxo in my_utxos: - outpoint = TransactionOutpoint( - transaction_id=utxo["outpoint"]["transactionId"], - index=utxo["outpoint"]["index"] - ) - tx_input = TransactionInput( - previous_outpoint=outpoint, - signature_script="", # Will be filled when signing - sequence=0, - sig_op_count=1, - utxo=UtxoEntryReference(utxo) - ) - inputs.append(tx_input) - -# Create outputs -outputs = [ - TransactionOutput( - value=amount, - script_public_key=pay_to_address_script(recipient) - ), - TransactionOutput( - value=change_amount, - script_public_key=pay_to_address_script(change_address) - ) -] - -# Build transaction -tx = Transaction( - version=0, - inputs=inputs, - outputs=outputs, - lock_time=0, - subnetwork_id="0000000000000000000000000000000000000000", - gas=0, - payload="", - mass=0 -) - -# Calculate and update mass -from kaspa import update_transaction_mass -update_transaction_mass("mainnet", tx) - -# Sign -signed_tx = sign_transaction(tx, [private_key], verify_sig=True) -``` - -## Transaction Mass and Fees - -Kaspa uses a mass-based fee model: - -```python -from kaspa import ( - calculate_transaction_mass, - calculate_transaction_fee, - calculate_storage_mass, - maximum_standard_transaction_mass -) - -# Get maximum allowed mass -max_mass = maximum_standard_transaction_mass() -print(f"Max mass: {max_mass}") - -# Calculate transaction mass -mass = calculate_transaction_mass("mainnet", tx) -print(f"Transaction mass: {mass}") - -# Calculate required fee -fee = calculate_transaction_fee("mainnet", tx) -print(f"Required fee: {fee} sompi") - -# Calculate storage mass component -storage_mass = calculate_storage_mass( - network_id="mainnet", - input_values=[1000000, 2000000], - output_values=[2500000, 400000] -) -``` - -## Submitting Transactions - -```python -# Using PendingTransaction -tx_id = await pending_tx.submit(client) - -# Manual submission -result = await client.submit_transaction({ - "transaction": tx.serialize_to_dict(), - "allowOrphan": False -}) -``` - -## Helper Functions - -### Create Single Transaction - -```python -from kaspa import create_transaction - -tx = create_transaction( - utxo_entry_source=utxos, - outputs=[{"address": "kaspa:...", "amount": 100000000}], - priority_fee=1000, - payload=None, - sig_op_count=1 -) -``` - -### Create Multiple Transactions - -```python -from kaspa import create_transactions - -result = create_transactions( - network_id="mainnet", - entries=utxos, - change_address=my_address, - outputs=[{"address": "kaspa:...", "amount": 100000000}], - priority_fee=1000, -) - -for pending in result["transactions"]: - pending.sign([private_key]) - # submit... - -print(f"Summary: {result['summary']}") -``` - -## Multi-Signature Transactions - -```python -from kaspa import ( - Generator, create_multisig_address, - PublicKey, NetworkType -) - -# Create multisig address (2-of-3) -pubkeys = [PublicKey(k) for k in [key1_pub, key2_pub, key3_pub]] -multisig_addr = create_multisig_address(2, pubkeys, NetworkType.Mainnet) - -# Build transaction spending from multisig -generator = Generator( - network_id="mainnet", - entries=multisig_utxos, - change_address=multisig_addr, - outputs=[PaymentOutput(recipient, amount)], - minimum_signatures=2, # For accurate mass calculation -) - -for pending_tx in generator: - # Collect signatures from 2 of 3 signers - pending_tx.sign([signer1_key, signer2_key]) - tx_id = await pending_tx.submit(client) -``` - -## Unit Conversions - -```python -from kaspa import kaspa_to_sompi, sompi_to_kaspa, sompi_to_kaspa_string_with_suffix - -# KAS to sompi -sompi = kaspa_to_sompi(1.5) # 150,000,000 sompi - -# Sompi to KAS -kas = sompi_to_kaspa(150000000) # 1.5 KAS - -# Formatted string -formatted = sompi_to_kaspa_string_with_suffix(150000000, "mainnet") -# "1.5 KAS" -``` diff --git a/docs/guides/wallet-recovery.md b/docs/guides/wallet-recovery.md new file mode 100644 index 00000000..0f7b78e0 --- /dev/null +++ b/docs/guides/wallet-recovery.md @@ -0,0 +1,96 @@ +# Recover a wallet (BIP-44 scan) + +You have a 24-word mnemonic and you want to restore the accounts it +backs. `accounts_discovery` scans BIP-44 account and address ranges +against a connected node, returns the highest-used `account_index`, and +gets you a list of indices to import. + +This is a how-to. For background on what these primitives mean, see +[Wallet → Accounts](../learn/wallet/accounts.md) and +[Wallet → Private Keys](../learn/wallet/private-keys.md). + +## Recipe + +```python +import asyncio +from kaspa import ( + AccountsDiscoveryKind, PrvKeyDataVariantKind, Resolver, Wallet, +) + +MNEMONIC = "<24 words>" +SECRET = "" + +async def main(): + wallet = Wallet(network_id="mainnet", resolver=Resolver()) + await wallet.start() + await wallet.connect() + + last_used = await wallet.accounts_discovery( + discovery_kind=AccountsDiscoveryKind.Bip44, + address_scan_extent=20, # consecutive empty addresses before stopping + account_scan_extent=5, # consecutive empty accounts before stopping + bip39_mnemonic=MNEMONIC, + bip39_passphrase=None, + ) + print(f"highest used account_index: {last_used}") + + # Open (or create) a wallet file to hold the imports + await wallet.wallet_create(wallet_secret=SECRET, filename="restored") + + pkd_id = await wallet.prv_key_data_create( + wallet_secret=SECRET, + secret=MNEMONIC, + kind=PrvKeyDataVariantKind.Mnemonic, + ) + + descriptors = [] + for i in range(0, last_used + 1): + d = await wallet.accounts_import_bip32( + wallet_secret=SECRET, + prv_key_data_id=pkd_id, + account_index=i, + ) + descriptors.append(d) + + while not wallet.is_synced: + await asyncio.sleep(0.5) + + await wallet.accounts_activate([d.account_id for d in descriptors]) + + await wallet.wallet_close() + await wallet.disconnect() + await wallet.stop() + +asyncio.run(main()) +``` + +## Tuning the extents + +- **`address_scan_extent`** — how far past the last used receive address + to look before declaring an account empty. BIP-44's recommended value + is 20. +- **`account_scan_extent`** — how many consecutive empty accounts to + tolerate before stopping. Most wallets stop at 5; raise it if the + user is known to have skipped account indices. + +If the discovery returns `-1`, no on-chain history exists under that +mnemonic — treat it as a fresh wallet (use `accounts_create_bip32` +instead of import). + +## Why `_import_*` and not `_create_*` + +`accounts_import_bip32` runs an address-discovery scan as part of the +import — addresses already in use are recognised and the receive / +change indices advance accordingly. The `_create_*` variants don't, so +the next address derived would silently re-issue an already-used one. +For a recovery flow, always import. + +## Notes + +- `accounts_discovery` does not require a wallet file to be open; it + *does* require a connected wRPC client. +- For testnets, set `network_id="testnet-10"` (or `testnet-11`) on both + the discovery and the wallet open. +- A passphrase-protected mnemonic must pass the same passphrase to + `bip39_passphrase=...` and to `prv_key_data_create(...)`. Mismatched + passphrases derive unrelated wallets. diff --git a/docs/index.md b/docs/index.md index cd9d0c9c..3362fee3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,24 +1,22 @@ # Kaspa Python SDK -This Python package, `kaspa`, provides an SDK for interacting with the Kaspa network from Python. +This Python package, `kaspa`, provides an SDK for interacting with the +Kaspa network from Python. -`kaspa` is a native extension module built from bindings to Rust and [rusty-kaspa](https://github.com/kaspanet/rusty-kaspa) source. [PyO3](https://pyo3.rs/) and [Maturin](https://www.maturin.rs/) are used to create bindings and build the extension module. More information on the inner workings can be found in the [Contributing section](contributing/index.md). +`kaspa` is a native extension module built from bindings to Rust and +[rusty-kaspa](https://github.com/kaspanet/rusty-kaspa) source. +[PyO3](https://pyo3.rs/) and [Maturin](https://www.maturin.rs/) are used +to create bindings and build the extension module. More information on +the inner workings can be found in the +[Contributing section](contributing/index.md). !!! warning "Beta Status" This project is in beta status. -This project very closely mirrors [Kaspa's WASM SDK](https://kaspa.aspectron.org/docs/), while trying to respect Python conventions. Feature parity with WASM SDK is a work in progress, not all features are available yet in Python. - -This documentation site currently provides API reference and basic usage guides. General cryptocurrency concepts, development practices, and Kaspa specific concepts are not covered here. - -## Features - -This SDK provides features in two primary categories: - -- **RPC Client** - Connect to Kaspa nodes via RPC. -- **Wallet Management** - Wallet related functionality (key management, derivation, addresses, transactions, etc.). - -Most features gaps with Kaspa WASM SDK exist around Wallet functionality. +This project closely mirrors +[Kaspa's WASM SDK](https://kaspa.aspectron.org/docs/), while trying to +respect Python conventions. Feature parity with the WASM SDK is a work +in progress; not every feature is available yet in Python. ## A (Very) Basic Example @@ -35,40 +33,40 @@ if __name__ == "__main__": asyncio.run(main()) ``` -## Getting Started +## How the docs are organised -1. [Installation](getting-started/installation.md) - Set up the SDK in your environment -2. [Examples](getting-started/examples.md) - Build your first Kaspa application - -## Guides +This site follows the [Diataxis](https://diataxis.fr) framework. Each +section answers a different question:
-- **[RPC Client](guides/rpc-client.md)** - Connect to Kaspa nodes - -- **[Transactions](guides/transactions.md)** - Build and sign transactions +- **[Getting Started](getting-started/installation.md)** + Install the SDK, run the first script, read the security note before + generating real keys. -- **[Addresses](guides/addresses.md)** - Create and validate Kaspa addresses +- **[Learn](learn/index.md)** + How the SDK is shaped, taught topic by topic. Connections, wallets, + derivation, transactions, the Kaspa concepts behind them. -- **[Mnemonics](guides/mnemonics.md)** - Generate and use seed phrases +- **[Guides](guides/mnemonics.md)** + Recipes for specific tasks — mnemonic restore, message signing, + multisig, wallet recovery, custom derivation. -- **[Key Derivation](guides/key-derivation.md)** - HD wallet key generation - -- **[Message Signing](guides/message-signing.md)** - Sign and verify messages +- **[API Reference](reference/index.md)** + Every public class, method, and signature. Auto-generated.
-## API Reference +## Where to start -For complete API documentation, see the [API Reference](reference/index.md). +- **New to the SDK:** [Installation](getting-started/installation.md) → + [Learn → RPC](learn/rpc/index.md) → + [Learn → Wallet SDK → Key Management](learn/wallet-sdk/key-management.md). +- **Looking for a recipe:** jump to [Guides](guides/mnemonics.md). +- **Looking up an API:** [API Reference](reference/index.md). +- **Generating real keys:** read + [Security](getting-started/security.md) first. ## License This project is licensed under the ISC License. - diff --git a/docs/learn/addresses.md b/docs/learn/addresses.md new file mode 100644 index 00000000..2506ee47 --- /dev/null +++ b/docs/learn/addresses.md @@ -0,0 +1,144 @@ +# Addresses + +A Kaspa address encodes a public key or a script hash, the address +*version* (which signature scheme or script type it pays to), and the +network it belongs to. The SDK exposes them as `Address` instances. + +## Anatomy + +``` +kaspa: qz0s9f5p7d3e2c4x8n1b6m9k0j2h4g5f3d7a8s9w0e1r2t3y4u5i6o7p8 +^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +prefix bech32-encoded version + payload + checksum +``` + +| Component | Source | +| --- | --- | +| Prefix | The network (see [Networks](networks.md)). | +| Version | One of `PubKey` (Schnorr), `PubKeyECDSA`, or `ScriptHash`. | +| Payload | The hash / public key bytes. | + +## Construct or parse + +```python +from kaspa import Address + +addr = Address("kaspa:qz...") +print(addr.prefix, addr.version, addr.to_string()) +``` + +`Address(string)` raises on malformed input. Use `Address.validate(s)` +to check first without raising: + +```python +if Address.validate(s): + addr = Address(s) +``` + +## From a key + +Schnorr (the default): + +```python +from kaspa import NetworkType, PrivateKey + +key = PrivateKey("<64-char hex>") +addr = key.to_address(NetworkType.Mainnet) +``` + +ECDSA: + +```python +ecdsa_addr = key.to_address_ecdsa(NetworkType.Mainnet) +``` + +From a public key: + +```python +from kaspa import PublicKey + +pub = PublicKey("02a1b2c3...") +addr = pub.to_address(NetworkType.Mainnet) +``` + +## Versions + +| Version | Pays to | Used by | +| --- | --- | --- | +| `PubKey` | Schnorr public key | The SDK's default key derivation; every BIP32 address. | +| `PubKeyECDSA` | ECDSA public key | Keypair accounts created with `ecdsa=True`. | +| `ScriptHash` | A script hash (P2SH-style) | Multisig addresses; custom scripts. | + +```python +addr = Address("kaspa:qz...") +print(addr.version) +``` + +## Network prefixes + +| Prefix | Network | +| --- | --- | +| `kaspa:` | mainnet | +| `kaspatest:` | testnet-10, testnet-11 | +| `kaspadev:` | devnet | +| `kaspasim:` | simnet | + +To re-encode an address for a different network — for example, to +display the testnet equivalent of a known mainnet address during +testing — set the prefix: + +```python +addr = Address("kaspa:qz...") +addr.prefix = "kaspatest" +print(addr.to_string()) # kaspatest:qz... +``` + +This *does not* re-derive the address from a key; it just rewrites the +prefix. For programmatic conversion of an actual key to a different +network's address, derive again with the right `NetworkType`. + +## Scripts and addresses + +```python +from kaspa import ( + Address, NetworkType, ScriptPublicKey, + address_from_script_public_key, pay_to_address_script, +) + +# script → address +spk = ScriptPublicKey(0, "20a1b2c3...") +addr = address_from_script_public_key(spk, NetworkType.Mainnet) + +# address → script +spk = pay_to_address_script(Address("kaspa:qz...")) +print(spk.script) +``` + +`pay_to_address_script` is the lockup script you put in a +`TransactionOutput` to pay to that address. See [Transactions → Outputs](transactions/outputs.md). + +## Multi-signature addresses + +```python +from kaspa import create_multisig_address, NetworkType, PublicKey + +pubkeys = [PublicKey("02key1..."), PublicKey("02key2..."), PublicKey("02key3...")] +multi = create_multisig_address( + minimum_signatures=2, + keys=pubkeys, + network_type=NetworkType.Mainnet, +) +print(multi.to_string()) +``` + +For the full multisig spend flow (creating the address, signing with +multiple cosigners, submitting), see the +[Multi-signature transactions](../guides/multisig.md) recipe. + +## Where to next + +- [Networks](networks.md) — what each prefix means. +- [Transactions](transactions/index.md) — using addresses inside transaction + outputs. +- [Wallet SDK → Derivation](wallet-sdk/derivation.md) — deriving many + addresses from one key. diff --git a/docs/learn/concepts.md b/docs/learn/concepts.md new file mode 100644 index 00000000..9b474423 --- /dev/null +++ b/docs/learn/concepts.md @@ -0,0 +1,114 @@ +# Kaspa Concepts + +This page is a fast tour of the protocol concepts you bump into when +using the SDK. It's deliberately surface-level — for the protocol-level +details, see the [Kaspa MDBook](https://kaspa-mdbook.aspectron.com/). + +## BlockDAG, not blockchain + +Kaspa orders transactions in a **directed acyclic graph of blocks**, not +a linear chain. Multiple blocks can be produced in parallel and reference +the same parents; consensus emerges from a deterministic ordering of +the DAG (the *virtual chain*) rather than from a single longest chain. + +The practical consequence for SDK users: + +- **Block rate is high** (one or more blocks per second on testnet-10). + You see far more `block-added` events than you would on a Bitcoin-shaped + chain. +- **Transactions confirm via the virtual chain.** A transaction is + "accepted" once it appears in the virtual-chain ordering, not when it + first lands in a block. +- **Reorgs happen at the DAG-ordering level.** A previously-accepted + transaction can be re-ordered out; the SDK surfaces this as a + [`Reorg` event](wallet/transaction-history.md). + +## UTXO model + +Every spendable balance is a set of *unspent transaction outputs*. To +spend, you select UTXOs that sum to at least the amount you need, plus a +change output for the leftover. + +The SDK never asks "what's my balance" of the chain directly — it tracks +UTXOs locally, derives a balance from them, and updates as new ones land +or existing ones spend. See +[Wallet → Architecture](wallet/architecture.md) and +[Wallet SDK → UTXO Context](wallet-sdk/utxo-context.md). + +## Virtual chain and DAA score + +Two ordering scalars come up in the SDK: + +- **DAA score** (Difficulty Adjustment Algorithm) — a monotonic counter + that increases roughly with wall-clock time. Used as a "what age is + this block" comparator; surfaces on every UTXO via + `block_daa_score`. +- **Virtual chain** — the canonical DAG ordering. The + `virtual-chain-changed` notification reports updates to it; transactions + are confirmed by appearing in it. + +For "wait until N seconds have passed", DAA score is roughly the right +metric. For "wait until this transaction is confirmed", a `Maturity` +event is the right gate (see below). + +## Maturity + +A UTXO moves through three states in the wallet's view: + +- **Pending** — seen, but not deeply enough confirmed to be spent. +- **Mature** — confirmed past the maturity threshold; spendable. +- **Outgoing** — locked because the wallet just spent it; awaits its + spend transaction maturing or being re-orged out. + +[Coinbase outputs](https://kaspa-mdbook.aspectron.com/) have a longer +maturity than regular transaction outputs. The SDK applies the right +threshold automatically — you observe the result via `Pending` / +`Maturity` events on either the [managed +Wallet](wallet/transaction-history.md) or the +[`UtxoProcessor`](wallet-sdk/utxo-processor.md). + +The right "wait for confirmation" gate is the `Maturity` event for the +specific transaction you care about — not `Pending`, and not "wait N +seconds". + +## Mass and the fee market + +Every transaction has a *mass* — a number derived from the transaction's +byte size, its compute cost, and its storage cost (a function of input +and output values). Mass replaces the simple "size × byte rate" fee +model some other UTXO chains use. + +The required fee for a transaction is `mass × fee_rate`, where +`fee_rate` is set by the network's current congestion. You query the +prevailing rate via `client.get_fee_estimate()` (see +[RPC → Calls](rpc/calls.md)) or via the wallet's +`fee_rate_estimate()` (see +[Wallet → Send Transaction](wallet/send-transaction.md)). + +The [Transaction Generator](wallet-sdk/tx-generator.md) handles all of +this for you — it computes mass, picks a fee rate, and adds a change +output that absorbs the leftover. You only set fees explicitly when you +want a non-standard policy (priority surcharges, exact-balance sweeps, +multisig with custom signature counts). + +## Sompi and KAS + +The atomic unit is the **sompi**: 1 KAS = 100 000 000 sompi. Every +amount in the SDK — UTXO value, output amount, fee, balance — is in +sompi. Convert at the UI boundary with `sompi_to_kaspa(...)` / +`kaspa_to_sompi(...)`. Don't store KAS-as-float anywhere internal; use +ints in sompi. + +## Subnetworks + +Most transactions live on the default subnetwork (id all zeros). The +field exists for protocol extensions; you'll usually leave +`subnetwork_id="0000...0"` unchanged when building manually. + +## Where to next + +- [Networks](networks.md) — picking a chain to talk to. +- [Addresses](addresses.md) and [Transactions](transactions/index.md) — the + on-chain primitives in Python. +- [Wallet → Architecture](wallet/architecture.md) — how the SDK turns + these concepts into a wallet you can actually use. diff --git a/docs/learn/index.md b/docs/learn/index.md new file mode 100644 index 00000000..1e960507 --- /dev/null +++ b/docs/learn/index.md @@ -0,0 +1,23 @@ +# Learn + +- **[RPC](rpc/index.md)** — the `RpcClient`: resolver, connection, calls, + notifications. +- **[Wallet](wallet/index.md)** — the managed high-level `Wallet` API: lifecycle, file + storage, accounts, addresses, sending, history. +- **[Wallet SDK](wallet-sdk/index.md)** — the lower-level primitives that the + managed `Wallet` is built on: key management, transaction `Generator`, + derivation, `UtxoContext`, `UtxoProcessor`, etc. +- **[Networks](networks.md)** - working with the various Kaspa networks (mainnet, testnets, etc.) from this SDK. +- **[Addresses](addresses.md)** - a quick primer on Kaspa addresses and handling in this SDK. +- **[Transactions](transactions/index.md)** — the on-chain primitives: + inputs, outputs, mass and fees, signing, submission, metadata fields, + and serialization. +- **[Kaspa Concepts](concepts.md)** — explanation of the BlockDAG, UTXO + model, mass-based fees, and maturity. Read this if any of those terms feel + fuzzy. + +## Before you get started + +Read [Security](../getting-started/security.md). The Learn snippets use +literal mnemonics, hex strings, and short passwords for readability — **that +is not how production code should handle secret material.** diff --git a/docs/learn/networks.md b/docs/learn/networks.md new file mode 100644 index 00000000..6e146813 --- /dev/null +++ b/docs/learn/networks.md @@ -0,0 +1,71 @@ +# Networks + +Kaspa runs three live networks: a production mainnet and two testnets. +Every SDK call that hits the chain — `RpcClient`, `Wallet`, +`Address`, derivation — needs a network identifier so it knows which +chain it's targeting and which address prefix to encode. + +## The networks + +| Network | `network_id` | Address prefix | When to use | +| --- | --- | --- | --- | +| Mainnet | `"mainnet"` | `kaspa:` | Production. Real KAS. | +| Testnet 10 | `"testnet-10"` | `kaspatest:` | The mature testnet. Default for SDK examples; faucets exist. | +| Testnet 11 | `"testnet-11"` | `kaspatest:` | Higher block-rate testnet for performance work. | +| Devnet | (operator-defined) | `kaspadev:` | A developer-run private chain. | +| Simnet | (operator-defined) | `kaspasim:` | Simulation / unit tests against a local sim. | + +Mainnet and testnet-10 are what almost every reader of these docs will +touch. Reach for testnet-11 when you specifically need its higher block +rate; the SDK behaves identically on it. + +## `network_id` strings vs `NetworkId` + +Most APIs accept the string form (`"mainnet"`, `"testnet-10"`). The +`NetworkId` class is the typed form — useful when you want a value to +hold and pass around without re-parsing: + +```python +from kaspa import NetworkId + +mainnet = NetworkId("mainnet") +testnet = NetworkId("testnet-10") +``` + +`NetworkId.network_type` and `NetworkId.suffix` give you the parts back. +`NetworkType` (the enum) is what some APIs accept as a third form — +`NetworkType.Mainnet`, `NetworkType.Testnet`, etc. They all describe the +same thing; pick whichever the call site reads cleanly with. + +## What changes between networks + +- **Address prefix.** A key derived under `mainnet` produces + `kaspa:...`; the same key under `testnet-10` produces `kaspatest:...`. + See [Addresses](addresses.md). +- **Genesis and chain state.** Every network has its own UTXO set; + funds on one network don't exist on another. +- **Resolver pool.** A `Resolver` only returns nodes for the + `network_id` of the client it was given to. +- **Maturity depths.** Coinbase maturity differs by network; the SDK + applies the right value automatically. + +## Picking one for development + +- **Writing examples / docs / tests:** use `testnet-10`. It's stable, + there's a faucet, and addresses look obviously test-shaped + (`kaspatest:...`). +- **Performance experiments:** use `testnet-11`. Block rate is higher, + so UTXO churn and event volume look more like a stress test. +- **Production code paths under CI:** parametrise the network — keep + test runs on testnet, mainnet only on a release pipeline. +- **Anything that ever touches mainnet:** read + [Security](../getting-started/security.md) first. + +## Where to next + +- [Addresses](addresses.md) — what the prefix encodes and how versions + fit in. +- [Wallet → Initialize](wallet/initialize.md) — `network_id` is a + required constructor argument. +- [RPC → Resolver](rpc/resolver.md) — how a `Resolver` finds a node for + the configured network. diff --git a/docs/learn/rpc/calls.md b/docs/learn/rpc/calls.md new file mode 100644 index 00000000..5ca10c8b --- /dev/null +++ b/docs/learn/rpc/calls.md @@ -0,0 +1,135 @@ +# Calls + +Once connected, every RPC method is `await client.(...)`. Most take +either no arguments or a single dict; all return a dict (or list of dicts) +shaped like the rusty-kaspa wire protocol. + +This page is a tour of the surface, grouped by what you're trying to do. +For full request/response shapes use the [API Reference](../../reference/index.md). + +## Network information + +```python +info = await client.get_info() +dag_info = await client.get_block_dag_info() +count = await client.get_block_count() # blockCount, headerCount +supply = await client.get_coin_supply() # circulatingSompi, maxSompi +network = await client.get_current_network() +sync = await client.get_sync_status() # {"isSynced": bool} +``` + +`get_block_dag_info` is the closest the SDK has to a "where is the chain +right now" call: it returns network name, block count, virtual DAA score, +tip hashes, and pruning point in one go. + +## Balances and UTXOs + +```python +balance = await client.get_balance_by_address({"address": "kaspa:qz..."}) +balances = await client.get_balances_by_addresses({ + "addresses": ["kaspa:qz...", "kaspa:qr..."], +}) + +utxos = await client.get_utxos_by_addresses({"addresses": ["kaspa:qz..."]}) +for entry in utxos.get("entries", []): + print(entry["outpoint"], entry["utxoEntry"]["amount"]) +``` + +`get_utxos_by_addresses` is the go-to call when you need a one-shot UTXO +snapshot. For *continuous* UTXO tracking, subscribe instead — see +[Subscriptions](subscriptions.md). + +## Blocks + +```python +block = await client.get_block({"hash": "...", "includeTransactions": True}) +blocks = await client.get_blocks({ + "lowHash": "...", + "includeBlocks": True, + "includeTransactions": False, +}) +template = await client.get_block_template({ + "payAddress": "kaspa:...", + "extraData": [], +}) +``` + +## Transactions and mempool + +```python +result = await client.submit_transaction({ + "transaction": tx.serialize_to_dict(), + "allowOrphan": False, +}) +mempool = await client.get_mempool_entries({ + "includeOrphanPool": False, + "filterTransactionPool": True, +}) +entry = await client.get_mempool_entry({"transactionId": "..."}) +``` + +If you have a `PendingTransaction` from the [Transaction +Generator](../wallet-sdk/tx-generator.md), prefer `pending_tx.submit(client)` — +it serialises and submits in one call. + +## Fees + +```python +fee = await client.get_fee_estimate() +# fee["estimate"]["priorityBucket"] etc. + +fee_x = await client.get_fee_estimate_experimental({"verbose": True}) +``` + +## Peers + +```python +peers = await client.get_connected_peer_info() +addresses = await client.get_peer_addresses() + +await client.add_peer({"peerAddress": "192.168.1.1:16111", "isPermanent": False}) +await client.ban({"ip": "192.168.1.1"}) +await client.unban({"ip": "192.168.1.1"}) +``` + +These are administrative — you use them when you operate the node, not +when you're a client. + +## System + +```python +await client.ping() +server = await client.get_server_info() +system = await client.get_system_info() +metrics = await client.get_metrics({ + "processMetrics": True, + "connectionMetrics": True, + "bandwidthMetrics": True, + "consensusMetrics": True, + "storageMetrics": False, + "customMetrics": False, +}) +``` + +## Errors + +A failing RPC call raises. Handle it like any other coroutine exception: + +```python +try: + info = await client.get_balance_by_address({"address": addr}) +except Exception as exc: + print("balance lookup failed:", exc) +``` + +Connection-level failures retry automatically (see +[Connecting](connecting.md)); the exception surface is for protocol-level +failures (invalid address, malformed request, node-side errors). + +## Where to next + +- [Subscriptions](subscriptions.md) — server-pushed notifications. +- [Wallet → Send Transaction](../wallet/send-transaction.md) — the managed + Wallet wraps `submit_transaction` with sensible defaults. +- [Wallet SDK → Transaction Generator](../wallet-sdk/tx-generator.md) — + build the transactions you submit through `submit_transaction`. diff --git a/docs/learn/rpc/connecting.md b/docs/learn/rpc/connecting.md new file mode 100644 index 00000000..0a518cb6 --- /dev/null +++ b/docs/learn/rpc/connecting.md @@ -0,0 +1,91 @@ +# Connecting + +`RpcClient.connect()` opens the WebSocket. `disconnect()` closes it. Between +those two calls, the client is "connected" — every RPC method is callable +and notifications stream in. + +## A connected client, end to end + +```python +import asyncio +from kaspa import Resolver, RpcClient + +async def main(): + client = RpcClient(resolver=Resolver(), network_id="mainnet") + await client.connect() + try: + info = await client.get_block_dag_info() + print(info["networkName"], info["blockCount"]) + finally: + await client.disconnect() + +asyncio.run(main()) +``` + +The `try`/`finally` matters: a Python exception before `disconnect()` would +otherwise leave the socket open and the event loop holding a reference. + +## Connecting to a known node + +Skip the resolver when you have a URL: + +```python +client = RpcClient( + url="wss://node.example.com:17110", + network_id="mainnet", + encoding="borsh", # or "json" +) +``` + +Use this for nodes you operate, paid endpoints, or testnet runs against a +local `kaspad`. + +## Connection options + +`connect()` accepts a handful of behavioural overrides: + +```python +await client.connect( + block_async_connect=True, # await until the socket is open (default True) + strategy="fallback", # "retry" or "fallback" — what to do if the first attempt fails + timeout_duration=30000, # per-attempt timeout, ms + retry_interval=1000, # delay between attempts, ms +) +``` + +The defaults are appropriate for production code. Lower `timeout_duration` +in fast-failure CI environments; raise `retry_interval` if you want to be +gentler on resolver back-ends during outages. + +## Inspecting the live client + +```python +print(client.is_connected) # bool +print(client.url) # the resolved or supplied node URL +print(client.encoding) # "borsh" or "json" +print(client.node_id) # node-reported identifier +print(client.resolver) # the Resolver instance, or None +``` + +These are all property reads — no I/O, no `await`. + +## Encoding: Borsh vs JSON + +`encoding="borsh"` is the default and the right choice. Borsh is the binary +format the node speaks natively; payloads are smaller and parsing is +faster. Pick `"json"` only when you need to inspect raw frames in a tool +that doesn't speak Borsh, or when the node you're targeting doesn't support +Borsh. + +## Reconnects + +If the WebSocket drops mid-session, the client reconnects on its own. Calls +made *during* the gap raise; calls made *after* the reconnect succeed. +There is no opt-out — if you need to know about disruptions, listen for the +relevant connection notifications (see [Subscriptions](subscriptions.md)). + +## Where to next + +- [Calls](calls.md) — what to do once `is_connected` is `True`. +- [Subscriptions](subscriptions.md) — real-time notifications, including + connection-state events. diff --git a/docs/learn/rpc/index.md b/docs/learn/rpc/index.md new file mode 100644 index 00000000..e7ba86d1 --- /dev/null +++ b/docs/learn/rpc/index.md @@ -0,0 +1,43 @@ +# RPC + +The `RpcClient` provides a connection to a Kaspa node, allowing you to interact via Kaspa's RPC calls. This includes query methods, submission methods (e.g. submitting a transaction), and event subscriptions. + +Other layers in the SDK depend on RPC Client. For example, the `Wallet` class submits transactions and monitors UTXO state via RPC. + +## Overview + +A persistent, asynchronous WebSocket client. Each instance: + +- Connects to one node at a time. +- Is async-first - every method is a coroutine. +- Handles connection state - when a connection drops the client attempts to reconnect automatically. +- Encodes in Borsh by default - Borsh is more compact and faster to parse than JSON. Pass `encoding="json"` if you need the JSON wire format. + +## Two ways to point a client at a node + +```python +# Resolver: let the SDK pick a public node for the network you want +client = RpcClient(resolver=Resolver(), network_id="mainnet") + +# Direct URL: a known node you control or trust +client = RpcClient(url="wss://node.example.com:17110", network_id="mainnet") +``` + +See [Resolver](resolver.md) for the discovery mechanism, and +[Connecting](connecting.md) for the connection lifecycle and options. + +## What you can do once connected + +- **[Calls](calls.md)** — request/response RPCs for network info, balances, + blocks, mempool, fee estimation, peer management, etc. +- **[Subscriptions](subscriptions.md)** — node-pushed notifications for + UTXO changes, new blocks, virtual chain updates, and DAA score + changes. Each subscription is paired with an event listener you + register on the client. + +## Where to next + +- [Connecting](connecting.md) — connect, disconnect, retries, encoding. +- [Resolver](resolver.md) — how node discovery works. +- [Calls](calls.md) — the RPC method catalog. +- [Subscriptions](subscriptions.md) — real-time notifications. diff --git a/docs/learn/rpc/resolver.md b/docs/learn/rpc/resolver.md new file mode 100644 index 00000000..5c8d59da --- /dev/null +++ b/docs/learn/rpc/resolver.md @@ -0,0 +1,53 @@ +# Resolver + +A `Resolver` finds a Kaspa node for a given [network](network.md). An `RpcClient` can be configured to use a Resolver instance instead of a hard-coded URL. On `connect()` the resolver (attempts) to connect to an available node on the public node-network (PNN). + +## When to use it + +- **Building an application that talks to mainnet or testnet** without + shipping a node alongside. +- **Quick scripts and notebooks** where "just give me a node" is the right + default. +- **Failover.** The resolver picks a different node if its first choice is + unreachable. + +If you need a deterministic URL — for example a load-balanced internal +node, or a node with an authenticated endpoint — point `RpcClient` at it +directly and don't construct a `Resolver` at all. + +## The basic shape + +```python +from kaspa import Resolver, RpcClient + +resolver = Resolver() +client = RpcClient(resolver=resolver, network_id="mainnet") +await client.connect() +``` + +The `network_id` argument is what the resolver uses to filter candidate +nodes — `"mainnet"`, `"testnet-10"`, or `"testnet-11"`. The same `Resolver` +instance can be reused across networks; the network is a property of the +*client*, not the resolver. + +## Configuring the resolver + +```python +# Default: uses the public PNN endpoints baked into the SDK +resolver = Resolver() + +# Override with explicit resolver URLs (advanced) +resolver = Resolver(urls=["https://resolver1.example.org"]) + +# Force TLS on resolver requests +resolver = Resolver(tls=True) +``` + +The default constructor is the right choice for almost everyone. Override +the URLs only if you're operating a private resolver fleet. + +## Where to next + +- [Connecting](connecting.md) — direct URLs, retry/timeout options, the + encoding choice. +- [Calls](calls.md) — what to do once you're connected. diff --git a/docs/learn/rpc/subscriptions.md b/docs/learn/rpc/subscriptions.md new file mode 100644 index 00000000..35ff8fb5 --- /dev/null +++ b/docs/learn/rpc/subscriptions.md @@ -0,0 +1,112 @@ +# Subscriptions + +Subscriptions turn the `RpcClient` from a request/response API into a live +feed. The node pushes events; the client invokes callbacks you registered. +You typically use them to react to UTXO changes for an address you care +about, or to track block / virtual-chain progression for an indexer. + +## The two-step pattern + +Every subscription has two parts: + +1. **A listener** — a Python callback registered with + `client.add_event_listener("", callback)`. +2. **A subscription** — `await client.subscribe_(...)` that tells the + node to start streaming. + +Both halves are required. A listener with no subscription receives nothing; +a subscription with no listener silently drops events. + +```python +def on_utxo_change(event): + print("UTXO change:", event) + +client.add_event_listener("utxos-changed", on_utxo_change) +await client.subscribe_utxos_changed([Address("kaspa:qz...")]) +``` + +## Available events + +| Event name | Subscribe with | +| --- | --- | +| `utxos-changed` | `subscribe_utxos_changed(addresses)` | +| `block-added` | `subscribe_block_added()` | +| `virtual-chain-changed` | `subscribe_virtual_chain_changed(include_accepted_transaction_ids=...)` | +| `virtual-daa-score-changed` | `subscribe_virtual_daa_score_changed()` | +| `sink-blue-score-changed` | `subscribe_sink_blue_score_changed()` | +| `finality-conflict` | `subscribe_finality_conflict()` | +| `finality-conflict-resolved` | `subscribe_finality_conflict_resolved()` | +| `new-block-template` | `subscribe_new_block_template()` | +| `pruning-point-utxo-set-override` | `subscribe_pruning_point_utxo_set_override()` | + +Each `subscribe_*` has a matching `unsubscribe_*` that takes the same +argument shape. + +## Watching addresses for UTXO changes + +```python +from kaspa import Address + +addresses = [Address("kaspa:qz...")] + +def on_change(event): + for added in event.get("added", []): + print("+", added["utxoEntry"]["amount"]) + for removed in event.get("removed", []): + print("-", removed["utxoEntry"]["amount"]) + +client.add_event_listener("utxos-changed", on_change) +await client.subscribe_utxos_changed(addresses) + +# ... later ... +await client.unsubscribe_utxos_changed(addresses) +``` + +For watching a managed-wallet account rather than raw addresses, use the +wallet's `Balance` and `Maturity` events instead — see +[Wallet → Transaction History](../wallet/transaction-history.md). + +## Block events + +```python +def on_block(event): + print("new block:", event["block"]["header"]["hash"]) + +client.add_event_listener("block-added", on_block) +await client.subscribe_block_added() +``` + +## Virtual chain progression + +```python +def on_chain(event): + print("chain update:", event) + +client.add_event_listener("virtual-chain-changed", on_chain) +await client.subscribe_virtual_chain_changed(include_accepted_transaction_ids=True) +``` + +`include_accepted_transaction_ids=True` makes the event payload usable as +a confirmation feed — every accepted transaction id appears in the stream. + +## Listener bookkeeping + +```python +client.add_event_listener("block-added", callback) # add +client.add_event_listener("block-added", callback, extra) # forward extra arg + +client.remove_event_listener("block-added", callback) # remove specific +client.remove_event_listener("block-added") # remove all for event +client.remove_all_event_listeners() # remove all globally +``` + +Listeners outlive a single subscription cycle — re-subscribing after an +unsubscribe does *not* re-fire previously delivered events. If you need to +catch up, do a one-shot `get_utxos_by_addresses` (or equivalent) before +re-subscribing. + +## Where to next + +- [Calls](calls.md) — the request/response side of the API. +- [Wallet → Transaction History](../wallet/transaction-history.md) — the + managed Wallet's higher-level event surface. diff --git a/docs/learn/transactions/index.md b/docs/learn/transactions/index.md new file mode 100644 index 00000000..5179b1f0 --- /dev/null +++ b/docs/learn/transactions/index.md @@ -0,0 +1,122 @@ +# Transactions + +A transaction in Kaspa is the same shape as in any UTXO chain: a list of +inputs (each spending a previous output), a list of outputs, and a few +metadata fields. The SDK exposes the underlying types — `Transaction`, +`TransactionInput`, `TransactionOutput`, `TransactionOutpoint`, +`UtxoEntryReference` — and the helpers that build, sign, mass, and +serialise them. + +Most of the time you'll use the higher-level +[Transaction Generator](../wallet-sdk/tx-generator.md) (or the managed +[Wallet](../wallet/send-transaction.md) on top of it). The pages in this +section cover the primitives underneath, so you can build manually when +you need to — custom lockup scripts, exact input ordering, payload data, +offline signing. + +## Anatomy + +``` +Transaction + version, lock_time, subnetwork_id, gas, payload, mass + inputs: [TransactionInput, ...] + outputs: [TransactionOutput, ...] + +TransactionInput + previous_outpoint: TransactionOutpoint(transaction_id, index) + signature_script (filled at sign time) + sequence + sig_op_count + utxo: UtxoEntryReference + +TransactionOutput + value (sompi) + script_public_key (lockup script) +``` + +A few things that distinguish Kaspa from a Bitcoin-shaped chain: + +- **Inputs carry their own UTXO context** via `UtxoEntryReference`, so + the signer doesn't have to re-fetch the spent output to know its amount + and lockup. See [Inputs](inputs.md). +- **Mass replaces "byte size × rate"** as the fee model. You compute mass + on the transaction (including a storage component derived from input + and output values), then multiply by the prevailing fee rate. See + [Mass & fees](mass-and-fees.md). +- **The atomic unit is the sompi**: `1 KAS = 100_000_000 sompi`. Every + amount in the transaction surface is a sompi int — convert at the UI + boundary only. + +## End-to-end (manual path) + +```python +from kaspa import ( + Transaction, TransactionInput, TransactionOutput, TransactionOutpoint, + UtxoEntryReference, sign_transaction, pay_to_address_script, + update_transaction_mass, +) + +inputs = [ + TransactionInput( + previous_outpoint=TransactionOutpoint( + transaction_id=u["outpoint"]["transactionId"], + index=u["outpoint"]["index"], + ), + signature_script="", # filled at sign time + sequence=0, + sig_op_count=1, + utxo=UtxoEntryReference(u), + ) + for u in my_utxos +] + +outputs = [ + TransactionOutput(value=amount, script_public_key=pay_to_address_script(recipient)), + TransactionOutput(value=change_amount, script_public_key=pay_to_address_script(change_addr)), +] + +tx = Transaction( + version=0, inputs=inputs, outputs=outputs, + lock_time=0, + subnetwork_id="0000000000000000000000000000000000000000", + gas=0, payload="", mass=0, +) + +update_transaction_mass("mainnet", tx) +signed = sign_transaction(tx, [private_key], verify_sig=True) + +await client.submit_transaction({ + "transaction": signed.serialize_to_dict(), + "allowOrphan": False, +}) +``` + +This is what the [Generator](../wallet-sdk/tx-generator.md) does +internally — it picks UTXOs, computes mass, signs, and yields one or more +ready-to-submit `PendingTransaction`s. Reach for the manual path when you +need control the Generator doesn't expose. + +## In this section + +- **[Inputs](inputs.md)** — `TransactionInput`, `TransactionOutpoint`, + `UtxoEntryReference`, and why inputs carry their UTXO context. +- **[Outputs](outputs.md)** — `TransactionOutput`, `ScriptPublicKey`, the + lockup scripts that pay to an address. +- **[Mass & fees](mass-and-fees.md)** — computing mass, storage mass, + the fee market, and when to call `update_transaction_mass`. +- **[Signing](signing.md)** — `sign_transaction`, `SighashType`, + per-input signing, multi-key flows. +- **[Submission](submission.md)** — `submit_transaction`, what + "submitted" means, and how confirmation works. +- **[Metadata fields](metadata.md)** — `version`, `lock_time`, + `subnetwork_id`, `gas`, `payload`, the fields you mostly leave alone. +- **[Serialization](serialization.md)** — `to_dict()` / `from_dict()` + for round-tripping through other systems. + +## Where to next + +- [Wallet SDK → Transaction Generator](../wallet-sdk/tx-generator.md) — + the high-level coin selector + signer. +- [Wallet → Send Transaction](../wallet/send-transaction.md) — the + managed Wallet's send surface. +- [Kaspa Concepts](../concepts.md) — UTXO model, mass, fees, maturity. diff --git a/docs/learn/transactions/inputs.md b/docs/learn/transactions/inputs.md new file mode 100644 index 00000000..fe968bf4 --- /dev/null +++ b/docs/learn/transactions/inputs.md @@ -0,0 +1,105 @@ +# Inputs + +A transaction's inputs say *which UTXOs are being spent*. Each input +points at a previous output by `(transaction_id, index)` and carries the +script that proves the spender is allowed to claim it. + +## Types involved + +``` +TransactionInput + previous_outpoint: TransactionOutpoint(transaction_id, index) + signature_script (filled at sign time) + sequence + sig_op_count + utxo: UtxoEntryReference # optional, but you almost always want it set +``` + +- **`TransactionOutpoint`** — `(transaction_id, index)`. The pointer to + the output you're spending. +- **`UtxoEntryReference`** — the cached copy of the *spent output*: its + amount, its lockup script, the block DAA score it landed in, and + whether it's a coinbase. See [UTXO Context](../wallet-sdk/utxo-context.md) + for how the SDK tracks these. +- **`signature_script`** — the unlocking script. Empty string at build + time; filled when you sign. See [Signing](signing.md). +- **`sequence`** — sequence number; leave at `0` unless you have a + specific protocol-level reason. +- **`sig_op_count`** — how many signature operations this input + performs (`1` for a normal Schnorr or ECDSA spend, `>1` for multisig). + This feeds into mass calculation. + +## Build an input + +From a UTXO dict returned by `client.get_utxos_by_addresses(...)`: + +```python +from kaspa import TransactionInput, TransactionOutpoint, UtxoEntryReference + +utxo = utxos["entries"][0] + +inp = TransactionInput( + previous_outpoint=TransactionOutpoint( + transaction_id=utxo["outpoint"]["transactionId"], + index=utxo["outpoint"]["index"], + ), + signature_script="", # filled at sign time + sequence=0, + sig_op_count=1, + utxo=UtxoEntryReference(utxo), +) +``` + +## Why inputs carry a UtxoEntryReference + +Kaspa signs over the spent output's amount and lockup, not just the +outpoint. The SDK can't sign an input correctly without that context, so +`TransactionInput.utxo` exists to *attach* it directly — no node +round-trip needed. + +A few practical consequences: + +- If you build inputs by hand and forget the `utxo=...` arg, signing + will fail. Always set it. +- A signed transaction can be moved between processes (offline signer, + co-signer, relay) without the receiving side needing access to the + source node, because every input carries what's needed. +- The Generator does this for you — you hand it a list of + `UtxoEntryReference`s (or a [`UtxoContext`](../wallet-sdk/utxo-context.md)) + and it picks and wraps inputs internally. + +## UTXO selection + +Selecting inputs that sum to at least `amount + fee` is what the +[Transaction Generator](../wallet-sdk/tx-generator.md) handles. When +building manually: + +- Sum the input values you intend to spend. +- Subtract output amounts and fee — the leftover becomes the change + output. +- Order matters only insofar as your downstream consumers care; protocol + rules don't impose an order. + +For input ordering rules, signature aggregation, or "spend exactly these +UTXOs first" semantics, see the Generator's `priority_entries` option. + +## Reading inputs back + +```python +for inp in tx.inputs: + print(inp.previous_outpoint.transaction_id, inp.previous_outpoint.index) + print(inp.signature_script_as_hex) # hex string, or None pre-sign + print(inp.sig_op_count, inp.sequence) + if inp.utxo: + print(inp.utxo.amount, inp.utxo.script_public_key) +``` + +`signature_script_as_hex` returns the unlocking script after signing as +a hex string (or `None` if the input hasn't been signed yet). + +## Where to next + +- [Outputs](outputs.md) — the other half of a transaction. +- [Signing](signing.md) — what "filled at sign time" actually does. +- [UTXO Context](../wallet-sdk/utxo-context.md) — managed UTXO state + the SDK keeps in sync with the chain. diff --git a/docs/learn/transactions/mass-and-fees.md b/docs/learn/transactions/mass-and-fees.md new file mode 100644 index 00000000..7b7dba4e --- /dev/null +++ b/docs/learn/transactions/mass-and-fees.md @@ -0,0 +1,130 @@ +# Mass & fees + +Kaspa uses a **mass-based fee model**. Every transaction has a *mass* — +a number derived from its byte size, its compute cost, and a storage +component that depends on input and output values. The required fee is +`mass × fee_rate`, where `fee_rate` is the prevailing rate the network +sets based on congestion. + +For the protocol-level view of why mass exists, see +[Kaspa Concepts](../concepts.md). This page covers the SDK helpers. + +## The two kinds of mass + +- **Compute / size mass** — derived from the transaction's serialized + size and signature operations. +- **Storage mass** — derived from input and output *values*, specifically + to discourage outputs that bloat the UTXO set (many tiny outputs from + one large input). + +The total mass is the larger of the two. You don't compute the parts +separately for normal use — `calculate_transaction_mass` and +`update_transaction_mass` handle it. `calculate_storage_mass` is exposed +for when you want to inspect the storage component on its own. + +## Compute mass for a transaction + +```python +from kaspa import ( + calculate_transaction_mass, + update_transaction_mass, + maximum_standard_transaction_mass, +) + +mass = calculate_transaction_mass("mainnet", tx) +print(mass) +print(maximum_standard_transaction_mass()) # protocol upper bound +``` + +`calculate_transaction_mass(network_id, tx)` returns the mass without +mutating the transaction. To write it onto the transaction itself +(required before signing or serializing), use: + +```python +update_transaction_mass("mainnet", tx) +print(tx.mass) +``` + +**Order matters.** Run `update_transaction_mass` after your inputs and +outputs are settled but *before* signing or serializing. The mass field +is part of the signed payload — sign first and you'll be signing over +mass=0. + +For multisig estimation, both calls take an optional +`minimum_signatures` to size the signature script correctly: + +```python +update_transaction_mass("mainnet", tx, minimum_signatures=2) +``` + +## Storage mass on its own + +```python +from kaspa import calculate_storage_mass + +storage_mass = calculate_storage_mass( + network_id="mainnet", + input_values=[1_000_000, 2_000_000], + output_values=[2_500_000, 400_000], +) +``` + +Useful for sizing change outputs: if your "tiny change output" pushes +storage mass through the roof, fold it into the fee instead. + +## Fees + +```python +from kaspa import calculate_transaction_fee + +fee = calculate_transaction_fee("mainnet", tx) +print(fee) # required fee in sompi +``` + +`calculate_transaction_fee` returns the minimum required fee for the +transaction at the network's current rate. The result is a sompi int (or +`None` if the calculation can't be performed — typically because the +transaction is malformed). + +## Querying the fee rate + +The network exposes a fee estimator over RPC: + +```python +estimate = await client.get_fee_estimate({}) +print(estimate["estimate"]["priorityBucket"]["feerate"]) +print(estimate["estimate"]["normalBuckets"]) +print(estimate["estimate"]["lowBuckets"]) +``` + +Each bucket carries a `feerate` (sompi-per-gram-of-mass) and an +`estimatedSeconds` for "how long until this rate gets you confirmed". +Pick a bucket based on how much you care about latency, multiply by +mass, and you have a fee. + +The Wallet wraps this as `fee_rate_estimate()` (see +[Send Transaction](../wallet/send-transaction.md)) and the Generator +picks a sensible default if you don't pass `fee_rate=` explicitly. + +## When to set fees explicitly + +The [Generator](../wallet-sdk/tx-generator.md) picks a fee rate, computes +mass, and folds the leftover into a change output. You only need to +override: + +- **`fee_rate=`** — when you have a specific sompi-per-gram you want to + pay. +- **`priority_fee=`** — when you want to add a flat surcharge on top of + the computed fee. +- **Manual path** — when you're building the transaction yourself and + need to size change outputs around the fee. + +For typical sends, the defaults are fine. + +## Where to next + +- [Signing](signing.md) — runs after mass, before submission. +- [Submission](submission.md) — `submit_transaction` and what counts as + confirmed. +- [Kaspa Concepts → Mass and the fee market](../concepts.md) — protocol + background on why mass is shaped this way. diff --git a/docs/learn/transactions/metadata.md b/docs/learn/transactions/metadata.md new file mode 100644 index 00000000..681470bc --- /dev/null +++ b/docs/learn/transactions/metadata.md @@ -0,0 +1,104 @@ +# Metadata fields + +Beyond inputs and outputs, a `Transaction` carries five fields that +affect how it's interpreted on-chain. For typical sends they all take +defaults — this page documents what they are so you know what to leave +alone and what to set when you're doing something specific. + +```python +Transaction( + version=0, + inputs=..., + outputs=..., + lock_time=0, + subnetwork_id="0000000000000000000000000000000000000000", + gas=0, + payload="", + mass=0, +) +``` + +## `version` + +Transaction format version. Use `0` — the only currently-defined version +on Kaspa. The field exists so the protocol can introduce future +formats; until then there's nothing to choose. + +## `lock_time` + +The earliest moment a transaction is allowed into a block. Encoded as a +DAA-score threshold. `0` means "no lock" and is what you want unless +you're building a time-locked construct (e.g. a refund branch in a +contract). + +```python +Transaction(..., lock_time=0, ...) +``` + +If you set this, the transaction is rejected from blocks whose DAA score +is below the threshold. See [Kaspa Concepts → Virtual chain and DAA +score](../concepts.md) for what DAA score is. + +## `subnetwork_id` + +The subnetwork the transaction belongs to. Most transactions live on +the default subnetwork — id all zeros — and that's what you should pass +when building manually: + +```python +subnetwork_id="0000000000000000000000000000000000000000" +``` + +The field exists for protocol extensions; non-default subnetwork IDs are +reserved for specific protocol-level transaction kinds (coinbase, etc.) +that you generally don't construct from the SDK. + +## `gas` + +Reserved for subnetwork transactions that have a compute-cost component. +On the default subnetwork it must be `0`. Pair it with +`subnetwork_id="00...0"` and forget about it. + +## `payload` + +Arbitrary bytes attached to the transaction. The closest analog in +Bitcoin terms is `OP_RETURN`-style data, but `payload` lives at the +transaction level rather than inside a script. + +```python +Transaction(..., payload="68656c6c6f", ...) # hex string +Transaction(..., payload=b"hello", ...) # or raw bytes +``` + +Use cases: + +- **Application-level metadata** that needs to ride alongside a payment + (an invoice ID, a memo, a reference number). +- **Protocol-level data** for systems that build on top of Kaspa + transactions. + +What it's not for: a substitute for cryptographic state. Payload bytes +get hashed into the transaction ID and signed over, but they don't bind +the transaction to anything off-chain on their own. + +The Generator accepts `payload=` directly: + +```python +Generator(..., payload=b"invoice-12345") +``` + +## `mass` + +The transaction's mass. Set to `0` at construction; populate it with +`update_transaction_mass(network_id, tx)` after inputs and outputs are +finalized, before signing or serializing. See +[Mass & fees](mass-and-fees.md). + +## Where to next + +- [Mass & fees](mass-and-fees.md) — the one metadata field you *do* + have to update. +- [Serialization](serialization.md) — how these fields ride through + `to_dict()` / `from_dict()`. +- [Kaspa Concepts](../concepts.md) — subnetworks, DAA score, virtual + chain. diff --git a/docs/learn/transactions/outputs.md b/docs/learn/transactions/outputs.md new file mode 100644 index 00000000..a7805dda --- /dev/null +++ b/docs/learn/transactions/outputs.md @@ -0,0 +1,121 @@ +# Outputs + +A transaction's outputs are the new UTXOs it creates. Each one carries a +value (in sompi) and a *locking script* — the conditions a future +spender will have to satisfy. + +## Types involved + +``` +TransactionOutput + value (sompi) + script_public_key: ScriptPublicKey + +ScriptPublicKey + version (int) + script (hex bytes — the lockup conditions) +``` + +`TransactionOutput` pairs the amount with the script that locks it. +`ScriptPublicKey` is the script itself: a version byte plus the encoded +program (the bytes that whoever spends this output later will have to +satisfy). + +## Build an output + +Pay-to-address is the common case. Build the lockup script with +`pay_to_address_script`: + +```python +from kaspa import Address, TransactionOutput, pay_to_address_script + +recipient = Address("kaspa:qz...") +out = TransactionOutput( + value=500_000_000, # 5 KAS in sompi + script_public_key=pay_to_address_script(recipient), +) +``` + +For the inverse — recovering the address that an output pays to — use +`address_from_script_public_key`: + +```python +from kaspa import NetworkType, address_from_script_public_key + +addr = address_from_script_public_key(out.script_public_key, NetworkType.Mainnet) +``` + +That second call needs a network argument because the script itself +doesn't carry the prefix; you have to tell the decoder which network +you're displaying for. + +## Pay-to-script-hash + +For multisig and other custom scripts, lockups go through a script hash. +`pay_to_script_hash_script(redeem_script)` produces the locking side; +the spender later supplies the redeem script plus signatures via +`pay_to_script_hash_signature_script(...)` at sign time. + +```python +from kaspa import pay_to_script_hash_script + +spk = pay_to_script_hash_script(redeem_script_bytes) +out = TransactionOutput(value=amount, script_public_key=spk) +``` + +For the multisig flow (creating the address, signing with multiple +cosigners, submitting), see the +[Multi-signature transactions](../../guides/multisig.md) recipe. + +## Change outputs + +When your selected inputs sum to more than `amount + fee`, the leftover +goes to a change output you control: + +```python +outputs = [ + TransactionOutput(value=amount, script_public_key=pay_to_address_script(recipient)), + TransactionOutput(value=change_amount, script_public_key=pay_to_address_script(change_addr)), +] +``` + +The [Generator](../wallet-sdk/tx-generator.md) computes `change_amount` +for you (selected total − outputs − fee) and writes the change output +last. When building manually, you do the arithmetic, including +re-checking it after `update_transaction_mass` if the fee shifted. + +If `change_amount` is too small to be worth a separate output, fold it +into the fee instead — paying a slightly inflated fee beats producing +dust. + +## Sompi vs KAS + +Every value in the output surface (and everywhere else in the +transaction API) is a **sompi int**. `1 KAS = 100_000_000 sompi`. + +```python +from kaspa import kaspa_to_sompi, sompi_to_kaspa + +kaspa_to_sompi(1.5) # 150_000_000 +sompi_to_kaspa(150_000_000) # 1.5 +``` + +Convert at the UI boundary only. Don't store KAS as a float anywhere +internal; everything in the SDK assumes integer sompi. + +## Reading outputs back + +```python +for out in tx.outputs: + print(out.value) # sompi + print(out.script_public_key.version) + print(out.script_public_key.script) # hex +``` + +## Where to next + +- [Addresses](../addresses.md) — what a `pay_to_address_script` is + actually pointing at. +- [Inputs](inputs.md) — the other half of a transaction. +- [Mass & fees](mass-and-fees.md) — output values feed into the + storage-mass component of the fee. diff --git a/docs/learn/transactions/serialization.md b/docs/learn/transactions/serialization.md new file mode 100644 index 00000000..c1f7fd1f --- /dev/null +++ b/docs/learn/transactions/serialization.md @@ -0,0 +1,64 @@ +# Serialization + +Most transaction-shaped types — `Transaction`, `TransactionInput`, +`TransactionOutput`, `TransactionOutpoint`, `UtxoEntryReference` — +support `to_dict()` and `from_dict()`. The dict shape matches the +wRPC wire format the node accepts and produces. + +## Round-tripping + +```python +tx_dict = tx.to_dict() +restored = Transaction.from_dict(tx_dict) +assert restored == tx +``` + +The same shape works for the component types: + +```python +inp_dict = inputs[0].to_dict() +restored_inp = TransactionInput.from_dict(inp_dict) + +out_dict = outputs[0].to_dict() +restored_out = TransactionOutput.from_dict(out_dict) + +ref_dict = utxo_ref.to_dict() +restored_ref = UtxoEntryReference.from_dict(ref_dict) +``` + +`to_dict()` produces a fresh Python dict — modifying it doesn't mutate +the source object. `from_dict()` raises on malformed input (missing +required keys, wrong types, invalid values). + +## When you need this + +Within a single process, you rarely need to round-trip — pass the +typed objects around. The dict form earns its place at process +boundaries: + +- **Submission** — `client.submit_transaction({"transaction": + tx.serialize_to_dict(), ...})` takes a dict, not a `Transaction`. See + [Submission](submission.md). +- **Offline signing** — build on an online machine, serialize, sign on + an air-gapped one, serialize again, send back, submit. The dict form + is the natural transport. +- **Co-signer flows** — multisig where each cosigner signs in turn. + Each step ships a dict; the next signer reads it back, signs, and + forwards. +- **Persistence** — saving a pending transaction to disk or a queue for + later submission. Store the dict (as JSON), not the typed object. + +## `serialize_to_dict` vs `to_dict` + +Both produce a dict matching the wRPC wire shape. `to_dict` is the +general-purpose Python conversion; `serialize_to_dict` (on +`Transaction`) is the form `submit_transaction` expects. In practice +they produce equivalent shapes — use `serialize_to_dict` when you're +about to submit, `to_dict` when you're shuttling the object somewhere +else. + +## Where to next + +- [Submission](submission.md) — where the dict form actually goes. +- [Inputs](inputs.md) and [Outputs](outputs.md) — the typed objects + these dicts represent. diff --git a/docs/learn/transactions/signing.md b/docs/learn/transactions/signing.md new file mode 100644 index 00000000..ee321398 --- /dev/null +++ b/docs/learn/transactions/signing.md @@ -0,0 +1,128 @@ +# Signing + +Signing fills in each input's `signature_script` with proof that the +spender controls the key the corresponding UTXO is locked to. Kaspa +defaults to **Schnorr** signatures over the secp256k1 curve; ECDSA is +also supported for inputs locked to ECDSA addresses. Multisig inputs +combine multiple signatures under a script-hash lockup. + +For Schnorr-vs-ECDSA on the addressing side, see +[Addresses → Versions](../addresses.md). + +## Sign a manually built transaction + +```python +from kaspa import sign_transaction, update_transaction_mass + +update_transaction_mass("mainnet", tx) # do this first — mass is signed over +signed = sign_transaction(tx, [private_key], verify_sig=True) +``` + +`sign_transaction(tx, signers, verify_sig)`: + +- `signers` — a list of `PrivateKey`. The signer for each input is + inferred from the input's UTXO lockup; pass every key that any input + needs. +- `verify_sig=True` — verifies each signature after writing it. Cheap + insurance during development; you can disable it in performance- + sensitive paths once you trust the inputs. + +Sign before submission, after mass is filled in. Mass is part of the +signed payload, so changing inputs, outputs, or mass *after* signing +invalidates the signature. + +## Sign a generator-produced PendingTransaction + +```python +for pending in gen: + pending.sign([key]) # all inputs at once + await pending.submit(client) +``` + +For multisig, hand in every cosigner's key: + +```python +pending.sign([key1, key2, key3]) +``` + +For per-input control: + +```python +for i, _ in enumerate(pending.get_utxo_entries()): + pending.sign_input(i, key_for(i)) +``` + +`sign_input` is the right surface when different inputs need different +signers (mixed-key wallets, partially-co-signed flows). + +## SighashType + +The hash that gets signed describes *which parts of the transaction* the +signature commits to. `SighashType.All` is the default and the only one +most code should use. + +```python +from kaspa import SighashType + +print(list(SighashType)) +# All, _None, Single, AllAnyOneCanPay, NoneAnyOneCanPay, SingleAnyOneCanPay +``` + +- **`All`** — signs every input and every output. Standard. +- **`_None`** — signs inputs only; outputs can be modified. Rare; + underscore-prefixed because `None` is a Python keyword. +- **`Single`** — signs the input being spent and the matching output by + index. +- **`*AnyOneCanPay`** — variants that *don't* sign the other inputs, + letting cosigners add inputs after the fact. + +In practice, leave it at `All` unless you have a specific protocol or +co-signing reason. The non-`All` modes are for advanced flows like +collaborative coin joins. + +## Build a signature without filling the input + +When you need the raw signature bytes — for example, to send to a +co-signer for aggregation — use `create_input_signature`: + +```python +from kaspa import SighashType, create_input_signature + +sig_hex = create_input_signature( + tx, + input_index=0, + private_key=key, + sighash_type=SighashType.All, +) +``` + +The same method exists on `PendingTransaction` (`pending.create_input_signature(...)`) +and you can write the resulting script back with `pending.fill_input(...)`. + +## Multisig and sig_op_count + +Two fields interact with mass when you sign: + +- **`sig_op_count`** on each input — how many signature ops that input + actually performs. `1` for a single-key spend, `M` for an `M`-of-`N` + multisig. +- **`minimum_signatures`** passed to `update_transaction_mass(..., + minimum_signatures=M)` and `calculate_transaction_mass` — tells the + mass calculator how big the signature script will be when it's filled + in. + +If either is wrong, mass will be wrong, and the resulting fee will be +either rejected (too low) or wasted (too high). The Generator handles +this when you pass `sig_op_count` and `minimum_signatures` to the +constructor. + +For the full multisig flow (creating the address, signing with multiple +cosigners, submitting), see the +[Multi-signature transactions](../../guides/multisig.md) recipe. + +## Where to next + +- [Submission](submission.md) — what to do with a signed transaction. +- [Mass & fees](mass-and-fees.md) — fill mass before signing, not after. +- [Multi-signature transactions](../../guides/multisig.md) — cosigner + flow end to end. diff --git a/docs/learn/transactions/submission.md b/docs/learn/transactions/submission.md new file mode 100644 index 00000000..e6da56e2 --- /dev/null +++ b/docs/learn/transactions/submission.md @@ -0,0 +1,98 @@ +# Submission & confirmation + +Submitting a signed transaction hands it to a node, which gossips it to +the network and includes it in a block when capacity allows. The SDK +gives you two surfaces: `pending.submit(client)` for transactions +produced by the [Generator](../wallet-sdk/tx-generator.md), and +`client.submit_transaction(...)` for everything else. + +## From a PendingTransaction + +```python +tx_id = await pending.submit(client) +print(tx_id) +``` + +`pending.submit` serializes the underlying `Transaction` and calls +`submit_transaction` for you. This is the right path for transactions +the Generator built — including the managed +[Wallet](../wallet/send-transaction.md), which is built on the +Generator. + +## Manual submission + +```python +result = await client.submit_transaction({ + "transaction": signed_tx.serialize_to_dict(), + "allowOrphan": False, +}) +print(result["transactionId"]) +``` + +The request takes: + +- **`transaction`** — the wire-format dict you get from + `Transaction.serialize_to_dict()` (or via the equivalent + `pending.transaction.serialize_to_dict()`). +- **`allowOrphan`** — whether to keep the transaction in mempool when + one of its inputs hasn't been seen yet (e.g. you submitted a chain of + transactions out of order). Default to `False` unless you know you're + submitting a chain. + +The manual path is the right call when you need to ship the transaction +through another system — a co-signer, a relay, an offline signer — +before the node sees it. For round-tripping through that other system, +see [Serialization](serialization.md). + +## What "submitted" means + +Submission is *acceptance into the mempool*, not confirmation. The +return value is the transaction ID; the transaction is now eligible to +be included in a block. + +A Kaspa transaction lifecycle has three observable states: + +- **In mempool** — accepted by the node, waiting for inclusion. The + transaction ID is returned from `submit_transaction`. +- **Virtual-chain accepted** — included in a block that's part of the + canonical DAG ordering. Surfaces via the + `virtual-chain-changed` notification. +- **Mature** — confirmed past the maturity threshold; the new UTXOs are + spendable. Surfaces via a `Maturity` event on the managed + [Wallet](../wallet/transaction-history.md) or the + [`UtxoProcessor`](../wallet-sdk/utxo-processor.md). + +For confirmation, the right gate is the `Maturity` event for the +specific transaction, not "wait N seconds" and not the first time it +appears in a block. See [Kaspa Concepts → Maturity](../concepts.md) for +the protocol view. + +## Failures and retries + +`submit_transaction` raises if the node rejects the transaction. +Common reasons: + +- **`fee too low`** — recompute mass with `update_transaction_mass` + *after* any input/output change, then re-sign. +- **`orphan`** — an input references a transaction the node hasn't seen. + Either wait for the parent to land, or set `allowOrphan=True` when you + intentionally submit a chain. +- **`already in mempool`** — the same `transaction_id` is already + pending. Safe to ignore for retries. +- **`mass exceeded`** — the transaction is over + `maximum_standard_transaction_mass()`. Split the inputs across + multiple transactions; the Generator does this automatically when its + input set is too large. + +A transaction that was virtual-chain accepted *can* be reorged out — at +which point its outputs are no longer mature. The SDK surfaces this as +a `Reorg` event; see +[Wallet → Transaction History](../wallet/transaction-history.md). + +## Where to next + +- [Wallet → Send Transaction](../wallet/send-transaction.md) — the + managed Wallet's send surface, which wraps all of this. +- [Wallet → Transaction History](../wallet/transaction-history.md) — + observing maturity and reorgs. +- [Kaspa Concepts](../concepts.md) — virtual chain, DAA score, maturity. diff --git a/docs/learn/wallet-sdk/derivation.md b/docs/learn/wallet-sdk/derivation.md new file mode 100644 index 00000000..6a8827ab --- /dev/null +++ b/docs/learn/wallet-sdk/derivation.md @@ -0,0 +1,159 @@ +# Derivation + +Once you have an `XPrv` (see [Key Management](key-management.md)), +derivation produces every other key the wallet uses. Kaspa follows +BIP-44 with coin type `111111`: + +``` +m / 44' / 111111' / account' / chain / address_index +``` + +`chain` is `0` for receive addresses, `1` for change. `account` and +`address_index` are unhardened relative to the account-level node. + +See the [Kaspa MDBook page on +derivation](https://kaspa-mdbook.aspectron.com/wallets/addresses.html) +for the protocol-level details. + +## Extended keys + +```python +from kaspa import Mnemonic, XPrv + +seed = Mnemonic.random().to_seed() +xprv = XPrv(seed) + +print(xprv.xprv) # serialized xprv string +print(xprv.private_key) # the master secp256k1 secret +print(xprv.depth) # 0 for the master +print(xprv.chain_code) # 32 bytes +``` + +`XPub` is the public counterpart — useful for watch-only wallets: + +```python +xpub = xprv.to_xpub() +print(xpub.xpub) +print(xpub.to_public_key()) +``` + +## Deriving child keys directly + +```python +from kaspa import DerivationPath + +# By child number +child = xprv.derive_child(0) +hardened = xprv.derive_child(0, hardened=True) + +# By path string +account_xprv = xprv.derive_path("m/44'/111111'/0'") + +# By DerivationPath instance +path = DerivationPath("m/44'/111111'/0'/0/0") +leaf = xprv.derive_path(path) +``` + +`DerivationPath` is mutable — handy when you want to walk a chain +incrementally: + +```python +path = DerivationPath("m/44'/111111'/0'") +path.push(0) # → m/44'/111111'/0'/0 +path.push(0) # → m/44'/111111'/0'/0/0 +print(path.to_string(), path.length(), path.is_empty()) +print(path.parent().to_string()) +``` + +## `PrivateKeyGenerator` + +For everyday "give me address `i`" derivation, use `PrivateKeyGenerator` — +it handles the full BIP-44 path for you: + +```python +from kaspa import PrivateKeyGenerator, NetworkType + +gen = PrivateKeyGenerator( + xprv=xprv, # accepts XPrv or its xprv-string form + is_multisig=False, + account_index=0, +) + +for i in range(5): + key = gen.receive_key(i) # m/44'/111111'/0'/0/i + addr = key.to_address(NetworkType.Mainnet) + print(i, addr.to_string()) + +change = gen.change_key(0) # m/44'/111111'/0'/1/0 +``` + +## `PublicKeyGenerator` (watch-only) + +When you only need addresses — no signing — `PublicKeyGenerator` derives +them from an `xpub` alone: + +```python +from kaspa import PublicKeyGenerator, NetworkType + +pub = PublicKeyGenerator.from_xpub("xpub...") + +# A single address +addr = pub.receive_address(NetworkType.Mainnet, 0) + +# A range +addrs = pub.receive_addresses(NetworkType.Mainnet, start=0, end=10) + +# Public keys (not addresses) +pubkeys = pub.receive_pubkeys(start=0, end=5) +``` + +Or, if you have an `XPrv` but want a public-key-only generator (e.g. +a watch-only mode in the same process): + +```python +pub = PublicKeyGenerator.from_master_xprv( + xprv=xprv_string, + is_multisig=False, + account_index=0, +) +``` + +`PublicKeyGenerator` exposes `change_addresses(...)` and the +`*_as_strings` variants that skip the `Address` wrapper. + +## Multi-signature derivation + +Each cosigner has their own `cosigner_index`: + +```python +gen0 = PrivateKeyGenerator(xprv=our_xprv, is_multisig=True, + account_index=0, cosigner_index=0) +gen1 = PrivateKeyGenerator(xprv=their_xprv, is_multisig=True, + account_index=0, cosigner_index=1) +``` + +For the full multisig wallet flow (creating the multisig address, +spending from it), see the +[Multi-signature transactions](../../guides/multisig.md) recipe. + +## Account-kind tag + +`AccountKind` is the metadata type the wallet uses to track which +derivation rules apply. You only construct one explicitly when calling +the wallet's account-creation methods: + +```python +from kaspa import AccountKind + +bip32 = AccountKind("bip32") +print(bip32.to_string()) # "bip32" +``` + +## Where to next + +- [Transaction Generator](tx-generator.md) — sign and submit transactions + with the keys you just derived. +- [Wallet → Accounts](../wallet/accounts.md) — the managed Wallet's + higher-level account API uses these primitives internally. +- [Custom derivation paths](../../guides/custom-derivation.md) — recipe + for non-standard paths. diff --git a/docs/learn/wallet-sdk/index.md b/docs/learn/wallet-sdk/index.md new file mode 100644 index 00000000..a8bde225 --- /dev/null +++ b/docs/learn/wallet-sdk/index.md @@ -0,0 +1,36 @@ +# Wallet SDK + +The **Wallet SDK** section is the layer beneath the managed +[Wallet](../wallet/index.md). When you don't need on-disk file storage, +multi-account management, or the wallet's event multiplexer — when you +just want to derive a key, build a transaction, or track UTXOs for a few +addresses — drop down here. + +## What lives here + +| Page | What it covers | +| --- | --- | +| [Key Management](key-management.md) | `Mnemonic`, BIP-39 seed, `XPrv`, hex import/export. | +| [Derivation](derivation.md) | `PrivateKeyGenerator`, `PublicKeyGenerator`, `DerivationPath`, BIP-44. | +| [Transaction Generator](tx-generator.md) | The `Generator` class — UTXO selection, fees, signing, submission. | +| [UTXO Context](utxo-context.md) | `UtxoContext`: per-address UTXO tracking. | +| [UTXO Processor](utxo-processor.md) | `UtxoProcessor`: the engine that drives `UtxoContext`s. | + +## Wallet vs. Wallet SDK in one table + +| You want to... | Use | +| --- | --- | +| Open a file, manage many accounts, track them long-term | [Wallet](../wallet/index.md) | +| Sign one transaction in a script with a key you already have | Wallet SDK ([Transaction Generator](tx-generator.md)) | +| Derive an address from a mnemonic without persisting anything | Wallet SDK ([Key Management](key-management.md), [Derivation](derivation.md)) | +| Watch a fixed set of addresses for incoming UTXOs without a wallet file | Wallet SDK ([UTXO Processor](utxo-processor.md), [UTXO Context](utxo-context.md)) | + +The managed `Wallet` is built from these pieces — every primitive on this +page is what `Wallet` wraps internally. + +## Where to next + +If you're new here, start with [Key Management](key-management.md) → +[Derivation](derivation.md) → [Transaction Generator](tx-generator.md). +That sequence walks the typical "make a key, build a transaction, send +it" flow without any file I/O. diff --git a/docs/learn/wallet-sdk/key-management.md b/docs/learn/wallet-sdk/key-management.md new file mode 100644 index 00000000..7d798ed9 --- /dev/null +++ b/docs/learn/wallet-sdk/key-management.md @@ -0,0 +1,117 @@ +# Key Management + +Everything in this page is BIP-39-compatible. The SDK gives you `Mnemonic` +for the human-readable phrase, the seed bytes that come out of it, and +`XPrv` for the master extended private key the seed produces. From an +`XPrv` you derive child keys — that's the next page, +[Derivation](derivation.md). + +Read [Security](../../getting-started/security.md) before generating a +real mnemonic. + +## Generate a mnemonic + +```python +from kaspa import Mnemonic + +m = Mnemonic.random() # 24 words, default +m12 = Mnemonic.random(word_count=12) # 12 words +print(m.phrase) +``` + +24 words is the recommended default — more entropy, lower brute-force +risk. 12 words is supported for compatibility with tools that emit them. + +## Restore from a mnemonic + +```python +from kaspa import Mnemonic + +phrase = "abandon abandon abandon ... about" + +if Mnemonic.validate(phrase): + m = Mnemonic(phrase) +else: + raise ValueError("invalid mnemonic") +``` + +`Mnemonic.validate(phrase)` checks word membership, length, and the BIP-39 +checksum. It returns a bool — it does not raise. + +## Validation + +```python +from kaspa import Mnemonic, Language + +ok = Mnemonic.validate(phrase) # English assumed +ok_en = Mnemonic.validate(phrase, Language.English) # explicit +``` + +## Convert to a seed + +```python +from kaspa import Mnemonic, XPrv + +m = Mnemonic.random() +seed = m.to_seed() # 64-byte BIP-39 seed +seed_with_passphrase = m.to_seed("25th-word") # different seed; same mnemonic + +xprv = XPrv(seed) +``` + +!!! info "Passphrase" + The optional passphrase (sometimes called the "25th word") changes + the seed. The same mnemonic with different passphrases produces + different wallets. An attacker who recovers the mnemonic alone gets + nothing without the passphrase. + +## Inspect entropy + +The `entropy` property exposes the underlying random bits as a hex string +— the raw input the BIP-39 phrase encodes: + +```python +m = Mnemonic.random() +print(m.entropy) # hex +m.entropy = "" # advanced; rebuilds the phrase +``` + +You rarely need to set `entropy` directly. The two cases that come up: +re-creating a `Mnemonic` from entropy emitted by another tool, and +debugging a vector mismatch against a third-party implementation. + +## Languages + +```python +from kaspa import Mnemonic, Language + +m = Mnemonic(phrase, Language.English) +``` + +English is the default and is what every Kaspa example uses. The other +BIP-39 wordlists exist on the enum but are rarely used in practice — if +you don't know you need a non-English wordlist, use English. + +## Hex private keys (`SecretKey`) + +For one-key accounts (a single secp256k1 secret with no derivation), +skip the mnemonic entirely: + +```python +from kaspa import PrivateKey + +key = PrivateKey("<64-char hex>") +addr = key.to_address("testnet-10") +``` + +The 64-char hex string is what +[Wallet → Keypair Accounts](../wallet/keypair.md) takes as the `secret` +input to `prv_key_data_create(kind=PrvKeyDataVariantKind.SecretKey)`. + +## Where to next + +- [Derivation](derivation.md) — turn the `XPrv` into addresses. +- [Transaction Generator](tx-generator.md) — sign a transaction with a + key you derived. +- [Security](../../getting-started/security.md) — secret-handling rules + before any of the above touches mainnet. diff --git a/docs/learn/wallet-sdk/tx-generator.md b/docs/learn/wallet-sdk/tx-generator.md new file mode 100644 index 00000000..5c0080ac --- /dev/null +++ b/docs/learn/wallet-sdk/tx-generator.md @@ -0,0 +1,185 @@ +# Transaction Generator + +The `Generator` is the SDK's built-in coin selector and fee calculator. +You hand it UTXOs, a change address, and the outputs you want; it picks +inputs, computes mass and fees, and yields one or more +`PendingTransaction`s ready to sign and submit. + +## Send a payment, end to end + +```python +import asyncio +from kaspa import ( + RpcClient, Resolver, Generator, PaymentOutput, + Address, PrivateKey, NetworkId, +) + +async def main(): + client = RpcClient(resolver=Resolver(), network_id="mainnet") + await client.connect() + try: + key = PrivateKey("<64-char hex>") + my_addr = key.to_address("mainnet") + + utxos = await client.get_utxos_by_addresses({ + "addresses": [my_addr.to_string()], + }) + + recipient = Address("kaspa:...") + gen = Generator( + network_id=NetworkId("mainnet"), + entries=utxos["entries"], + change_address=my_addr, + outputs=[PaymentOutput(recipient, 500_000_000)], # 5 KAS + ) + + for pending in gen: + pending.sign([key]) + tx_id = await pending.submit(client) + print("submitted:", tx_id) + + print(gen.summary().fees, gen.summary().transactions) + finally: + await client.disconnect() + +asyncio.run(main()) +``` + +A `Generator` is *iterable* — when the input set is too large for a single +transaction's mass budget, it yields a chain of consolidating transactions +followed by the final payment. Loop and submit each. + +## Constructor options + +```python +gen = Generator( + network_id=NetworkId("mainnet"), + entries=utxos, # list[UtxoEntry] OR a UtxoContext + change_address=my_addr, + outputs=[PaymentOutput(recipient, amount)], + + # optional + payload=b"optional-data", # OP_RETURN-equivalent payload + priority_fee=1000, # extra fee in sompi + priority_entries=priority, # UTXOs to consume first + sig_op_count=1, # signature ops per input + minimum_signatures=1, # for multisig mass estimation + fee_rate=2.0, # explicit sompi/gram override +) +``` + +`entries` accepts a [`UtxoContext`](utxo-context.md) directly — pass the +context and it consumes from the mature set without you copying the list. + +## Estimate before signing + +```python +from kaspa import estimate_transactions + +# via a Generator +summary = gen.estimate() +print(summary.fees, summary.transactions, summary.utxos) + +# via the standalone function +summary = estimate_transactions( + network_id="mainnet", + entries=utxos, + change_address=my_addr, + outputs=[{"address": recipient, "amount": amount}], +) +``` + +`estimate()` does not consume the generator; you can iterate it for real +afterwards. + +## Pending transactions + +Each item the generator yields exposes the proposed transaction's +metadata: + +```python +for pending in gen: + print(pending.id, pending.fee_amount, pending.mass) + print(pending.payment_amount, pending.change_amount, pending.transaction_type) + inputs = pending.get_utxo_entries() + addrs = pending.addresses() + raw_tx = pending.transaction +``` + +Use this when you need to surface fee / mass to a user before they +authorize a signature. + +## Signing + +```python +# All inputs at once with one or more keys +pending.sign([key]) +pending.sign([key1, key2, key3]) # multisig + +# Per-input control +for i, _ in enumerate(pending.get_utxo_entries()): + pending.sign_input(i, key) + +# Custom signature scripts (advanced) +from kaspa import SighashType + +sig = pending.create_input_signature( + input_index=0, + private_key=key, + sighash_type=SighashType.All, +) +pending.fill_input(0, custom_script_bytes) +``` + +## Submit + +```python +tx_id = await pending.submit(client) + +# Or manually: +result = await client.submit_transaction({ + "transaction": pending.transaction.serialize_to_dict(), + "allowOrphan": False, +}) +``` + +`pending.submit(client)` is the right path. The manual route is for cases +where you need to round-trip the transaction through another system +before submission. + +## One-shot helpers + +When the loop-and-submit pattern is more code than you need: + +```python +from kaspa import create_transaction, create_transactions + +tx = create_transaction( + utxo_entry_source=utxos, + outputs=[{"address": "kaspa:...", "amount": 100_000_000}], + priority_fee=1000, +) + +result = create_transactions( + network_id="mainnet", + entries=utxos, + change_address=my_addr, + outputs=[{"address": "kaspa:...", "amount": 100_000_000}], + priority_fee=1000, +) + +for pending in result["transactions"]: + pending.sign([key]) + await pending.submit(client) + +print(result["summary"]) +``` + +## Where to next + +- [UTXO Context](utxo-context.md) — pass a context as `entries` instead + of a raw list. +- [Wallet → Send Transaction](../wallet/send-transaction.md) — the + managed Wallet wraps `Generator` with sensible defaults. +- [Multi-signature transactions](../../guides/multisig.md) — full multisig + recipe including `minimum_signatures`. diff --git a/docs/learn/wallet-sdk/utxo-context.md b/docs/learn/wallet-sdk/utxo-context.md new file mode 100644 index 00000000..a4215401 --- /dev/null +++ b/docs/learn/wallet-sdk/utxo-context.md @@ -0,0 +1,86 @@ +# UTXO Context + +A `UtxoContext` tracks UTXOs for a fixed set of addresses. It's bound to +a [UTXO Processor](utxo-processor.md) and gets fed by it: as the +processor receives notifications from the node, it routes the changes +into whichever contexts have registered the relevant addresses. The +context exposes the resulting UTXO set, balance, and mature/pending +splits. + +If you're using the managed [Wallet](../wallet/index.md), it manages a +`UtxoContext` per activated account internally — you usually don't +construct one yourself. Drop down here when you want UTXO tracking +without the on-disk wallet file. + +## Build one + +```python +from kaspa import NetworkId, Resolver, RpcClient, UtxoContext, UtxoProcessor + +client = RpcClient(resolver=Resolver(), network_id="testnet-10") +await client.connect() + +processor = UtxoProcessor(client, NetworkId("testnet-10")) +await processor.start() + +context = UtxoContext(processor) +await context.track_addresses(["kaspatest:qr0lr4ml..."]) +``` + +`UtxoContext(processor, id=...)` accepts an optional 32-byte hex id. If +you omit it, one is generated. Use the explicit id when you need the +context to be addressable across reconnects. + +## What it exposes + +```python +print(context.is_active) # bool — processor running? +print(context.balance) # Balance | None +print(context.balance_strings) # BalanceStrings | None (formatted) +print(context.mature_length) # int — number of spendable UTXOs + +mature = context.mature_range(from_=0, to=10) # list[UtxoEntryReference] +pending = context.pending() # list[UtxoEntryReference] +``` + +`balance` is `None` until the first notification arrives. After that it's +a `Balance(mature, pending, outgoing)` in sompi. + +## Add and remove tracked addresses + +```python +await context.track_addresses(["kaspatest:..."]) +await context.unregister_addresses(["kaspatest:..."]) +await context.clear() # forget every address and UTXO +``` + +`track_addresses` accepts `Address` instances or their string forms. +`current_daa_score=...` is optional — supply it if you want the scan to +ignore confirmations older than that score. + +## Use as `Generator` input + +The [Transaction Generator](tx-generator.md) accepts a `UtxoContext` as +its `entries` argument: + +```python +from kaspa import Generator, PaymentOutput + +gen = Generator( + entries=context, # mature UTXOs come from here + change_address=my_addr, + outputs=[PaymentOutput(recipient, 100_000_000)], +) +``` + +This avoids snapshotting the UTXO list yourself; the generator pulls the +current mature set when it iterates. + +## Where to next + +- [UTXO Processor](utxo-processor.md) — the engine the context is bound + to. +- [Transaction Generator](tx-generator.md) — sending using a + `UtxoContext` as input. +- [Wallet → Architecture](../wallet/architecture.md) — how the managed + Wallet uses `UtxoContext`s internally. diff --git a/docs/learn/wallet-sdk/utxo-processor.md b/docs/learn/wallet-sdk/utxo-processor.md new file mode 100644 index 00000000..b91aacaf --- /dev/null +++ b/docs/learn/wallet-sdk/utxo-processor.md @@ -0,0 +1,96 @@ +# UTXO Processor + +A `UtxoProcessor` subscribes to a node's UTXO and virtual-chain +notifications and dispatches them to one or more +[UTXO Contexts](utxo-context.md). It's the engine that makes context +tracking work. If you're using the managed +[Wallet](../wallet/index.md), one is constructed for you. If you're not, +you build one yourself and bind contexts to it. + +## Construction + +```python +from kaspa import NetworkId, Resolver, RpcClient, UtxoProcessor + +client = RpcClient(resolver=Resolver(), network_id="testnet-10") +await client.connect() + +processor = UtxoProcessor(client, NetworkId("testnet-10")) +await processor.start() + +# ...later... +await processor.stop() +await client.disconnect() +``` + +`start()` is what activates the processor — it begins subscribing to +node notifications and forwarding them. `stop()` is the matching +shutdown. Without `start()`, contexts bound to it stay empty. + +## Properties + +| Property | Meaning | +| --- | --- | +| `processor.rpc` | The `RpcClient` it's reading from. | +| `processor.network_id` | The `NetworkId` it was constructed with. | +| `processor.is_active` | `True` after `start()`, `False` after `stop()` or before. | + +## Events + +The processor has its own event surface — a smaller, lower-level cousin +of the [managed Wallet's events](../wallet/transaction-history.md). +Listeners use the same shape: + +```python +def on_event(event): + print(event["type"], event.get("data")) + +processor.add_event_listener( + ["utxo-proc-start", "utxo-proc-stop", "pending", "maturity", + "reorg", "stasis", "discovery", "balance", "utxo-proc-error", "error"], + on_event, +) +``` + +Common events: + +| Event | When it fires | +| --- | --- | +| `utxo-proc-start` / `utxo-proc-stop` | Processor entered / left the active state. | +| `pending` | A UTXO landed for a tracked address but isn't mature yet. | +| `maturity` | A previously-pending UTXO crossed the maturity depth. | +| `reorg`, `stasis` | A UTXO was unwound or coinbase-locked. | +| `discovery` | A scan-time discovery hit. | +| `balance` | A bound `UtxoContext`'s balance changed. | +| `utxo-proc-error`, `error` | Something went wrong. | + +`UtxoProcessorEvent` is the enum form if you'd rather pass it as a +typed value than a string. + +## Coordinating with `asyncio` + +Listener callbacks may run on a background thread. If you need to +signal an `asyncio.Event` from one, bridge through the loop: + +```python +loop = asyncio.get_running_loop() +got_start = asyncio.Event() + +def on_event(event): + if event.get("type") == "utxo-proc-start": + loop.call_soon_threadsafe(got_start.set) + +processor.add_event_listener("utxo-proc-start", on_event) +await processor.start() +await got_start.wait() +``` + +This is the same pattern the managed Wallet uses internally. + +## Where to next + +- [UTXO Context](utxo-context.md) — bind a context to the processor. +- [Transaction Generator](tx-generator.md) — pass that context as + `entries`. +- [Wallet → Architecture](../wallet/architecture.md) — how the managed + Wallet drives a `UtxoProcessor` for you. diff --git a/docs/learn/wallet/accounts.md b/docs/learn/wallet/accounts.md new file mode 100644 index 00000000..866111fa --- /dev/null +++ b/docs/learn/wallet/accounts.md @@ -0,0 +1,120 @@ +# Accounts + +A wallet holds N accounts of mixed kinds, each backed by exactly one PKD. +The two everyday kinds are **BIP32** (HD-derived; one mnemonic backs many +accounts at different `account_index`es) and **keypair** (a single +secp256k1 key, one address — see [Keypair Accounts](keypair.md)). + +## Account kinds + +| Kind | Backing PKD | Address derivation | +| --- | --- | --- | +| `bip32` | `Mnemonic`, `Bip39Seed`, or `ExtendedPrivateKey` | HD path `m/44'/111111'/'//` | +| `keypair` | `SecretKey` | One address per account (Schnorr or ECDSA) | +| `multisig`, `bip32watch`, `legacy` | — | Specialised; not covered here. | + +This page covers BIP32. Keypair accounts have their own page. + +## Surface + +| Method | Purpose | +| --- | --- | +| `accounts_enumerate()` | List `AccountDescriptor` for every account. | +| `accounts_get(id)` | Fetch a single descriptor. | +| `accounts_create_bip32(...)` | Create a new HD account at a given `account_index`. | +| `accounts_import_bip32(...)` | Same, but runs an address-discovery scan first. | +| `accounts_rename(...)` | Update an account's name. | +| `accounts_activate([ids])` | Begin UTXO tracking for the given accounts (or all). | +| `accounts_ensure_default(...)` | Idempotently ensure a default `bip32` account exists. | + +For deriving the next address on an existing account, see +[Addresses](addresses.md). For sending, see +[Send Transaction](send-transaction.md). + +## Create a BIP32 account + +```python +prv_key_id = await wallet.prv_key_data_create( + wallet_secret=secret, + secret="<24-word mnemonic>", + kind=PrvKeyDataVariantKind.Mnemonic, + name="demo-mnemonic-key", +) + +acct = await wallet.accounts_create_bip32( + wallet_secret=secret, + prv_key_data_id=prv_key_id, + account_name="demo-acct-0", + account_index=0, # omit to use the next free index +) +``` + +A second account from the same mnemonic only changes `account_index`: + +```python +acct1 = await wallet.accounts_create_bip32( + wallet_secret=secret, prv_key_data_id=prv_key_id, account_index=1, +) +``` + +## Inspect + +```python +for a in await wallet.accounts_enumerate(): + print(a.account_id, a.kind, a.account_name, a.balance) + print(" receive:", a.receive_address) + print(" change: ", a.change_address) +``` + +`AccountDescriptor` exposes `kind`, `account_id`, `account_name`, +`balance`, `prv_key_data_ids`, `receive_address` / `change_address`, and +(for HD only) `account_index`, `xpub_keys`, `ecdsa`, +`receive_address_index`, `change_address_index`. `get_addresses()` returns +every derived address. + +## Activate + +Accounts must be activated before they emit balance events or are usable +for sends. Activation requires a connected wRPC client *and* a synced +wallet — see [Start](start.md). + +```python +await wallet.accounts_activate([acct.account_id]) +# or, activate everything in the wallet: +await wallet.accounts_activate() +``` + +## Ensure-default + +```python +from kaspa import AccountKind + +acct = await wallet.accounts_ensure_default( + wallet_secret=secret, + account_kind=AccountKind("bip32"), + mnemonic_phrase=None, # generate a fresh mnemonic if creating +) +``` + +Returns the existing default `bip32` account if there is one, otherwise +creates one (generating a fresh mnemonic when `mnemonic_phrase` is +`None`). Only `bip32` is supported; other kinds raise. + +## Import vs. create + +`accounts_import_bip32` is the recovery-flow variant: it runs an +address-discovery scan before adding the account, so addresses that have +already received funds are recognised as used. Use it when restoring from +a known-used mnemonic; use `accounts_create_bip32` for fresh accounts. + +To scan a mnemonic *before* picking an index, see +[Wallet Recovery](../../guides/wallet-recovery.md). + +## Where to next + +- [Addresses](addresses.md) — derive new receive / change addresses on an + existing account. +- [Keypair Accounts](keypair.md) — single-key accounts. +- [Send Transaction](send-transaction.md) — outgoing flows. +- [Transaction History](transaction-history.md) — `Balance`, `Pending`, + and `Maturity` events. diff --git a/docs/learn/wallet/addresses.md b/docs/learn/wallet/addresses.md new file mode 100644 index 00000000..fa5e8070 --- /dev/null +++ b/docs/learn/wallet/addresses.md @@ -0,0 +1,70 @@ +# Addresses + +A BIP32 account derives addresses lazily. The wallet records two indices +per account — one for receive, one for change — and `accounts_create_new_address` +advances them. Keypair accounts have a single, fixed address and reject this +call. + +## Read the current addresses + +`AccountDescriptor` already carries the most recent of each: + +```python +acct = await wallet.accounts_get(acct_id) +print(acct.receive_address) # for the next-to-receive index +print(acct.change_address) # for the next-to-spend-from-as-change index +print(acct.receive_address_index) # int, BIP32 only +print(acct.change_address_index) # int, BIP32 only +``` + +`get_addresses()` returns *every* derived address on the account, which +is what you want for re-subscribing UTXO notifications across all of them. + +## Derive the next address + +```python +from kaspa import NewAddressKind + +next_recv = await wallet.accounts_create_new_address( + acct.account_id, NewAddressKind.Receive, +) +next_change = await wallet.accounts_create_new_address( + acct.account_id, NewAddressKind.Change, +) +``` + +The index used is the descriptor's `receive_address_index` or +`change_address_index` *before* the call; afterwards the descriptor's +counter advances by one. Each newly derived address is automatically +registered with the account's `UtxoContext`, so funds sent to it will +appear in the next sync. + +## Receive vs. change + +- **Receive** addresses are what you give out. Generate one any time you + want a new public-facing address — for billing, for separating + customers, for a hot/cold split. +- **Change** addresses are where the wallet returns leftover funds when + spending. The `Generator` (used internally by `accounts_send`) picks the + current change address automatically; you usually don't need to advance + the change index manually. + +If you're sweeping a UTXO set and want the leftover to stay in the same +account but on a *fresh* change address, advance the change index first — +see [Sweep Funds](sweep.md). + +## Address discovery on import + +When you `accounts_import_bip32` (rather than `accounts_create_bip32`), +the wallet walks the receive and change chains looking for addresses that +have ever held a UTXO and bumps the indices accordingly. This is what +makes a restored wallet "remember" addresses it had previously handed out. +Without it, `next_recv` would silently re-issue an already-used address. + +## Where to next + +- [Send Transaction](send-transaction.md) — sending to an address. +- [Transaction History](transaction-history.md) — events that fire when a + derived address receives funds. +- [Wallet Recovery](../../guides/wallet-recovery.md) — scanning a mnemonic + to find used account indices before importing. diff --git a/docs/learn/wallet/architecture.md b/docs/learn/wallet/architecture.md new file mode 100644 index 00000000..61fa7e05 --- /dev/null +++ b/docs/learn/wallet/architecture.md @@ -0,0 +1,77 @@ +# Architecture + +A `Wallet` is not a single object — it's a small system of cooperating +pieces. Knowing how they fit together is what makes the rest of this +section make sense, especially the [sync gate](start.md) and the +[transaction-history events](transaction-history.md). + +## The pieces + +``` + ┌──────────────────────────────────────────┐ + │ Wallet │ + │ (lifecycle, file storage, accounts) │ + └───────────┬───────────────┬──────────────┘ + │ │ + owns │ │ owns + ▼ ▼ + ┌────────────────┐ ┌──────────────┐ + │ UtxoProcessor │ │ RpcClient │ + └────────┬───────┘ └──────┬───────┘ + │ │ + fans out to │ pushes notifications + │ │ + ▼ ▼ + ┌────────────────────────────────┐ + │ UtxoContext (one per │ + │ activated account) │ + └────────────────────────────────┘ +``` + +| Component | Job | +| --- | --- | +| **`Wallet`** | Lifecycle, on-disk file storage, account list, event multiplexer. The thing your code calls. | +| **`RpcClient`** | The wRPC connection. Used internally for calls and as the source of node-pushed notifications. | +| **`UtxoProcessor`** | Subscribes to virtual-chain / UTXO notifications, tracks `synced` state, routes incoming UTXO changes to the right `UtxoContext`. | +| **`UtxoContext`** | One per activated account. Holds the tracked addresses, the per-state balance (`mature`, `pending`, `outgoing`), and the mature UTXO set the coin selector pulls from. | + +The wallet *does not poll* the node for UTXO state. It is **fed**, by the +processor, from notifications. This is why the [sync gate](start.md) +matters — before sync, the processor isn't forwarding anything, so the +contexts stay empty. + +## UTXO maturity + +Every UTXO the processor sees moves through three states: + +- **Pending** — seen, but the chain confirmation depth is below the + maturity threshold. Counted in `Balance.pending`. *Not* spendable. +- **Mature** — confirmed deeply enough to spend. Counted in + `Balance.mature`. Returned by `accounts_get_utxos`. Selectable. +- **Outgoing** — locked because the wallet just spent it in a transaction + it generated. Counted in `Balance.outgoing` until the spend matures or is + reorged out. + +[Send Transaction](send-transaction.md) waits on `Maturity` for this +reason: a `Pending` UTXO is real, but the next `accounts_send` won't see +it as spendable. + +## Why `accounts_get_utxos` can return `[]` + +`accounts_get_utxos` is a read of the in-memory `UtxoContext`. It returns +`[]` when: + +1. The wallet isn't synced yet (the processor isn't forwarding). +2. The account hasn't been activated. +3. No notification for a funding tx has reached the processor yet. + +None of these are "the address has no funds" — they're "the wallet hasn't +been told yet." The fix is to gate UTXO-dependent code on `is_synced` and +to listen for `Maturity` rather than polling. See [Start](start.md) and +[Transaction History](transaction-history.md). + +## Where to next + +- [Lifecycle](lifecycle.md) — the state machine. +- [Start](start.md) — `start → connect → is_synced` and why each step matters. +- [Transaction History](transaction-history.md) — the event surface. diff --git a/docs/learn/wallet/index.md b/docs/learn/wallet/index.md new file mode 100644 index 00000000..52922b65 --- /dev/null +++ b/docs/learn/wallet/index.md @@ -0,0 +1,73 @@ +# Wallet + +The `Wallet` class is the SDK's high-level managed wallet. It layers +encrypted on-disk storage, multi-account management, an event bus, and +built-in send / transfer / sweep flows on top of the lower-level primitives +in [Wallet SDK](../wallet-sdk/index.md). + +## When to reach for `Wallet` + +| You want to... | Use | +| --- | --- | +| Persist secrets, manage multiple accounts, react to chain events | `Wallet` | +| Sign one transaction in a script, no on-disk state | The primitives in [Wallet SDK](../wallet-sdk/index.md) | +| Embed wallet behaviour in your own app | `Wallet` — the listener model and account API are designed for this | + +If you only need a one-shot signer, the primitives are simpler. If you need +the "open file → manage keys → track UTXOs → send" loop, `Wallet` saves +you from re-implementing it. + +## A wallet, end to end + +```python +import asyncio +from kaspa import PrvKeyDataVariantKind, Resolver, Wallet + +async def main(): + wallet = Wallet(network_id="testnet-10", resolver=Resolver()) + await wallet.start() + await wallet.connect() + + await wallet.wallet_create( + wallet_secret="example-secret", + filename="demo", + title="demo", + ) + prv_key_id = await wallet.prv_key_data_create( + wallet_secret="example-secret", + secret="", + kind=PrvKeyDataVariantKind.Mnemonic, + ) + descriptor = await wallet.accounts_create_bip32( + wallet_secret="example-secret", + prv_key_data_id=prv_key_id, + account_index=0, + ) + await wallet.accounts_activate([descriptor.account_id]) + + await wallet.wallet_close() + await wallet.stop() + +asyncio.run(main()) +``` + +This script creates a wallet file, derives a BIP32 account from a +mnemonic, and activates it. Re-running fails with +`WalletAlreadyExistsError` unless you switch to `wallet_open` — see +[Open](open.md). + +## How this section is laid out + +- [Architecture](architecture.md) — `Wallet` / `UtxoProcessor` / + `UtxoContext` and how notifications flow through them. +- [Lifecycle](lifecycle.md) — the state machine and ordering rules for the + whole class. +- [Initialize](initialize.md), [Start](start.md), [Open](open.md) — each + phase of bringing a wallet up. +- [Private Keys](private-keys.md), [Accounts](accounts.md), + [Addresses](addresses.md), [Keypair Accounts](keypair.md) — populating + the wallet. +- [Send Transaction](send-transaction.md), [Sweep Funds](sweep.md) — + outgoing flows. +- [Transaction History](transaction-history.md) — the event surface and + history APIs. diff --git a/docs/learn/wallet/initialize.md b/docs/learn/wallet/initialize.md new file mode 100644 index 00000000..a5a3e759 --- /dev/null +++ b/docs/learn/wallet/initialize.md @@ -0,0 +1,62 @@ +# Initialize + +Constructing a `Wallet` does no I/O. It builds the local file store and an +internal wRPC client, and that's it — `start()` is the next step. + +## Constructor + +```python +from kaspa import Resolver, Wallet + +wallet = Wallet( + network_id="testnet-10", # required + resolver=Resolver(), # discover a public node + # url=... # OR a known node URL + # encoding="borsh", # default; "json" also accepted + # storage_folder=None, # override default ~/.kaspa +) +``` + +| Argument | Required | Notes | +| --- | --- | --- | +| `network_id` | yes | `"mainnet"`, `"testnet-10"`, `"testnet-11"`. Drives both the resolver query and the address encoding. | +| `resolver` | one of | A `Resolver` instance — see [RPC → Resolver](../rpc/resolver.md). | +| `url` | resolver/url | A known wRPC URL (`wss://node.example:17110`). Skip the resolver if you set this. | +| `encoding` | optional | `"borsh"` (default) or `"json"`. Borsh is right for almost everything. | + +`network_id` is **required** — addresses derived from this wallet will be +encoded for that network and the resolver only returns nodes on that +network. + +## Switching networks + +`set_network_id` raises if the wallet is currently connected: + +```python +await wallet.disconnect() +wallet.set_network_id("mainnet") +await wallet.connect() +``` + +Switching network does not invalidate the file store on disk — but the +*addresses* derived from a BIP32 account are network-specific, so a key +created under `testnet-10` produces different (testnet) addresses than the +same key under `mainnet`. + +## Storage folder + +By default the wallet stores files under `~/.kaspa/`. Override with +`storage_folder=...` when: + +- Running tests against a temp directory. +- Running multiple isolated wallets in the same process. +- Sandboxing per-user wallet stores in a multi-tenant service. + +The folder is created on first write; nothing is done at construction +time. + +## Where to next + +- [Start](start.md) — boot the runtime and connect to the node. +- [Open](open.md) — create or open a wallet file. +- [Lifecycle](lifecycle.md) — the full state machine. diff --git a/docs/learn/wallet/keypair.md b/docs/learn/wallet/keypair.md new file mode 100644 index 00000000..dc7d2c6c --- /dev/null +++ b/docs/learn/wallet/keypair.md @@ -0,0 +1,74 @@ +# Keypair Accounts + +A keypair account holds one secp256k1 key and produces one address. It has +no derivation tree — there's no "next address" and no `account_index`. +Use these when you have a single secret you want to manage alongside other +accounts in the same wallet, or when you're moving an existing standalone +key into managed storage. + +## Create a keypair account + +A keypair account is backed by a `SecretKey`-variant PKD: + +```python +from kaspa import PrivateKey, PrvKeyDataVariantKind + +# 64-char hex secp256k1 secret +secret_hex = PrivateKey(...).to_string() + +secret_pkd = await wallet.prv_key_data_create( + wallet_secret=secret, + secret=secret_hex, + kind=PrvKeyDataVariantKind.SecretKey, + name="demo-secret-key", +) + +kp = await wallet.accounts_create_keypair( + wallet_secret=secret, + prv_key_data_id=secret_pkd, + ecdsa=False, # False = Schnorr (default), True = ECDSA + account_name="keypair-acct", +) +``` + +`ecdsa=True` is for ECDSA-style keypair accounts; the default Schnorr +variant is what most callers want. + +## What the descriptor looks like + +Keypair `AccountDescriptor`s have: + +| Field | Value | +| --- | --- | +| `kind` | `"keypair"` | +| `account_id` | stable id | +| `receive_address` | the one address | +| `change_address` | the *same* address — there is no separate change chain | +| `account_index`, `xpub_keys`, `receive_address_index`, `change_address_index`, `ecdsa` | `None` for the indices; `ecdsa` reflects the constructor flag | + +`accounts_create_new_address` raises on a keypair account — there is no +next address to derive. + +## When to use a keypair account + +- You generated a key with the standalone `PrivateKey` API or imported one + from another tool, and want it managed inside a wallet file. +- You want a single-purpose hot wallet — one address, no rotation. +- You're testing and a single deterministic address is easier to reason + about than an HD chain. + +For everyday wallets — anything user-facing or anything where address +rotation matters — use a [BIP32 account](accounts.md) instead. + +## Import vs. create + +`accounts_import_keypair` is the variant for an existing key that may +already have on-chain history. The address-discovery scan path is a no-op +(there's only one address), so it's effectively the same as +`accounts_create_keypair` — use whichever reads better at the call site. + +## Where to next + +- [Accounts](accounts.md) — BIP32 accounts. +- [Send Transaction](send-transaction.md) — sending from a keypair account + works the same as from a BIP32 account. diff --git a/docs/learn/wallet/lifecycle.md b/docs/learn/wallet/lifecycle.md new file mode 100644 index 00000000..337b6d37 --- /dev/null +++ b/docs/learn/wallet/lifecycle.md @@ -0,0 +1,64 @@ +# Lifecycle + +A `Wallet` moves through five states. Each transition is async and ordered +— calling them out of order raises. + +```mermaid +stateDiagram-v2 + [*] --> Constructed: Wallet(...) + Constructed --> Started: start() + Started --> Connected: connect() + Connected --> Open: wallet_create() / wallet_open() + Open --> Active: accounts_activate() + Active --> Open: wallet_close() + Open --> Started: disconnect() + Started --> [*]: stop() +``` + +## Transitions + +| Step | Method | Effect | +| --- | --- | --- | +| Construct | `Wallet(network_id, encoding, url, resolver)` | Builds the local file store and an internal wRPC client. No I/O. | +| Start | `await wallet.start()` | Boots the `UtxoProcessor`, the wRPC notifier, and the event-dispatch task. | +| Connect | `await wallet.connect(...)` | Connects the wRPC client to a node (via `resolver` or explicit `url`). | +| Open | `await wallet.wallet_create(...)` / `wallet_open(...)` | Decrypts and loads a wallet file; secrets become available in memory. | +| Activate | `await wallet.accounts_activate([ids])` | Begins UTXO tracking and event emission for the chosen accounts. | +| Close | `await wallet.wallet_close()` | Releases the open wallet; activated accounts stop tracking. | +| Disconnect | `await wallet.disconnect()` | Drops the wRPC connection. The wallet remains started. | +| Stop | `await wallet.stop()` | Tears down the runtime and event task. | + +## Ordering rules + +!!! warning "Preconditions" + - `start()` must precede `connect()`, `wallet_create()`, and `wallet_open()`. + - `wallet_create()` / `wallet_open()` may be called before or after + `connect()`, but `accounts_activate()` requires the wRPC client to be + connected *and* the wallet to be synced (see [Start](start.md)). + - `set_network_id()` raises if the wRPC client is currently connected + — `disconnect()` first, change the network, then `connect()` again. + - `wallet_close()` does not stop the runtime; pair it with `stop()` on + shutdown. + +## Properties + +| Property | Type | Meaning | +| --- | --- | --- | +| `wallet.rpc` | `RpcClient` | The underlying wRPC client. Use it for direct node calls. | +| `wallet.is_open` | `bool` | `True` between `wallet_open` / `wallet_create` and `wallet_close`. | +| `wallet.is_synced` | `bool` | `True` once the `UtxoProcessor` has caught up. See [Start](start.md). | +| `wallet.descriptor` | `WalletDescriptor \| None` | Metadata for the open wallet, or `None` when closed. | + +## Reload without re-reading + +`wallet_reload(reactivate)` reboots the account runtime using cached wallet +data — no disk I/O. Pass `reactivate=True` to resume previously active +accounts; pass `False` if you intend to call `accounts_activate` yourself. +A `WalletReload` event fires either way. + +## Where to next + +- [Initialize](initialize.md), [Start](start.md), [Open](open.md) — the + three phases of bringing a wallet up, in order. +- [Architecture](architecture.md) — what `start` / `connect` / `activate` + actually wire up. diff --git a/docs/learn/wallet/open.md b/docs/learn/wallet/open.md new file mode 100644 index 00000000..ee1bdc7b --- /dev/null +++ b/docs/learn/wallet/open.md @@ -0,0 +1,136 @@ +# Open + +A wallet file is a single encrypted file on disk. Only one is open at a +time per `Wallet` instance. This page covers the file-management surface: +create, open, enumerate, export/import, change-secret, rename. + +## Surface + +| Method | Purpose | +| --- | --- | +| `wallet_enumerate()` | List every wallet file in the store. | +| `wallet_create(...)` | Create a new encrypted file and open it. | +| `wallet_open(...)` | Decrypt and open an existing file. | +| `wallet_close()` | Release the open file; secrets leave memory. | +| `wallet_export(...)` | Dump the encrypted payload as hex. | +| `wallet_import(...)` | Materialise a previously exported payload as a new file. | +| `wallet_change_secret(...)` | Re-encrypt the open file with a new password. | +| `wallet_rename(...)` | Update the title (and/or filename — see warning below). | + +All methods require `start()`; none require a wRPC connection. + +## Create + +```python +created = await wallet.wallet_create( + wallet_secret="example-secret", + filename="demo", + overwrite_wallet_storage=False, + title="demo", + user_hint="example", +) +``` + +- `filename` is the on-disk basename; omit for the SDK default. +- `overwrite_wallet_storage=False` raises `WalletAlreadyExistsError` if + the file exists; pass `True` to clobber. +- `user_hint` is stored alongside the file as a recoverable password hint. + +A freshly created wallet has no private key data and no accounts — see +[Private Keys](private-keys.md) and [Accounts](accounts.md). + +## Open + +```python +opened = await wallet.wallet_open( + wallet_secret="example-secret", + account_descriptors=True, # include account list in the response + filename="demo", +) +``` + +`account_descriptors=True` returns the account list in the response dict +so you can pick which to activate without a follow-up +`accounts_enumerate()`. + +## Create-or-open pattern + +`wallet_create` raises `WalletAlreadyExistsError` when the file exists. +The canonical idempotent boot is: + +```python +from kaspa.exceptions import WalletAlreadyExistsError + +try: + await wallet.wallet_create( + wallet_secret=secret, filename="demo", overwrite_wallet_storage=False, + ) +except WalletAlreadyExistsError: + await wallet.wallet_open(secret, True, "demo") +``` + +## Enumerate + +```python +descriptors = await wallet.wallet_enumerate() +for d in descriptors: + print(d.filename, d.title) +``` + +Returns a `list[WalletDescriptor]`. Available before any wallet is opened — +useful for showing the user a wallet picker. + +## Export & import + +```python +hex_payload = await wallet.wallet_export( + wallet_secret="example-secret", + include_transactions=True, +) +# ...transfer hex_payload to another machine, then: +imported = await wallet.wallet_import( + wallet_secret="example-secret", + wallet_data=hex_payload, +) +new_filename = imported["walletDescriptor"]["filename"] +await wallet.wallet_open("example-secret", True, new_filename) +``` + +The exported payload is borsh-serialized and remains encrypted with +`wallet_secret`; private key material never leaves memory in the clear. +`wallet_import` writes a new file in the store and returns its descriptor — +you still need to `wallet_open` it. + +## Change secret + +```python +await wallet.wallet_change_secret( + old_wallet_secret="example-secret", + new_wallet_secret="new-secret", +) +``` + +Re-encrypts the open file in place. The wallet stays open under the new +secret; future `wallet_open` calls must use the new password. + +## Rename + +```python +await wallet.wallet_rename( + wallet_secret="example-secret", + title="renamed wallet", +) +``` + +!!! warning "Rename `title` only — `filename` is broken upstream" + Passing `filename` triggers an upstream rusty-kaspa bug: the new path + is resolved relative to the process cwd (not the wallet store folder) + and the `.wallet` extension is not appended. The renamed file ends up + at `./` and the in-memory store starts pointing at that bare + path. Until fixed upstream, leave `filename=None`. + +## Where to next + +- [Private Keys](private-keys.md) — the next step after creating a wallet. +- [Accounts](accounts.md) — derive accounts from stored key data. +- [Lifecycle](lifecycle.md) — how these calls fit into start/stop. diff --git a/docs/learn/wallet/private-keys.md b/docs/learn/wallet/private-keys.md new file mode 100644 index 00000000..8a91f414 --- /dev/null +++ b/docs/learn/wallet/private-keys.md @@ -0,0 +1,86 @@ +# Private Keys + +A *private key data* (PKD) entry is the encrypted secret that backs one or +more accounts. A wallet file holds zero or more PKDs; each account +references exactly one by `PrvKeyDataId`. + +## Variants + +`PrvKeyDataVariantKind` selects the format of `secret` passed to +`prv_key_data_create`: + +| Variant | `secret` format | Typical source | +| --- | --- | --- | +| `Mnemonic` | BIP-39 phrase (12 or 24 words) | New wallets, `Mnemonic.random(...)` | +| `Bip39Seed` | Hex-encoded BIP-39 seed | Pre-derived seeds from another tool | +| `ExtendedPrivateKey` | xprv string | Migrating an existing HD wallet | +| `SecretKey` | 64-char hex secp256k1 key | Single-key (keypair) accounts | + +## Surface + +| Method | Purpose | +| --- | --- | +| `prv_key_data_create(...)` | Encrypt and store a new PKD; returns its `PrvKeyDataId`. | +| `prv_key_data_enumerate()` | List `PrvKeyDataInfo` for every stored PKD. | +| `prv_key_data_get(secret, id)` | Fetch metadata for a single PKD. | + +The wallet must be open. The actual secret never leaves the wallet — only +its metadata is returned. + +## Create + +```python +from kaspa import PrvKeyDataVariantKind + +prv_key_id = await wallet.prv_key_data_create( + wallet_secret="example-secret", + secret="", + kind=PrvKeyDataVariantKind.Mnemonic, + payment_secret=None, # optional second factor + name="demo-key", +) +``` + +`payment_secret`, when set, layers a second password on top of +`wallet_secret`. Every operation that decrypts this PKD (account creation, +signing, export) must supply it. Use `None` for single-password wallets. + +## Enumerate & inspect + +```python +for info in await wallet.prv_key_data_enumerate(): + print(info.id, info.name, info.is_encrypted) +``` + +`PrvKeyDataInfo` exposes: + +- `id: PrvKeyDataId` — stable identifier for account creation. +- `name: str | None` — the label set at creation time. +- `is_encrypted: bool` — `True` if a `payment_secret` is required. + +`prv_key_data_get(wallet_secret, id)` returns the same metadata for one +entry, raising if the id is unknown. + +## Using a PKD + +`PrvKeyDataId` is the link between a PKD and the accounts derived from it: + +```python +descriptor = await wallet.accounts_create_bip32( + wallet_secret="example-secret", + prv_key_data_id=prv_key_id, + account_index=0, +) +``` + +A single PKD can back many accounts — common for BIP32 wallets where +multiple account indices share one mnemonic. See [Accounts](accounts.md) +for the account-creation surface. + +## Where to next + +- [Accounts](accounts.md) — derive BIP32 accounts from a PKD. +- [Keypair Accounts](keypair.md) — single-key accounts from `SecretKey` + PKDs. +- [Wallet Recovery](../../guides/wallet-recovery.md) — BIP-44 scan for + accounts already used under a mnemonic. diff --git a/docs/learn/wallet/send-transaction.md b/docs/learn/wallet/send-transaction.md new file mode 100644 index 00000000..4519d24c --- /dev/null +++ b/docs/learn/wallet/send-transaction.md @@ -0,0 +1,128 @@ +# Send Transaction + +Outgoing flows from an activated account. Every method on this page +requires the wallet to be open, the wRPC client connected, the source +account activated, and `wallet.is_synced` to be `True` — see +[Start](start.md). + +## Surface + +| Method | Purpose | +| --- | --- | +| `accounts_estimate(...)` | Dry-run a send; returns a `GeneratorSummary` without submitting. | +| `accounts_send(...)` | Sign and submit a send. Returns the same `GeneratorSummary` after submission. | +| `accounts_transfer(...)` | Internal transfer between two accounts in the same wallet. | +| `accounts_get_utxos(...)` | Snapshot of an account's tracked UTXOs (post-sync). | +| `fee_rate_estimate()` | Current low / normal / priority fee rates from the node. | +| `fee_rate_poller_enable(seconds)` / `_disable()` | Background fee-rate refresh. | + +For sweeping (consolidating every UTXO), see [Sweep Funds](sweep.md). + +## Send a single output + +```python +from kaspa import Fees, FeeSource, PaymentOutput + +result = await wallet.accounts_send( + wallet_secret="example-secret", + account_id=account.account_id, + priority_fee_sompi=Fees(0, FeeSource.SenderPays), + destination=[PaymentOutput("kaspatest:...", 100_000_000)], # 1 KAS +) +print(result.final_transaction_id, result.fees, result.final_amount) +``` + +## Multi-output send + +A single `destination` list with N outputs becomes one transaction with +N + 1 outputs (the +1 is the change return). + +```python +outputs = [PaymentOutput(addr, 100_000_000) for addr in addresses] +result = await wallet.accounts_send( + wallet_secret=secret, + account_id=account.account_id, + priority_fee_sompi=Fees(0, FeeSource.SenderPays), + destination=outputs, +) +``` + +## Estimate before sending + +```python +estimate = await wallet.accounts_estimate( + account_id=account.account_id, + priority_fee_sompi=Fees(0, FeeSource.SenderPays), + destination=outputs, +) +print(estimate.fees, estimate.final_amount, estimate.aggregated_utxos) +``` + +`accounts_estimate` and `accounts_send` take the same arguments. +Estimating first is cheap — it surfaces fees and UTXO selection before +signing. + +## Fees + +`priority_fee_sompi` is a `Fees(amount, FeeSource)` (or equivalent dict): + +- **`FeeSource.SenderPays`** — fee is added on top of the destination + amount. Used in normal sends. +- **`FeeSource.ReceiverPays`** — fee is deducted from the destination + amount. Used to sweep an exact balance with no leftover change (see + [Sweep Funds](sweep.md)). + +`fee_rate` overrides the resolved sompi-per-gram rate explicitly. Leave +it `None` to let the wallet pick the network-suggested rate. + +```python +rates = await wallet.fee_rate_estimate() +# {"low": ..., "normal": ..., "priority": ...} +``` + +For latency-sensitive flows, run a background poller: + +```python +wallet.fee_rate_poller_enable(15) # refresh every 15 seconds +# ... later ... +wallet.fee_rate_poller_disable() +``` + +## Internal transfers + +Funds moved between two accounts in the **same wallet** are immediately +spendable on transaction acceptance — no maturity wait: + +```python +await wallet.accounts_transfer( + wallet_secret=secret, + source_account_id=src.account_id, + destination_account_id=dst.account_id, + transfer_amount_sompi=500_000_000, +) +``` + +Use `accounts_transfer` for in-wallet movement; use `accounts_send` for +external addresses. + +## Waiting for funds and confirmations + +Sends submit immediately, but spent UTXOs need to mature before the next +`accounts_send` will see them. Two correct waits, both via +[Transaction History](transaction-history.md): + +- **Pending** fires when a UTXO lands but isn't spendable yet — useful + for UI. +- **Maturity** fires when a UTXO crosses the maturity depth and is + spendable. This is the right gate for "send → wait → send again" flows. + +Polling `accounts_get_utxos` works for one-shot scripts, but a `Maturity` +listener is the production pattern. + +## Where to next + +- [Sweep Funds](sweep.md) — consolidating every UTXO. +- [Transaction History](transaction-history.md) — `Pending`, `Maturity`, + and listener registration. +- [Wallet SDK → Transaction Generator](../wallet-sdk/tx-generator.md) — + the lower-level primitive `accounts_send` is built on. diff --git a/docs/learn/wallet/start.md b/docs/learn/wallet/start.md new file mode 100644 index 00000000..f31379ec --- /dev/null +++ b/docs/learn/wallet/start.md @@ -0,0 +1,107 @@ +# Start + +`start()` boots the wallet's runtime: the `UtxoProcessor`, the wRPC +notifier task, and the event-dispatch loop. `connect()` then attaches the +wRPC client to the node. After both, the wallet is ready to *open a file* +— but not yet ready to do anything that touches UTXO state. + +## Boot sequence + +```python +wallet = Wallet(network_id="testnet-10", resolver=Resolver()) +await wallet.start() +await wallet.connect() +``` + +Both calls are required. `start()` without `connect()` leaves the runtime +running but unable to talk to the node; `connect()` without `start()` +raises. + +## Connect options + +`connect()` takes the same options as `RpcClient.connect`: + +```python +await wallet.connect( + block_async_connect=True, # await readiness before returning + strategy="retry", # "retry" or "fallback" + url=None, # override the resolver-discovered URL + timeout_duration=10_000, # ms + retry_interval=1_000, # ms +) +``` + +If you constructed with a `Resolver`, omit `url` and let the resolver pick +a public node. Pass `url=` to override for one connection (useful for +pinning to a specific node temporarily). + +## The sync gate + +`connect()` resolves as soon as the WebSocket is up — **not** when the +wallet's UTXO processor has caught up. UTXO-dependent state +(`AccountDescriptor.balance`, `accounts_get_utxos`, `accounts_send`) is +unusable in this gap. + +```python +await wallet.connect(...) + +while not wallet.is_synced: + await asyncio.sleep(0.5) +``` + +After this loop, `accounts_activate` actually attaches a working +`UtxoContext` and node notifications start populating balances and UTXOs. + +## Why the gate is necessary + +Before `is_synced` flips to `True`: + +- The `UtxoProcessor` is not driving per-account `UtxoContext`s. +- `accounts_activate` is a no-op for UTXO discovery — accounts are + registered but the processor isn't pulling state. +- Notifications from the node are buffered or ignored. + +So `accounts_get_utxos` returns `[]` and `AccountDescriptor.balance` is +`None` — not because the address is unfunded, but because the wallet +hasn't started tracking it. See [Architecture](architecture.md). + +## Event-driven wait + +If you've registered a listener (see +[Transaction History](transaction-history.md)), the `SyncState` event +reports progress and `is_synced` flips at the end: + +```python +import asyncio +from kaspa import WalletEventType + +ready = asyncio.Event() + +async def on_event(event): + if event["type"] == WalletEventType.SyncState.name and wallet.is_synced: + ready.set() + +wallet.add_event_listener(WalletEventType.All, on_event) +await wallet.connect(...) +await ready.wait() +``` + +Use this when you want a UI progress indicator alongside the gate. +Polling is fine for scripts. + +## Shutdown + +```python +await wallet.disconnect() # drop the wRPC link; runtime stays alive +await wallet.stop() # tear down the runtime and event task +``` + +Skipping `stop()` leaks the notification task. Skipping `disconnect()` +keeps the WebSocket open. + +## Where to next + +- [Open](open.md) — create or open a wallet file. +- [Architecture](architecture.md) — what `start` actually wires up. +- [Transaction History](transaction-history.md) — `SyncState` and the + rest of the event taxonomy. diff --git a/docs/learn/wallet/sweep.md b/docs/learn/wallet/sweep.md new file mode 100644 index 00000000..c25bdbbe --- /dev/null +++ b/docs/learn/wallet/sweep.md @@ -0,0 +1,74 @@ +# Sweep Funds + +A sweep consolidates every UTXO in an account into one address. There are +two patterns, and the difference between them is whether you want any +leftover change. + +## Pattern 1: sweep to your own change address + +Omit `destination` entirely. The wallet routes the full sweepable balance +to the account's current change address and the resulting transaction has +no external recipient — useful for collapsing a long UTXO history into a +single mature output before a high-volume sending flow. + +```python +from kaspa import Fees, FeeSource + +await wallet.accounts_send( + wallet_secret=secret, + account_id=account.account_id, + priority_fee_sompi=Fees(0, FeeSource.SenderPays), + destination=None, +) +``` + +`SenderPays` is fine here because the change return absorbs the fee — +there's no external recipient to "subtract from." + +## Pattern 2: sweep an exact balance to a fresh address + +When you want to leave the account at zero and the destination receives +*the exact aggregate balance minus fees*, use `ReceiverPays`: + +```python +from kaspa import Fees, FeeSource, PaymentOutput + +utxos = await wallet.accounts_get_utxos(account_id=account.account_id) +total = sum(u["amount"] for u in utxos) + +await wallet.accounts_send( + wallet_secret=secret, + account_id=account.account_id, + priority_fee_sompi=Fees(0, FeeSource.ReceiverPays), + destination=[PaymentOutput(sweep_address, total)], +) +``` + +The destination amount is the *gross* balance; `ReceiverPays` deducts the +network fee from that amount before broadcasting. The result: no +change output, no dust left behind. + +## Which to use + +| You want… | Use | +| --- | --- | +| To consolidate UTXOs and keep them in this account | Pattern 1 (no `destination`) | +| To move every sompi to an external address, leaving zero | Pattern 2 (`ReceiverPays`) | +| To sweep within the same wallet to a different account | [`accounts_transfer`](send-transaction.md#internal-transfers), not a sweep | + +## Big sweeps come back as multiple transactions + +If the input set is too large for one transaction's mass budget, the +underlying `Generator` produces a series of transactions: each one +consolidates some UTXOs into a single intermediate output, and the final +transaction sends that aggregate to the destination. `accounts_send` +returns the final `GeneratorSummary`. Watch the +[`Maturity` event](transaction-history.md) to know when the chain has +caught up — only the final output is what you'd hand off downstream. + +## Where to next + +- [Send Transaction](send-transaction.md) — non-sweep sends and fee + modes. +- [Transaction History](transaction-history.md) — gating "wait until + swept" on `Maturity`. diff --git a/docs/learn/wallet/transaction-history.md b/docs/learn/wallet/transaction-history.md new file mode 100644 index 00000000..4241bb87 --- /dev/null +++ b/docs/learn/wallet/transaction-history.md @@ -0,0 +1,122 @@ +# Transaction History + +The wallet emits events for every state change the node pushes through. You +register Python callbacks on the wallet, the wallet's event multiplexer +forwards relevant events to them, and you get to react: update a UI, +trigger the next send, log a maturity, ignore a re-org. The history APIs +(`transactions_data_get` and friends) round this out for the +"what happened in this account?" question. + +## Listener API + +```python +def add_event_listener(event, callback, *args, **kwargs) -> None +def remove_event_listener(event, callback=None) -> None +``` + +- `event`: a `WalletEventType`, its lowercase string name (`"balance"`, + `"connect"`), or `"all"` / `WalletEventType.All` for every event. +- `callback`: invoked as `callback(event, *args, **kwargs)`. Sync or async. +- `args` / `kwargs`: forwarded verbatim to every invocation — handy for + routing context (account id, channel) without closures. +- `remove_event_listener(event)` with no callback clears every listener + for that event. With `"all"` and no callback, clears every listener. + +## A minimal subscriber + +```python +import asyncio +from kaspa import Resolver, Wallet, WalletEventType + +async def on_event(event): + print(event["type"], event.get("data")) + +wallet = Wallet(network_id="testnet-10", resolver=Resolver()) +wallet.add_event_listener(WalletEventType.All, on_event) + +await wallet.start() +await wallet.connect() +# ... events stream in for the rest of the session ... +``` + +Each event is a dict with at least a `type` key (the `WalletEventType` +name as a string) and an optional `data` payload specific to that event. + +## Event taxonomy + +| Group | Events | +| --- | --- | +| Connection | `Connect`, `Disconnect`, `ServerStatus`, `UtxoIndexNotEnabled` | +| Wallet file | `WalletList`, `WalletStart`, `WalletHint`, `WalletOpen`, `WalletCreate`, `WalletReload`, `WalletClose`, `WalletError` | +| Key & account state | `PrvKeyDataCreate`, `AccountCreate`, `AccountActivation`, `AccountDeactivation`, `AccountSelection`, `AccountUpdate` | +| Sync & runtime | `SyncState`, `UtxoProcStart`, `UtxoProcStop`, `UtxoProcError`, `DaaScoreChange`, `Metrics`, `FeeRate` | +| UTXO movement | `Pending`, `Maturity`, `Reorg`, `Stasis`, `Discovery`, `Balance` | +| Catch-all | `All`, `Error` | + +The most common subscriptions: + +- **`SyncState`** — progress while the `UtxoProcessor` catches up. Pair + with `wallet.is_synced` (see [Start](start.md)). +- **`Balance`** — fires whenever a `UtxoContext` balance changes. The + right signal for live UI updates. +- **`Pending`** — a new UTXO landed for a tracked address but isn't yet + spendable. +- **`Maturity`** — a previously-pending UTXO has reached the maturity + depth and is now spendable. The strongest gate for "send-then-wait" + flows: don't trigger the next `accounts_send` on `Pending` alone. +- **`Reorg`** / **`Stasis`** — a UTXO was unwound or coinbase-locked. + Defensive code for high-value flows. +- **`AccountActivation`** / **`AccountDeactivation`** — react to + `accounts_activate` / `wallet_close`. + +## Targeted subscriptions + +```python +wallet.add_event_listener("balance", on_balance) +wallet.add_event_listener("maturity", on_maturity) +wallet.add_event_listener(WalletEventType.SyncState, on_sync) +``` + +To pass context to a generic callback: + +```python +wallet.add_event_listener("balance", on_change, account.account_id, label="primary") +# callback receives: on_change(event, account.account_id, label="primary") +``` + +## History queries + +For the "what happened" view rather than the live stream: + +```python +data = await wallet.transactions_data_get( + account_id=account.account_id, + network_id="testnet-10", + start=0, + end=20, +) +# Annotate / re-annotate: +await wallet.transactions_replace_note(account.account_id, tx_id, "rent") +await wallet.transactions_replace_metadata(account.account_id, tx_id, {"tag": "ops"}) +``` + +The note and metadata fields are wallet-local — they live in the wallet +file, not on chain. + +## Cleanup + +Listeners outlive the wallet's open file but not its runtime. Always pair +a permanent registration with an explicit removal on shutdown, or use +`"all"` to clear in one call: + +```python +wallet.remove_event_listener(WalletEventType.All) +await wallet.stop() +``` + +## Where to next + +- [Send Transaction](send-transaction.md) — `Maturity` as the right wait + condition. +- [Architecture](architecture.md) — what's actually generating these events. +- [Lifecycle](lifecycle.md) — when each event group fires. From 73f99e89d113c30e95ce0bf3e8e054d61ab62f51 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Thu, 30 Apr 2026 18:07:01 -0400 Subject: [PATCH 2/7] docs learn section refinement --- docs/index.md | 2 +- docs/learn/addresses.md | 2 +- docs/learn/concepts.md | 2 +- docs/learn/index.md | 8 +- docs/learn/rpc/calls.md | 125 +++++++++--- docs/learn/rpc/connecting.md | 91 +++++---- docs/learn/rpc/index.md | 43 ---- docs/learn/rpc/overview.md | 47 +++++ docs/learn/rpc/resolver.md | 102 +++++++--- docs/learn/rpc/subscriptions.md | 191 ++++++++++++++---- .../transactions/{index.md => overview.md} | 0 .../wallet-sdk/{index.md => overview.md} | 4 +- docs/learn/wallet-sdk/utxo-context.md | 2 +- docs/learn/wallet-sdk/utxo-processor.md | 2 +- docs/learn/wallet/{index.md => overview.md} | 4 +- mkdocs.yml | 53 ++++- python/kaspa/__init__.pyi | 164 ++++++++++++++- 17 files changed, 646 insertions(+), 196 deletions(-) delete mode 100644 docs/learn/rpc/index.md create mode 100644 docs/learn/rpc/overview.md rename docs/learn/transactions/{index.md => overview.md} (100%) rename docs/learn/wallet-sdk/{index.md => overview.md} (94%) rename docs/learn/wallet/{index.md => overview.md} (96%) diff --git a/docs/index.md b/docs/index.md index 3362fee3..1022dbb6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -60,7 +60,7 @@ section answers a different question: ## Where to start - **New to the SDK:** [Installation](getting-started/installation.md) → - [Learn → RPC](learn/rpc/index.md) → + [Learn → RPC](learn/rpc/overview.md) → [Learn → Wallet SDK → Key Management](learn/wallet-sdk/key-management.md). - **Looking for a recipe:** jump to [Guides](guides/mnemonics.md). - **Looking up an API:** [API Reference](reference/index.md). diff --git a/docs/learn/addresses.md b/docs/learn/addresses.md index 2506ee47..68f989c6 100644 --- a/docs/learn/addresses.md +++ b/docs/learn/addresses.md @@ -138,7 +138,7 @@ multiple cosigners, submitting), see the ## Where to next - [Networks](networks.md) — what each prefix means. -- [Transactions](transactions/index.md) — using addresses inside transaction +- [Transactions](transactions/overview.md) — using addresses inside transaction outputs. - [Wallet SDK → Derivation](wallet-sdk/derivation.md) — deriving many addresses from one key. diff --git a/docs/learn/concepts.md b/docs/learn/concepts.md index 9b474423..9bcefac0 100644 --- a/docs/learn/concepts.md +++ b/docs/learn/concepts.md @@ -108,7 +108,7 @@ field exists for protocol extensions; you'll usually leave ## Where to next - [Networks](networks.md) — picking a chain to talk to. -- [Addresses](addresses.md) and [Transactions](transactions/index.md) — the +- [Addresses](addresses.md) and [Transactions](transactions/overview.md) — the on-chain primitives in Python. - [Wallet → Architecture](wallet/architecture.md) — how the SDK turns these concepts into a wallet you can actually use. diff --git a/docs/learn/index.md b/docs/learn/index.md index 1e960507..82e31223 100644 --- a/docs/learn/index.md +++ b/docs/learn/index.md @@ -1,15 +1,15 @@ # Learn -- **[RPC](rpc/index.md)** — the `RpcClient`: resolver, connection, calls, +- **[RPC](rpc/overview.md)** — the `RpcClient`: resolver, connection, calls, notifications. -- **[Wallet](wallet/index.md)** — the managed high-level `Wallet` API: lifecycle, file +- **[Wallet](wallet/overview.md)** — the managed high-level `Wallet` API: lifecycle, file storage, accounts, addresses, sending, history. -- **[Wallet SDK](wallet-sdk/index.md)** — the lower-level primitives that the +- **[Wallet SDK](wallet-sdk/overview.md)** — the lower-level primitives that the managed `Wallet` is built on: key management, transaction `Generator`, derivation, `UtxoContext`, `UtxoProcessor`, etc. - **[Networks](networks.md)** - working with the various Kaspa networks (mainnet, testnets, etc.) from this SDK. - **[Addresses](addresses.md)** - a quick primer on Kaspa addresses and handling in this SDK. -- **[Transactions](transactions/index.md)** — the on-chain primitives: +- **[Transactions](transactions/overview.md)** — the on-chain primitives: inputs, outputs, mass and fees, signing, submission, metadata fields, and serialization. - **[Kaspa Concepts](concepts.md)** — explanation of the BlockDAG, UTXO diff --git a/docs/learn/rpc/calls.md b/docs/learn/rpc/calls.md index 5ca10c8b..f2a93881 100644 --- a/docs/learn/rpc/calls.md +++ b/docs/learn/rpc/calls.md @@ -1,27 +1,24 @@ # Calls -Once connected, every RPC method is `await client.(...)`. Most take -either no arguments or a single dict; all return a dict (or list of dicts) -shaped like the rusty-kaspa wire protocol. +Once connected, every RPC method is `await client.(...)`. Most +take no arguments or a single dict, and return a dict (or list of +dicts) shaped like the rusty-kaspa wire protocol. -This page is a tour of the surface, grouped by what you're trying to do. -For full request/response shapes use the [API Reference](../../reference/index.md). +This page is a brief tour of the available RPC calls, grouped by category. This is not an +exhaustive reference. For every call and its request/response model, +see [`RpcClient`](../../reference/Classes/RpcClient.md) in the API +Reference. ## Network information ```python info = await client.get_info() dag_info = await client.get_block_dag_info() -count = await client.get_block_count() # blockCount, headerCount -supply = await client.get_coin_supply() # circulatingSompi, maxSompi +count = await client.get_block_count() +supply = await client.get_coin_supply() network = await client.get_current_network() -sync = await client.get_sync_status() # {"isSynced": bool} ``` -`get_block_dag_info` is the closest the SDK has to a "where is the chain -right now" call: it returns network name, block count, virtual DAA score, -tip hashes, and pruning point in one go. - ## Balances and UTXOs ```python @@ -35,9 +32,11 @@ for entry in utxos.get("entries", []): print(entry["outpoint"], entry["utxoEntry"]["amount"]) ``` -`get_utxos_by_addresses` is the go-to call when you need a one-shot UTXO -snapshot. For *continuous* UTXO tracking, subscribe instead — see -[Subscriptions](subscriptions.md). +Use `get_utxos_by_addresses` for one-shot queries or polling. For +continuous tracking, subscribe to +[`utxos-changed`](subscriptions.md#available-events), or use +[`UtxoContext`](../wallet-sdk/utxo-context.md) for per-address tracking +on top of that subscription. ## Blocks @@ -54,23 +53,88 @@ template = await client.get_block_template({ }) ``` +## Virtual chain + +Walk the selected-parent chain forward from a known block. Useful for +indexers and confirmation tracking — every accepted transaction id +passes through this stream. + +```python +chain = await client.get_virtual_chain_from_block({ + "startHash": "...", + "includeAcceptedTransactionIds": True, + "minConfirmationCount": 10, # optional; skip blocks with fewer confirmations +}) + +for h in chain["addedChainBlockHashes"]: + print("+", h) +for h in chain["removedChainBlockHashes"]: + print("-", h) +for entry in chain["acceptedTransactionIds"]: + print(entry["acceptingBlockHash"], entry["acceptedTransactionIds"]) +``` + +`addedChainBlockHashes` and `removedChainBlockHashes` give the chain +delta from `startHash` to the current sink. With +`includeAcceptedTransactionIds=True`, each entry maps an accepting +block hash to the transaction ids it accepted. + +For a streaming version, subscribe to +[`virtual-chain-changed`](subscriptions.md#virtual-chain-progression). + +### `get_virtual_chain_from_block_v2` + +V2 swaps the boolean flag for a verbosity level and returns richer +per-block data: + +```python +chain = await client.get_virtual_chain_from_block_v2({ + "startHash": "...", + "dataVerbosityLevel": "Low", # "None" | "Low" | "High" | "Full" + "minConfirmationCount": 10, # optional +}) + +for entry in chain["chainBlockAcceptedTransactions"]: + header = entry.get("chainBlockHeader") # populated at higher verbosity + txs = entry.get("acceptedTransactions", []) + print(len(txs), "txs accepted under header:", header) +``` + +Verbosity levels: + +- `"None"` — chain delta only (`addedChainBlockHashes`, + `removedChainBlockHashes`). +- `"Low"` — adds accepted-transaction ids per chain block. +- `"High"` — adds the chain block header and per-transaction metadata. +- `"Full"` — full headers and full transactions. + +Pick the lowest level that meets your needs; higher verbosity costs +bandwidth and node CPU. + ## Transactions and mempool ```python result = await client.submit_transaction({ - "transaction": tx.serialize_to_dict(), + "transaction": signed_tx, # a Transaction instance "allowOrphan": False, }) mempool = await client.get_mempool_entries({ "includeOrphanPool": False, "filterTransactionPool": True, }) -entry = await client.get_mempool_entry({"transactionId": "..."}) +entry = await client.get_mempool_entry({ + "transactionId": "...", + "includeOrphanPool": False, + "filterTransactionPool": True, +}) ``` -If you have a `PendingTransaction` from the [Transaction -Generator](../wallet-sdk/tx-generator.md), prefer `pending_tx.submit(client)` — -it serialises and submits in one call. +If you have a +[`PendingTransaction`](../../reference/Classes/PendingTransaction.md) +from the [Transaction Generator](../wallet-sdk/tx-generator.md), +prefer `pending_tx.submit(client)` — it serialises and submits in one +call. See [Submission](../transactions/submission.md) for the full +flow and `allowOrphan` semantics. ## Fees @@ -81,6 +145,9 @@ fee = await client.get_fee_estimate() fee_x = await client.get_fee_estimate_experimental({"verbose": True}) ``` +See [Mass & Fees](../transactions/mass-and-fees.md) for how these +estimates feed into transaction construction. + ## Peers ```python @@ -92,13 +159,13 @@ await client.ban({"ip": "192.168.1.1"}) await client.unban({"ip": "192.168.1.1"}) ``` -These are administrative — you use them when you operate the node, not -when you're a client. +These are administrative — for node operators, not clients. ## System ```python await client.ping() +sync = await client.get_sync_status() server = await client.get_server_info() system = await client.get_system_info() metrics = await client.get_metrics({ @@ -113,7 +180,8 @@ metrics = await client.get_metrics({ ## Errors -A failing RPC call raises. Handle it like any other coroutine exception: +A failing RPC call raises. Handle it like any other coroutine +exception: ```python try: @@ -123,13 +191,14 @@ except Exception as exc: ``` Connection-level failures retry automatically (see -[Connecting](connecting.md)); the exception surface is for protocol-level -failures (invalid address, malformed request, node-side errors). +[Connecting](connecting.md#reconnects)). The exception surface is for +protocol-level failures: invalid address, malformed request, node-side +errors. ## Where to next - [Subscriptions](subscriptions.md) — server-pushed notifications. -- [Wallet → Send Transaction](../wallet/send-transaction.md) — the managed - Wallet wraps `submit_transaction` with sensible defaults. +- [Wallet → Send Transaction](../wallet/send-transaction.md) — the + managed Wallet wraps `submit_transaction` with sensible defaults. - [Wallet SDK → Transaction Generator](../wallet-sdk/tx-generator.md) — - build the transactions you submit through `submit_transaction`. + build the transactions you submit via `submit_transaction`. diff --git a/docs/learn/rpc/connecting.md b/docs/learn/rpc/connecting.md index 0a518cb6..8804bfab 100644 --- a/docs/learn/rpc/connecting.md +++ b/docs/learn/rpc/connecting.md @@ -1,10 +1,17 @@ # Connecting -`RpcClient.connect()` opens the WebSocket. `disconnect()` closes it. Between -those two calls, the client is "connected" — every RPC method is callable -and notifications stream in. +[`RpcClient.connect()`](../../reference/Classes/RpcClient.md#connect) +opens the WebSocket; +[`disconnect()`](../../reference/Classes/RpcClient.md#disconnect) +closes it. While connected, every RPC method is callable and +notifications stream in. -## A connected client, end to end +You can connect via the Public Node Network (PNN) using +[`Resolver`](resolver.md), or directly to a node URL. + +## Example + +Connecting to a PNN mainnet node via `Resolver`: ```python import asyncio @@ -13,79 +20,89 @@ from kaspa import Resolver, RpcClient async def main(): client = RpcClient(resolver=Resolver(), network_id="mainnet") await client.connect() - try: - info = await client.get_block_dag_info() - print(info["networkName"], info["blockCount"]) - finally: - await client.disconnect() + + info = await client.get_block_dag_info() + + await client.disconnect() asyncio.run(main()) ``` -The `try`/`finally` matters: a Python exception before `disconnect()` would -otherwise leave the socket open and the event loop holding a reference. - ## Connecting to a known node -Skip the resolver when you have a URL: +Pass a URL directly. See [Networks](../networks.md) for the canonical +wRPC ports per network: ```python client = RpcClient( - url="wss://node.example.com:17110", + url="ws://node.example.com:17110", network_id="mainnet", encoding="borsh", # or "json" ) ``` -Use this for nodes you operate, paid endpoints, or testnet runs against a -local `kaspad`. +## URL schemes + +- `ws://` — plaintext WebSocket +- `wss://` — TLS WebSocket ## Connection options -`connect()` accepts a handful of behavioural overrides: +`connect()` takes a few behavioural overrides: ```python await client.connect( - block_async_connect=True, # await until the socket is open (default True) - strategy="fallback", # "retry" or "fallback" — what to do if the first attempt fails - timeout_duration=30000, # per-attempt timeout, ms - retry_interval=1000, # delay between attempts, ms + block_async_connect=True, + strategy="fallback", + url="ws://node.example.com:17110", + timeout_duration=30000, + retry_interval=1000, ) ``` -The defaults are appropriate for production code. Lower `timeout_duration` -in fast-failure CI environments; raise `retry_interval` if you want to be -gentler on resolver back-ends during outages. +- `block_async_connect` — if `False`, `connect()` returns immediately + and the socket opens in the background. +- `strategy` — `"retry"` (default) loops until a connection succeeds; + `"fallback"` returns on the first failure. Applies to both URL-based + and `Resolver`-driven clients. +- `url` — overrides the constructor URL for this attempt only. Lets you + retarget a long-lived client without rebuilding it. +- `timeout_duration` — per-attempt ceiling, in milliseconds. +- `retry_interval` — delay between attempts, in milliseconds. ## Inspecting the live client ```python print(client.is_connected) # bool -print(client.url) # the resolved or supplied node URL +print(client.url) # resolved or supplied node URL, or None print(client.encoding) # "borsh" or "json" -print(client.node_id) # node-reported identifier +print(client.node_id) # resolver-supplied node UID; None for direct URLs print(client.resolver) # the Resolver instance, or None ``` -These are all property reads — no I/O, no `await`. - ## Encoding: Borsh vs JSON -`encoding="borsh"` is the default and the right choice. Borsh is the binary -format the node speaks natively; payloads are smaller and parsing is -faster. Pick `"json"` only when you need to inspect raw frames in a tool -that doesn't speak Borsh, or when the node you're targeting doesn't support -Borsh. +Borsh (the default) is a compact binary format used natively by the +node. + +Use `"json"` only to inspect raw frames in a tool that doesn't speak +Borsh, or when targeting a node that doesn't support it. See the +[`Encoding`](../../reference/Enums/Encoding.md) enum for accepted +values. ## Reconnects -If the WebSocket drops mid-session, the client reconnects on its own. Calls -made *during* the gap raise; calls made *after* the reconnect succeed. -There is no opt-out — if you need to know about disruptions, listen for the -relevant connection notifications (see [Subscriptions](subscriptions.md)). +If the WebSocket drops mid-session, the client reconnects on its own. +Calls made *during* the gap raise; calls made *after* a successful +reconnect work normally. To stop reconnect attempts, call +`disconnect()` — or use `strategy="fallback"`, which gives up after +one failed reconnect instead of looping. To track disruptions, listen +for the `connect` and `disconnect` events +(see [Subscriptions](subscriptions.md#available-events)). ## Where to next +- [Resolver](resolver.md) — node discovery details. - [Calls](calls.md) — what to do once `is_connected` is `True`. - [Subscriptions](subscriptions.md) — real-time notifications, including connection-state events. diff --git a/docs/learn/rpc/index.md b/docs/learn/rpc/index.md deleted file mode 100644 index e7ba86d1..00000000 --- a/docs/learn/rpc/index.md +++ /dev/null @@ -1,43 +0,0 @@ -# RPC - -The `RpcClient` provides a connection to a Kaspa node, allowing you to interact via Kaspa's RPC calls. This includes query methods, submission methods (e.g. submitting a transaction), and event subscriptions. - -Other layers in the SDK depend on RPC Client. For example, the `Wallet` class submits transactions and monitors UTXO state via RPC. - -## Overview - -A persistent, asynchronous WebSocket client. Each instance: - -- Connects to one node at a time. -- Is async-first - every method is a coroutine. -- Handles connection state - when a connection drops the client attempts to reconnect automatically. -- Encodes in Borsh by default - Borsh is more compact and faster to parse than JSON. Pass `encoding="json"` if you need the JSON wire format. - -## Two ways to point a client at a node - -```python -# Resolver: let the SDK pick a public node for the network you want -client = RpcClient(resolver=Resolver(), network_id="mainnet") - -# Direct URL: a known node you control or trust -client = RpcClient(url="wss://node.example.com:17110", network_id="mainnet") -``` - -See [Resolver](resolver.md) for the discovery mechanism, and -[Connecting](connecting.md) for the connection lifecycle and options. - -## What you can do once connected - -- **[Calls](calls.md)** — request/response RPCs for network info, balances, - blocks, mempool, fee estimation, peer management, etc. -- **[Subscriptions](subscriptions.md)** — node-pushed notifications for - UTXO changes, new blocks, virtual chain updates, and DAA score - changes. Each subscription is paired with an event listener you - register on the client. - -## Where to next - -- [Connecting](connecting.md) — connect, disconnect, retries, encoding. -- [Resolver](resolver.md) — how node discovery works. -- [Calls](calls.md) — the RPC method catalog. -- [Subscriptions](subscriptions.md) — real-time notifications. diff --git a/docs/learn/rpc/overview.md b/docs/learn/rpc/overview.md new file mode 100644 index 00000000..f827e946 --- /dev/null +++ b/docs/learn/rpc/overview.md @@ -0,0 +1,47 @@ +# RPC + +Kaspa nodes expose an RPC API. This SDK wraps it in +[`RpcClient`](../../reference/Classes/RpcClient.md) — one class for +connection management, request/response calls, and event subscriptions. + +Higher-level SDK layers build on `RpcClient`. For example, +[`Wallet`](../wallet/overview.md) submits transactions and tracks UTXO +state through it. + +## Overview + +`RpcClient` is an async WebSocket client. Each instance: + +- Connects to one node at a time. +- Reconnects automatically if the socket drops. +- Uses Borsh encoding by default — compact and faster to parse than + JSON. Pass `encoding="json"` for the JSON wire format. See the + [`Encoding`](../../reference/Enums/Encoding.md) enum. + +## Two ways to point a client at a node + +```python +# Resolver: let the SDK pick a public node for the network you want +client = RpcClient(resolver=Resolver(), network_id="mainnet") + +# Direct URL: a known node you control or trust +client = RpcClient(url="wss://node.example.com:17110", network_id="mainnet") +``` + +See [Resolver](resolver.md) for node discovery and +[Connecting](connecting.md) for the connection lifecycle. + +## RPC methods + +- **[Calls](calls.md)** — request/response RPCs for network info, + balances, blocks, mempool, fees, and peers. +- **[Subscriptions](subscriptions.md)** — node-pushed notifications for + UTXO changes, new blocks, virtual chain updates, and DAA score + changes. Each subscription pairs with an event listener. + +## Where to next + +- [Connecting](connecting.md) — connect, disconnect, retries, encoding. +- [Resolver](resolver.md) — node discovery. +- [Calls](calls.md) — RPC method catalog. +- [Subscriptions](subscriptions.md) — real-time notifications. diff --git a/docs/learn/rpc/resolver.md b/docs/learn/rpc/resolver.md index 5c8d59da..cdf947a1 100644 --- a/docs/learn/rpc/resolver.md +++ b/docs/learn/rpc/resolver.md @@ -1,53 +1,93 @@ # Resolver -A `Resolver` finds a Kaspa node for a given [network](network.md). An `RpcClient` can be configured to use a Resolver instance instead of a hard-coded URL. On `connect()` the resolver (attempts) to connect to an available node on the public node-network (PNN). +A [`Resolver`](../../reference/Classes/Resolver.md) finds a public Kaspa +node so you don't need a URL up front. Pass one to +[`RpcClient`](../../reference/Classes/RpcClient.md) instead of `url=` +and `client.connect()` picks a live node from the Public Node Network +(PNN). -## When to use it - -- **Building an application that talks to mainnet or testnet** without - shipping a node alongside. -- **Quick scripts and notebooks** where "just give me a node" is the right - default. -- **Failover.** The resolver picks a different node if its first choice is - unreachable. - -If you need a deterministic URL — for example a load-balanced internal -node, or a node with an authenticated endpoint — point `RpcClient` at it -directly and don't construct a `Resolver` at all. - -## The basic shape +For most apps, this is all you need: ```python from kaspa import Resolver, RpcClient -resolver = Resolver() -client = RpcClient(resolver=resolver, network_id="mainnet") +client = RpcClient(resolver=Resolver(), network_id="mainnet") await client.connect() ``` -The `network_id` argument is what the resolver uses to filter candidate -nodes — `"mainnet"`, `"testnet-10"`, or `"testnet-11"`. The same `Resolver` -instance can be reused across networks; the network is a property of the -*client*, not the resolver. +**For security critical applications, or to ensure a trusted node, you should consider connecting to your own node.** + +`network_id` selects the network — `"mainnet"` or a testnet +(e.g. `"testnet-10"`). It takes a string or a +[`NetworkId`](../../reference/Classes/NetworkId.md); see +[Networks](../networks.md) for the full list. Not every testnet has PNN +nodes. -## Configuring the resolver +## Constructor options ```python -# Default: uses the public PNN endpoints baked into the SDK +# Default resolver = Resolver() -# Override with explicit resolver URLs (advanced) +# Require a TLS-capable node (wss://) +resolver = Resolver(tls=True) + +# Point at your own resolver fleet (advanced — see "Under the hood") resolver = Resolver(urls=["https://resolver1.example.org"]) +``` -# Force TLS on resolver requests -resolver = Resolver(tls=True) +- `tls=True` — restrict to `wss://` nodes. Default `False` allows any + reachable node. +- `urls=` — replaces the default resolver-service list with your own + (see [Under the hood](#under-the-hood)). + +## Querying the resolver directly + +You can fetch a URL without constructing an `RpcClient`: + +```python +from kaspa import Encoding, NetworkId, Resolver + +resolver = Resolver() + +url = await resolver.get_url(Encoding.Borsh, NetworkId("mainnet")) +descriptor = await resolver.get_node(Encoding.Borsh, NetworkId("mainnet")) ``` -The default constructor is the right choice for almost everyone. Override -the URLs only if you're operating a private resolver fleet. +[`get_url`](../../reference/Classes/Resolver.md#get_url) returns a +WebSocket URL ready for `RpcClient(url=...)`. +[`get_node`](../../reference/Classes/Resolver.md#get_node) returns a +dict with the node's `uid`, `url`, and other metadata. + +## Under the hood + +You don't need any of this to use `Resolver` — it's here for anyone +running their own infrastructure or debugging connectivity. + +A `Resolver` doesn't open WebSockets or hold Kaspa node URLs. It holds +a list of *resolver service* HTTP endpoints (see +[aspectron/kaspa-resolver](https://github.com/aspectron/kaspa-resolver)) +that track live PNN nodes and load-balance across them. + +On `get_url` / `get_node` (called internally by `client.connect()`): + +1. Pick a configured resolver-service URL at random. +2. `GET {url}/v2/kaspa/{network_id}/{tls_or_any}/wrpc/{encoding}`. +3. Parse the response as a node descriptor and return the URL. +4. On failure, try the next service; raise if all fail. + +The default `Resolver()` ships with the public resolver-service list +embedded in the SDK (sourced from +[`Resolvers.toml`](https://github.com/kaspanet/rusty-kaspa/blob/master/rpc/wrpc/client/Resolvers.toml) +in `kaspa-wrpc-client`). `Resolver(urls=...)` replaces that list — +useful for a private node cluster behind your own resolver fleet. +`resolver.urls()` returns the configured list, or an empty list when +using the embedded defaults (concrete URLs are hidden so they can +rotate without breaking SDK consumers). ## Where to next -- [Connecting](connecting.md) — direct URLs, retry/timeout options, the - encoding choice. -- [Calls](calls.md) — what to do once you're connected. +- [Connecting](connecting.md) — direct URLs, retry/timeout options, + encoding. +- [Calls](calls.md) — what to do once connected. +- [Subscriptions](subscriptions.md) — real-time notifications. diff --git a/docs/learn/rpc/subscriptions.md b/docs/learn/rpc/subscriptions.md index 35ff8fb5..31fc949d 100644 --- a/docs/learn/rpc/subscriptions.md +++ b/docs/learn/rpc/subscriptions.md @@ -1,21 +1,21 @@ # Subscriptions -Subscriptions turn the `RpcClient` from a request/response API into a live -feed. The node pushes events; the client invokes callbacks you registered. -You typically use them to react to UTXO changes for an address you care -about, or to track block / virtual-chain progression for an indexer. +Subscriptions let +[`RpcClient`](../../reference/Classes/RpcClient.md) receive a live feed +of node events. The node pushes events as they happen; the client +invokes the callbacks you registered. ## The two-step pattern Every subscription has two parts: -1. **A listener** — a Python callback registered with - `client.add_event_listener("", callback)`. -2. **A subscription** — `await client.subscribe_(...)` that tells the +1. **A listener** — a Python callback registered via + [`add_event_listener("", callback)`](../../reference/Classes/RpcClient.md#add_event_listener). +2. **A subscription** — `await client.subscribe_(...)` tells the node to start streaming. -Both halves are required. A listener with no subscription receives nothing; -a subscription with no listener silently drops events. +Both halves are required. A listener with no subscription receives +nothing; a subscription with no listener silently drops events. ```python def on_utxo_change(event): @@ -27,22 +27,113 @@ await client.subscribe_utxos_changed([Address("kaspa:qz...")]) ## Available events -| Event name | Subscribe with | -| --- | --- | -| `utxos-changed` | `subscribe_utxos_changed(addresses)` | -| `block-added` | `subscribe_block_added()` | -| `virtual-chain-changed` | `subscribe_virtual_chain_changed(include_accepted_transaction_ids=...)` | -| `virtual-daa-score-changed` | `subscribe_virtual_daa_score_changed()` | -| `sink-blue-score-changed` | `subscribe_sink_blue_score_changed()` | -| `finality-conflict` | `subscribe_finality_conflict()` | -| `finality-conflict-resolved` | `subscribe_finality_conflict_resolved()` | -| `new-block-template` | `subscribe_new_block_template()` | -| `pruning-point-utxo-set-override` | `subscribe_pruning_point_utxo_set_override()` | +| Event name | Subscribe with | Event payload | +| --- | --- | --- | +| `utxos-changed` | [`subscribe_utxos_changed(addresses)`](../../reference/Classes/RpcClient.md#subscribe_utxos_changed) | [`UtxosChangedEvent`](../../reference/TypedDicts/UtxosChangedEvent.md) | +| `block-added` | [`subscribe_block_added()`](../../reference/Classes/RpcClient.md#subscribe_block_added) | [`BlockAddedEvent`](../../reference/TypedDicts/BlockAddedEvent.md) | +| `virtual-chain-changed` | [`subscribe_virtual_chain_changed(include_accepted_transaction_ids=...)`](../../reference/Classes/RpcClient.md#subscribe_virtual_chain_changed) | [`VirtualChainChangedEvent`](../../reference/TypedDicts/VirtualChainChangedEvent.md) | +| `virtual-daa-score-changed` | [`subscribe_virtual_daa_score_changed()`](../../reference/Classes/RpcClient.md#subscribe_virtual_daa_score_changed) | [`VirtualDaaScoreChangedEvent`](../../reference/TypedDicts/VirtualDaaScoreChangedEvent.md) | +| `sink-blue-score-changed` | [`subscribe_sink_blue_score_changed()`](../../reference/Classes/RpcClient.md#subscribe_sink_blue_score_changed) | [`SinkBlueScoreChangedEvent`](../../reference/TypedDicts/SinkBlueScoreChangedEvent.md) | +| `finality-conflict` | [`subscribe_finality_conflict()`](../../reference/Classes/RpcClient.md#subscribe_finality_conflict) | [`FinalityConflictEvent`](../../reference/TypedDicts/FinalityConflictEvent.md) | +| `finality-conflict-resolved` | [`subscribe_finality_conflict_resolved()`](../../reference/Classes/RpcClient.md#subscribe_finality_conflict_resolved) | [`FinalityConflictResolvedEvent`](../../reference/TypedDicts/FinalityConflictResolvedEvent.md) | +| `new-block-template` | [`subscribe_new_block_template()`](../../reference/Classes/RpcClient.md#subscribe_new_block_template) | [`NewBlockTemplateEvent`](../../reference/TypedDicts/NewBlockTemplateEvent.md) | +| `pruning-point-utxo-set-override` | [`subscribe_pruning_point_utxo_set_override()`](../../reference/Classes/RpcClient.md#subscribe_pruning_point_utxo_set_override) | [`PruningPointUtxoSetOverrideEvent`](../../reference/TypedDicts/PruningPointUtxoSetOverrideEvent.md) | + +Each `subscribe_*` has a matching `unsubscribe_*` with the same +argument shape. Event names also map to the +[`NotificationEvent`](../../reference/Enums/NotificationEvent.md) enum +if you prefer typed variants over kebab-case strings. + +The client also emits two control events that don't require a +`subscribe_*` call — just register a listener: + +| Event name | Fires when | Event payload | +| --- | --- | --- | +| `connect` | The WebSocket has connected (including after a reconnect). | [`ConnectEvent`](../../reference/TypedDicts/ConnectEvent.md) | +| `disconnect` | The WebSocket has dropped. | [`DisconnectEvent`](../../reference/TypedDicts/DisconnectEvent.md) | + +Use these to track connection state without polling `client.is_connected`. + +### Listening to all events + +Pass the special `"all"` event name (or `NotificationEvent.All`) to +register one callback for every notification — node-pushed events plus +`connect` / `disconnect`. You still need `subscribe_*` for any +node-pushed event you want to receive; `"all"` only multiplexes +delivery, it doesn't subscribe on your behalf. -Each `subscribe_*` has a matching `unsubscribe_*` that takes the same -argument shape. +```python +def on_any(event): + print(event["type"], event) + +client.add_event_listener("all", on_any) +await client.subscribe_block_added() +await client.subscribe_virtual_daa_score_changed() +``` -## Watching addresses for UTXO changes +## Event payload shape + +Every callback receives a `dict` with a `"type"` key naming the +event. The remaining keys depend on the event. + +### `utxos-changed` ([`UtxosChangedEvent`](../../reference/TypedDicts/UtxosChangedEvent.md)) +Top-level `"added"` and `"removed"` lists of + [`RpcUtxosByAddressesEntry`](../../reference/TypedDicts/RpcUtxosByAddressesEntry.md). + This is the only event that does *not* nest its body under `"data"` — + it's flattened so callbacks can read `event["added"]` directly: + + ```python + { + "type": "utxos-changed", + "added": [ + { + "address": "kaspa:qz...", + "outpoint": {"transactionId": "...", "index": 0}, + "utxoEntry": { + "amount": 100000000, + "scriptPublicKey": {"version": 0, "script": "..."}, + "blockDaaScore": 123456789, + "isCoinbase": False, + }, + }, + ], + "removed": [], + } + ``` + +### All other events +A `"data"` key holds the notification body. Each event has a wrapper + TypedDict (e.g. [`BlockAddedEvent`](../../reference/TypedDicts/BlockAddedEvent.md)) + and a body TypedDict (e.g. + [`RpcBlockAddedNotification`](../../reference/TypedDicts/RpcBlockAddedNotification.md)). + See the [Available events](#available-events) table for the full + list. For example, a `virtual-daa-score-changed` callback receives: + + ```python + { + "type": "virtual-daa-score-changed", + "data": { + "virtualDaaScore": 123456789, + }, + } + ``` + +### `connect` / `disconnect` + ([`ConnectEvent`](../../reference/TypedDicts/ConnectEvent.md) / + [`DisconnectEvent`](../../reference/TypedDicts/DisconnectEvent.md)): + a `"rpc"` key with the node URL. + +The bundled +[`Notification`](../../reference/Classes/Notification.md) class wraps +notifications internally; for callback type hints, use the per-event +TypedDicts above. + +## Examples + +### Watching addresses for UTXO changes + +Pass [`Address`](../../reference/Classes/Address.md) instances (or +strings parsed by `Address(...)`): ```python from kaspa import Address @@ -62,32 +153,52 @@ await client.subscribe_utxos_changed(addresses) await client.unsubscribe_utxos_changed(addresses) ``` -For watching a managed-wallet account rather than raw addresses, use the -wallet's `Balance` and `Maturity` events instead — see +To watch a managed-wallet account instead of raw addresses, use the +wallet's `Balance` and `Maturity` events — see [Wallet → Transaction History](../wallet/transaction-history.md). -## Block events +### Block events ```python def on_block(event): - print("new block:", event["block"]["header"]["hash"]) + print("new block:", event["data"]["block"]["header"]["hash"]) client.add_event_listener("block-added", on_block) await client.subscribe_block_added() ``` -## Virtual chain progression +### Virtual chain progression ```python def on_chain(event): - print("chain update:", event) + data = event["data"] + print("added:", data["addedChainBlockHashes"]) + print("removed:", data["removedChainBlockHashes"]) client.add_event_listener("virtual-chain-changed", on_chain) await client.subscribe_virtual_chain_changed(include_accepted_transaction_ids=True) ``` -`include_accepted_transaction_ids=True` makes the event payload usable as -a confirmation feed — every accepted transaction id appears in the stream. +With `include_accepted_transaction_ids=True`, the payload doubles as a +confirmation feed — every accepted transaction id appears in +`event["data"]["acceptedTransactionIds"]`. For the one-shot equivalent, +see [`get_virtual_chain_from_block`](calls.md#virtual-chain). + +### Connection state + +```python +def on_connect(event): + print("connected to", event["rpc"]) + +def on_disconnect(event): + print("disconnected from", event["rpc"]) + +client.add_event_listener("connect", on_connect) +client.add_event_listener("disconnect", on_disconnect) +``` + +No `subscribe_*` is needed — the client emits these itself when the +WebSocket transitions. ## Listener bookkeeping @@ -100,13 +211,19 @@ client.remove_event_listener("block-added") # remove all for client.remove_all_event_listeners() # remove all globally ``` -Listeners outlive a single subscription cycle — re-subscribing after an -unsubscribe does *not* re-fire previously delivered events. If you need to -catch up, do a one-shot `get_utxos_by_addresses` (or equivalent) before -re-subscribing. +Listeners outlive a single subscription cycle. Re-subscribing after an +unsubscribe does *not* re-fire previously delivered events. To catch +up, do a one-shot +[`get_utxos_by_addresses`](calls.md#balances-and-utxos) (or equivalent) +before re-subscribing. ## Where to next - [Calls](calls.md) — the request/response side of the API. -- [Wallet → Transaction History](../wallet/transaction-history.md) — the - managed Wallet's higher-level event surface. +- [`RpcClient`](../../reference/Classes/RpcClient.md) — full + `subscribe_*` / `unsubscribe_*` / `add_event_listener` reference. +- [`UtxoProcessor`](../wallet-sdk/utxo-processor.md) and + [`UtxoContext`](../wallet-sdk/utxo-context.md) — higher-level UTXO + tracking built on `utxos-changed`. +- [Wallet → Transaction History](../wallet/transaction-history.md) — + the managed Wallet's higher-level event surface. diff --git a/docs/learn/transactions/index.md b/docs/learn/transactions/overview.md similarity index 100% rename from docs/learn/transactions/index.md rename to docs/learn/transactions/overview.md diff --git a/docs/learn/wallet-sdk/index.md b/docs/learn/wallet-sdk/overview.md similarity index 94% rename from docs/learn/wallet-sdk/index.md rename to docs/learn/wallet-sdk/overview.md index a8bde225..b3551951 100644 --- a/docs/learn/wallet-sdk/index.md +++ b/docs/learn/wallet-sdk/overview.md @@ -1,7 +1,7 @@ # Wallet SDK The **Wallet SDK** section is the layer beneath the managed -[Wallet](../wallet/index.md). When you don't need on-disk file storage, +[Wallet](../wallet/overview.md). When you don't need on-disk file storage, multi-account management, or the wallet's event multiplexer — when you just want to derive a key, build a transaction, or track UTXOs for a few addresses — drop down here. @@ -20,7 +20,7 @@ addresses — drop down here. | You want to... | Use | | --- | --- | -| Open a file, manage many accounts, track them long-term | [Wallet](../wallet/index.md) | +| Open a file, manage many accounts, track them long-term | [Wallet](../wallet/overview.md) | | Sign one transaction in a script with a key you already have | Wallet SDK ([Transaction Generator](tx-generator.md)) | | Derive an address from a mnemonic without persisting anything | Wallet SDK ([Key Management](key-management.md), [Derivation](derivation.md)) | | Watch a fixed set of addresses for incoming UTXOs without a wallet file | Wallet SDK ([UTXO Processor](utxo-processor.md), [UTXO Context](utxo-context.md)) | diff --git a/docs/learn/wallet-sdk/utxo-context.md b/docs/learn/wallet-sdk/utxo-context.md index a4215401..e4410ad4 100644 --- a/docs/learn/wallet-sdk/utxo-context.md +++ b/docs/learn/wallet-sdk/utxo-context.md @@ -7,7 +7,7 @@ into whichever contexts have registered the relevant addresses. The context exposes the resulting UTXO set, balance, and mature/pending splits. -If you're using the managed [Wallet](../wallet/index.md), it manages a +If you're using the managed [Wallet](../wallet/overview.md), it manages a `UtxoContext` per activated account internally — you usually don't construct one yourself. Drop down here when you want UTXO tracking without the on-disk wallet file. diff --git a/docs/learn/wallet-sdk/utxo-processor.md b/docs/learn/wallet-sdk/utxo-processor.md index b91aacaf..cdec37c8 100644 --- a/docs/learn/wallet-sdk/utxo-processor.md +++ b/docs/learn/wallet-sdk/utxo-processor.md @@ -4,7 +4,7 @@ A `UtxoProcessor` subscribes to a node's UTXO and virtual-chain notifications and dispatches them to one or more [UTXO Contexts](utxo-context.md). It's the engine that makes context tracking work. If you're using the managed -[Wallet](../wallet/index.md), one is constructed for you. If you're not, +[Wallet](../wallet/overview.md), one is constructed for you. If you're not, you build one yourself and bind contexts to it. ## Construction diff --git a/docs/learn/wallet/index.md b/docs/learn/wallet/overview.md similarity index 96% rename from docs/learn/wallet/index.md rename to docs/learn/wallet/overview.md index 52922b65..7462f905 100644 --- a/docs/learn/wallet/index.md +++ b/docs/learn/wallet/overview.md @@ -3,14 +3,14 @@ The `Wallet` class is the SDK's high-level managed wallet. It layers encrypted on-disk storage, multi-account management, an event bus, and built-in send / transfer / sweep flows on top of the lower-level primitives -in [Wallet SDK](../wallet-sdk/index.md). +in [Wallet SDK](../wallet-sdk/overview.md). ## When to reach for `Wallet` | You want to... | Use | | --- | --- | | Persist secrets, manage multiple accounts, react to chain events | `Wallet` | -| Sign one transaction in a script, no on-disk state | The primitives in [Wallet SDK](../wallet-sdk/index.md) | +| Sign one transaction in a script, no on-disk state | The primitives in [Wallet SDK](../wallet-sdk/overview.md) | | Embed wallet behaviour in your own app | `Wallet` — the listener model and account API are designed for this | If you only need a one-shot signer, the primitives are simpler. If you need diff --git a/mkdocs.yml b/mkdocs.yml index 81a571da..1056e41a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -111,13 +111,54 @@ nav: - Getting Started: - Installation: getting-started/installation.md - Examples: getting-started/examples.md + - Security: getting-started/security.md + - Learn: + - Overview: learn/index.md + - RPC: + - RPC Intro: learn/rpc/overview.md + - Connecting: learn/rpc/connecting.md + - Resolver: learn/rpc/resolver.md + - Calls: learn/rpc/calls.md + - Subscriptions: learn/rpc/subscriptions.md + - Wallet: + - Overview: learn/wallet/overview.md + - Architecture: learn/wallet/architecture.md + - Lifecycle: learn/wallet/lifecycle.md + - Initialize: learn/wallet/initialize.md + - Start: learn/wallet/start.md + - Open: learn/wallet/open.md + - Private Keys: learn/wallet/private-keys.md + - Accounts: learn/wallet/accounts.md + - Addresses: learn/wallet/addresses.md + - Keypair Accounts: learn/wallet/keypair.md + - Send Transaction: learn/wallet/send-transaction.md + - Sweep Funds: learn/wallet/sweep.md + - Transaction History: learn/wallet/transaction-history.md + - Wallet SDK: + - Overview: learn/wallet-sdk/overview.md + - Key Management: learn/wallet-sdk/key-management.md + - Transaction Generator: learn/wallet-sdk/tx-generator.md + - Derivation: learn/wallet-sdk/derivation.md + - UTXO Context: learn/wallet-sdk/utxo-context.md + - UTXO Processor: learn/wallet-sdk/utxo-processor.md + - Networks: learn/networks.md + - Addresses: learn/addresses.md + - Transactions: + - Overview: learn/transactions/overview.md + - Inputs: learn/transactions/inputs.md + - Outputs: learn/transactions/outputs.md + - Mass & Fees: learn/transactions/mass-and-fees.md + - Signing: learn/transactions/signing.md + - Submission: learn/transactions/submission.md + - Metadata Fields: learn/transactions/metadata.md + - Serialization: learn/transactions/serialization.md + - Kaspa Concepts: learn/concepts.md - Guides: - - RPC Client: guides/rpc-client.md - - Addresses: guides/addresses.md - - Transactions: guides/transactions.md - - Mnemonics: guides/mnemonics.md - - Key Derivation: guides/key-derivation.md - - Message Signing: guides/message-signing.md + - Generate or restore a mnemonic: guides/mnemonics.md + - Sign and verify a message: guides/message-signing.md + - Recover a wallet: guides/wallet-recovery.md + - Custom derivation paths: guides/custom-derivation.md + - Multi-signature transactions: guides/multisig.md - API Reference: reference/ - Contributing: - Overview: contributing/index.md diff --git a/python/kaspa/__init__.pyi b/python/kaspa/__init__.pyi index 203bd04d..d9de118a 100644 --- a/python/kaspa/__init__.pyi +++ b/python/kaspa/__init__.pyi @@ -5666,4 +5666,166 @@ class SubmitTransactionResponse(TypedDict): class SubmitTransactionReplacementResponse(TypedDict): """Response from submit_transaction_replacement.""" transactionId: str - replacedTransaction: RpcTransaction \ No newline at end of file + replacedTransaction: RpcTransaction + + +# ============================================================================= +# Notification Bodies (the inner notification struct delivered as event["data"]) +# ============================================================================= + +class RpcBlockAddedNotification(TypedDict): + """Body for the `block-added` notification. + + Delivered as `event["data"]` to listeners registered with + `add_event_listener("block-added", ...)`. + """ + block: RpcBlock + + +class RpcVirtualChainChangedNotification(TypedDict): + """Body for the `virtual-chain-changed` notification. + + `acceptedTransactionIds` is only populated when the subscription was + opened with `include_accepted_transaction_ids=True`. + """ + removedChainBlockHashes: list[str] + addedChainBlockHashes: list[str] + acceptedTransactionIds: list[RpcAcceptedTransactionIds] + + +class RpcFinalityConflictNotification(TypedDict): + """Body for the `finality-conflict` notification.""" + violatingBlockHash: str + + +class RpcFinalityConflictResolvedNotification(TypedDict): + """Body for the `finality-conflict-resolved` notification.""" + finalityBlockHash: str + + +class RpcSinkBlueScoreChangedNotification(TypedDict): + """Body for the `sink-blue-score-changed` notification.""" + sinkBlueScore: int + + +class RpcVirtualDaaScoreChangedNotification(TypedDict): + """Body for the `virtual-daa-score-changed` notification.""" + virtualDaaScore: int + + +class RpcPruningPointUtxoSetOverrideNotification(TypedDict, total=False): + """Body for the `pruning-point-utxo-set-override` notification. + + The body has no fields; the notification fires as a signal only. + """ + pass + + +class RpcNewBlockTemplateNotification(TypedDict, total=False): + """Body for the `new-block-template` notification. + + The body has no fields; the notification fires as a signal only. + """ + pass + + +# ============================================================================= +# Event Payloads (the dict an `add_event_listener` callback receives) +# ============================================================================= + +class BlockAddedEvent(TypedDict): + """Payload delivered for the `block-added` event.""" + type: typing.Literal["block-added"] + data: RpcBlockAddedNotification + + +class VirtualChainChangedEvent(TypedDict): + """Payload delivered for the `virtual-chain-changed` event.""" + type: typing.Literal["virtual-chain-changed"] + data: RpcVirtualChainChangedNotification + + +class FinalityConflictEvent(TypedDict): + """Payload delivered for the `finality-conflict` event.""" + type: typing.Literal["finality-conflict"] + data: RpcFinalityConflictNotification + + +class FinalityConflictResolvedEvent(TypedDict): + """Payload delivered for the `finality-conflict-resolved` event.""" + type: typing.Literal["finality-conflict-resolved"] + data: RpcFinalityConflictResolvedNotification + + +class SinkBlueScoreChangedEvent(TypedDict): + """Payload delivered for the `sink-blue-score-changed` event.""" + type: typing.Literal["sink-blue-score-changed"] + data: RpcSinkBlueScoreChangedNotification + + +class VirtualDaaScoreChangedEvent(TypedDict): + """Payload delivered for the `virtual-daa-score-changed` event.""" + type: typing.Literal["virtual-daa-score-changed"] + data: RpcVirtualDaaScoreChangedNotification + + +class PruningPointUtxoSetOverrideEvent(TypedDict): + """Payload delivered for the `pruning-point-utxo-set-override` event.""" + type: typing.Literal["pruning-point-utxo-set-override"] + data: RpcPruningPointUtxoSetOverrideNotification + + +class NewBlockTemplateEvent(TypedDict): + """Payload delivered for the `new-block-template` event.""" + type: typing.Literal["new-block-template"] + data: RpcNewBlockTemplateNotification + + +class UtxosChangedEvent(TypedDict): + """Payload delivered for the `utxos-changed` event. + + Note: unlike most notifications, the body is flattened onto the + event dict rather than nested under a `data` key. + """ + type: typing.Literal["utxos-changed"] + added: list[RpcUtxosByAddressesEntry] + removed: list[RpcUtxosByAddressesEntry] + + +class ConnectEvent(TypedDict): + """Payload delivered for the `connect` RPC control event. + + Fires when the underlying WebSocket connects (including reconnects). + `add_event_listener("connect", ...)` alone is enough — no + `subscribe_*` is required. + """ + type: typing.Literal["connect"] + rpc: str | None + + +class DisconnectEvent(TypedDict): + """Payload delivered for the `disconnect` RPC control event. + + Fires when the underlying WebSocket drops. + `add_event_listener("disconnect", ...)` alone is enough — no + `subscribe_*` is required. + """ + type: typing.Literal["disconnect"] + rpc: str | None + + +NotificationEventPayload = typing.Union[ + BlockAddedEvent, + VirtualChainChangedEvent, + FinalityConflictEvent, + FinalityConflictResolvedEvent, + SinkBlueScoreChangedEvent, + VirtualDaaScoreChangedEvent, + PruningPointUtxoSetOverrideEvent, + NewBlockTemplateEvent, + UtxosChangedEvent, + ConnectEvent, + DisconnectEvent, +] +"""Discriminated union of every dict shape that an event listener can +receive. Narrow on the `type` field to access event-specific keys.""" \ No newline at end of file From f96c205af6486ac28e0a0e84d44c1cbb575b33f1 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Fri, 1 May 2026 17:13:08 -0400 Subject: [PATCH 3/7] docs wip --- docs/learn/addresses.md | 32 +-- docs/learn/concepts.md | 105 ++++---- docs/learn/index.md | 34 +-- docs/learn/networks.md | 54 ++-- docs/learn/transactions/inputs.md | 66 ++--- docs/learn/transactions/mass-and-fees.md | 88 +++---- docs/learn/transactions/metadata.md | 68 ++--- docs/learn/transactions/outputs.md | 53 ++-- docs/learn/transactions/overview.md | 68 ++--- docs/learn/transactions/serialization.md | 39 +-- docs/learn/transactions/signing.md | 88 ++++--- docs/learn/transactions/submission.md | 72 ++--- docs/learn/wallet-sdk/derivation.md | 34 +-- docs/learn/wallet-sdk/key-management.md | 45 ++-- docs/learn/wallet-sdk/overview.md | 18 +- docs/learn/wallet-sdk/tx-generator.md | 40 +-- docs/learn/wallet-sdk/utxo-context.md | 42 +-- docs/learn/wallet-sdk/utxo-processor.md | 33 +-- docs/learn/wallet/accounts.md | 44 ++-- docs/learn/wallet/addresses.md | 44 ++-- docs/learn/wallet/architecture.md | 89 ++----- docs/learn/wallet/initialize.md | 62 ----- docs/learn/wallet/keypair.md | 51 ++-- docs/learn/wallet/lifecycle.md | 246 ++++++++++++++++-- docs/learn/wallet/overview.md | 51 ++-- docs/learn/wallet/private-keys.md | 64 +++-- docs/learn/wallet/send-transaction.md | 41 +-- docs/learn/wallet/start.md | 107 -------- docs/learn/wallet/sweep.md | 40 +-- docs/learn/wallet/sync-state.md | 169 ++++++++++++ docs/learn/wallet/transaction-history.md | 81 +++--- docs/learn/wallet/utxo-maturity.md | 37 +++ .../learn/wallet/{open.md => wallet-files.md} | 86 ++---- mkdocs.yml | 12 +- 34 files changed, 1207 insertions(+), 996 deletions(-) delete mode 100644 docs/learn/wallet/initialize.md delete mode 100644 docs/learn/wallet/start.md create mode 100644 docs/learn/wallet/sync-state.md create mode 100644 docs/learn/wallet/utxo-maturity.md rename docs/learn/wallet/{open.md => wallet-files.md} (50%) diff --git a/docs/learn/addresses.md b/docs/learn/addresses.md index 68f989c6..a75fd62d 100644 --- a/docs/learn/addresses.md +++ b/docs/learn/addresses.md @@ -1,8 +1,9 @@ # Addresses -A Kaspa address encodes a public key or a script hash, the address -*version* (which signature scheme or script type it pays to), and the -network it belongs to. The SDK exposes them as `Address` instances. +A Kaspa address encodes a public key or script hash, the address +*version* (signature scheme or script type), and its network. The SDK +exposes them as [`Address`](../reference/Classes/Address.md) +instances. ## Anatomy @@ -83,9 +84,9 @@ print(addr.version) | `kaspadev:` | devnet | | `kaspasim:` | simnet | -To re-encode an address for a different network — for example, to -display the testnet equivalent of a known mainnet address during -testing — set the prefix: +To re-encode an address for a different network — e.g. to display the +testnet equivalent of a mainnet address during testing — overwrite +the prefix: ```python addr = Address("kaspa:qz...") @@ -93,9 +94,9 @@ addr.prefix = "kaspatest" print(addr.to_string()) # kaspatest:qz... ``` -This *does not* re-derive the address from a key; it just rewrites the -prefix. For programmatic conversion of an actual key to a different -network's address, derive again with the right `NetworkType`. +This rewrites the prefix only; it does *not* re-derive from a key. For +a real key-to-network conversion, derive again with the right +`NetworkType`. ## Scripts and addresses @@ -114,8 +115,9 @@ spk = pay_to_address_script(Address("kaspa:qz...")) print(spk.script) ``` -`pay_to_address_script` is the lockup script you put in a -`TransactionOutput` to pay to that address. See [Transactions → Outputs](transactions/outputs.md). +[`pay_to_address_script`](../reference/Functions/pay_to_address_script.md) +is the lockup script you put in a `TransactionOutput` to pay an +address. See [Transactions → Outputs](transactions/outputs.md). ## Multi-signature addresses @@ -131,14 +133,14 @@ multi = create_multisig_address( print(multi.to_string()) ``` -For the full multisig spend flow (creating the address, signing with -multiple cosigners, submitting), see the +For the full multisig spend flow (address creation, multi-cosigner +signing, submission), see the [Multi-signature transactions](../guides/multisig.md) recipe. ## Where to next - [Networks](networks.md) — what each prefix means. -- [Transactions](transactions/overview.md) — using addresses inside transaction - outputs. +- [Transactions → Outputs](transactions/outputs.md) — using addresses + inside transaction outputs. - [Wallet SDK → Derivation](wallet-sdk/derivation.md) — deriving many addresses from one key. diff --git a/docs/learn/concepts.md b/docs/learn/concepts.md index 9bcefac0..a2ec0c8f 100644 --- a/docs/learn/concepts.md +++ b/docs/learn/concepts.md @@ -1,23 +1,23 @@ # Kaspa Concepts -This page is a fast tour of the protocol concepts you bump into when -using the SDK. It's deliberately surface-level — for the protocol-level -details, see the [Kaspa MDBook](https://kaspa-mdbook.aspectron.com/). +A fast tour of the protocol concepts you'll bump into while using the +SDK. It's deliberately surface-level — for the full picture, see the +[Kaspa MDBook](https://kaspa-mdbook.aspectron.com/). ## BlockDAG, not blockchain -Kaspa orders transactions in a **directed acyclic graph of blocks**, not -a linear chain. Multiple blocks can be produced in parallel and reference -the same parents; consensus emerges from a deterministic ordering of -the DAG (the *virtual chain*) rather than from a single longest chain. +Kaspa orders transactions in a **directed acyclic graph of blocks**, +not a linear chain. Multiple blocks can be produced in parallel and +reference the same parents; consensus emerges from a deterministic DAG +ordering (the *virtual chain*), not a single longest chain. -The practical consequence for SDK users: +What this means in practice: -- **Block rate is high** (one or more blocks per second on testnet-10). - You see far more `block-added` events than you would on a Bitcoin-shaped - chain. +- **Block rate is high** (one or more blocks per second on testnet-10), + so [`block-added`](rpc/subscriptions.md#available-events) events fire + far more often than on a Bitcoin-shaped chain. - **Transactions confirm via the virtual chain.** A transaction is - "accepted" once it appears in the virtual-chain ordering, not when it + "accepted" when it enters the virtual-chain ordering, not when it first lands in a block. - **Reorgs happen at the DAG-ordering level.** A previously-accepted transaction can be re-ordered out; the SDK surfaces this as a @@ -26,89 +26,84 @@ The practical consequence for SDK users: ## UTXO model Every spendable balance is a set of *unspent transaction outputs*. To -spend, you select UTXOs that sum to at least the amount you need, plus a +spend, select UTXOs that sum to at least the amount you need, plus a change output for the leftover. -The SDK never asks "what's my balance" of the chain directly — it tracks -UTXOs locally, derives a balance from them, and updates as new ones land -or existing ones spend. See -[Wallet → Architecture](wallet/architecture.md) and +The SDK never asks the chain "what's my balance" — it tracks UTXOs +locally, derives a balance, and updates as new ones land or existing +ones spend. See [Wallet → Architecture](wallet/architecture.md) and [Wallet SDK → UTXO Context](wallet-sdk/utxo-context.md). ## Virtual chain and DAA score -Two ordering scalars come up in the SDK: +Two ordering scalars show up in the SDK: - **DAA score** (Difficulty Adjustment Algorithm) — a monotonic counter - that increases roughly with wall-clock time. Used as a "what age is - this block" comparator; surfaces on every UTXO via - `block_daa_score`. + that grows roughly with wall-clock time. Use as an "age of block" + comparator; appears on every UTXO via `block_daa_score`. - **Virtual chain** — the canonical DAG ordering. The - `virtual-chain-changed` notification reports updates to it; transactions - are confirmed by appearing in it. + [`virtual-chain-changed`](rpc/subscriptions.md#virtual-chain-progression) + notification reports updates; a transaction is confirmed by appearing + in it. -For "wait until N seconds have passed", DAA score is roughly the right -metric. For "wait until this transaction is confirmed", a `Maturity` -event is the right gate (see below). +For "wait N seconds", DAA score is roughly right. For "wait until this +transaction is confirmed", use a `Maturity` event (see below). ## Maturity A UTXO moves through three states in the wallet's view: -- **Pending** — seen, but not deeply enough confirmed to be spent. +- **Pending** — seen, not yet confirmed deeply enough to spend. - **Mature** — confirmed past the maturity threshold; spendable. -- **Outgoing** — locked because the wallet just spent it; awaits its +- **Outgoing** — locked because the wallet just spent it; awaits the spend transaction maturing or being re-orged out. [Coinbase outputs](https://kaspa-mdbook.aspectron.com/) have a longer -maturity than regular transaction outputs. The SDK applies the right -threshold automatically — you observe the result via `Pending` / -`Maturity` events on either the [managed -Wallet](wallet/transaction-history.md) or the +maturity than regular outputs. The SDK applies the right threshold +automatically — you observe via `Pending` / `Maturity` events on the +[managed Wallet](wallet/transaction-history.md) or the [`UtxoProcessor`](wallet-sdk/utxo-processor.md). The right "wait for confirmation" gate is the `Maturity` event for the -specific transaction you care about — not `Pending`, and not "wait N -seconds". +specific transaction — not `Pending`, and not "wait N seconds". ## Mass and the fee market -Every transaction has a *mass* — a number derived from the transaction's -byte size, its compute cost, and its storage cost (a function of input -and output values). Mass replaces the simple "size × byte rate" fee -model some other UTXO chains use. +Every transaction has a *mass* — a number derived from byte size, +compute cost, and a storage cost (a function of input and output +values). Mass replaces the "size × byte rate" fee model used by some +other UTXO chains. -The required fee for a transaction is `mass × fee_rate`, where -`fee_rate` is set by the network's current congestion. You query the -prevailing rate via `client.get_fee_estimate()` (see -[RPC → Calls](rpc/calls.md)) or via the wallet's -`fee_rate_estimate()` (see -[Wallet → Send Transaction](wallet/send-transaction.md)). +The required fee is `mass × fee_rate`, where `fee_rate` reflects current +congestion. Query the rate via +[`client.get_fee_estimate()`](rpc/calls.md#fees) or the wallet's +[`fee_rate_estimate()`](wallet/send-transaction.md#fees). The [Transaction Generator](wallet-sdk/tx-generator.md) handles all of -this for you — it computes mass, picks a fee rate, and adds a change -output that absorbs the leftover. You only set fees explicitly when you -want a non-standard policy (priority surcharges, exact-balance sweeps, -multisig with custom signature counts). +this — it computes mass, picks a rate, and folds the leftover into +change. Set fees explicitly only for non-standard policies (priority +surcharges, exact-balance sweeps, multisig with custom signature +counts). ## Sompi and KAS -The atomic unit is the **sompi**: 1 KAS = 100 000 000 sompi. Every +The atomic unit is the **sompi**: `1 KAS = 100_000_000 sompi`. Every amount in the SDK — UTXO value, output amount, fee, balance — is in sompi. Convert at the UI boundary with `sompi_to_kaspa(...)` / -`kaspa_to_sompi(...)`. Don't store KAS-as-float anywhere internal; use -ints in sompi. +`kaspa_to_sompi(...)`. Don't store KAS as a float internally; use +sompi ints. ## Subnetworks Most transactions live on the default subnetwork (id all zeros). The -field exists for protocol extensions; you'll usually leave +field exists for protocol extensions; leave `subnetwork_id="0000...0"` unchanged when building manually. ## Where to next - [Networks](networks.md) — picking a chain to talk to. -- [Addresses](addresses.md) and [Transactions](transactions/overview.md) — the - on-chain primitives in Python. +- [Addresses](addresses.md) and + [Transactions](transactions/overview.md) — the on-chain primitives + in Python. - [Wallet → Architecture](wallet/architecture.md) — how the SDK turns - these concepts into a wallet you can actually use. + these concepts into a working wallet. diff --git a/docs/learn/index.md b/docs/learn/index.md index 82e31223..985f80b8 100644 --- a/docs/learn/index.md +++ b/docs/learn/index.md @@ -1,23 +1,23 @@ # Learn -- **[RPC](rpc/overview.md)** — the `RpcClient`: resolver, connection, calls, - notifications. -- **[Wallet](wallet/overview.md)** — the managed high-level `Wallet` API: lifecycle, file - storage, accounts, addresses, sending, history. -- **[Wallet SDK](wallet-sdk/overview.md)** — the lower-level primitives that the - managed `Wallet` is built on: key management, transaction `Generator`, - derivation, `UtxoContext`, `UtxoProcessor`, etc. -- **[Networks](networks.md)** - working with the various Kaspa networks (mainnet, testnets, etc.) from this SDK. -- **[Addresses](addresses.md)** - a quick primer on Kaspa addresses and handling in this SDK. -- **[Transactions](transactions/overview.md)** — the on-chain primitives: - inputs, outputs, mass and fees, signing, submission, metadata fields, - and serialization. -- **[Kaspa Concepts](concepts.md)** — explanation of the BlockDAG, UTXO - model, mass-based fees, and maturity. Read this if any of those terms feel - fuzzy. +- **[RPC](rpc/overview.md)** — the `RpcClient`: resolver, connection, + calls, notifications. +- **[Wallet](wallet/overview.md)** — the managed high-level `Wallet` + API: lifecycle, file storage, accounts, addresses, sending, history. +- **[Wallet SDK](wallet-sdk/overview.md)** — the lower-level primitives + the managed `Wallet` is built on: key management, transaction + `Generator`, derivation, `UtxoContext`, `UtxoProcessor`. +- **[Networks](networks.md)** — working with mainnet, testnets, and the + rest from this SDK. +- **[Addresses](addresses.md)** — a primer on Kaspa addresses. +- **[Transactions](transactions/overview.md)** — the on-chain + primitives: inputs, outputs, mass and fees, signing, submission, + metadata, serialization. +- **[Kaspa Concepts](concepts.md)** — BlockDAG, UTXO model, mass-based + fees, and maturity. Read this if any of those terms feel fuzzy. ## Before you get started Read [Security](../getting-started/security.md). The Learn snippets use -literal mnemonics, hex strings, and short passwords for readability — **that -is not how production code should handle secret material.** +literal mnemonics, hex strings, and short passwords for readability — +**that is not how production code should handle secret material.** diff --git a/docs/learn/networks.md b/docs/learn/networks.md index 6e146813..8ec671f7 100644 --- a/docs/learn/networks.md +++ b/docs/learn/networks.md @@ -1,29 +1,29 @@ # Networks Kaspa runs three live networks: a production mainnet and two testnets. -Every SDK call that hits the chain — `RpcClient`, `Wallet`, -`Address`, derivation — needs a network identifier so it knows which -chain it's targeting and which address prefix to encode. +Every SDK entry point that hits the chain — `RpcClient`, `Wallet`, +`Address`, derivation — needs a network identifier to pick the right +chain and address prefix. ## The networks | Network | `network_id` | Address prefix | When to use | | --- | --- | --- | --- | | Mainnet | `"mainnet"` | `kaspa:` | Production. Real KAS. | -| Testnet 10 | `"testnet-10"` | `kaspatest:` | The mature testnet. Default for SDK examples; faucets exist. | +| Testnet 10 | `"testnet-10"` | `kaspatest:` | Mature testnet. Default for SDK examples; faucets available. | | Testnet 11 | `"testnet-11"` | `kaspatest:` | Higher block-rate testnet for performance work. | | Devnet | (operator-defined) | `kaspadev:` | A developer-run private chain. | | Simnet | (operator-defined) | `kaspasim:` | Simulation / unit tests against a local sim. | -Mainnet and testnet-10 are what almost every reader of these docs will -touch. Reach for testnet-11 when you specifically need its higher block -rate; the SDK behaves identically on it. +Most readers will only touch mainnet and testnet-10. Use testnet-11 +only when you need its higher block rate — the SDK behaves identically +on it. ## `network_id` strings vs `NetworkId` -Most APIs accept the string form (`"mainnet"`, `"testnet-10"`). The -`NetworkId` class is the typed form — useful when you want a value to -hold and pass around without re-parsing: +Most APIs accept the string form (`"mainnet"`, `"testnet-10"`). +[`NetworkId`](../reference/Classes/NetworkId.md) is the typed form — +useful when you want to hold a value without re-parsing: ```python from kaspa import NetworkId @@ -32,40 +32,42 @@ mainnet = NetworkId("mainnet") testnet = NetworkId("testnet-10") ``` -`NetworkId.network_type` and `NetworkId.suffix` give you the parts back. -`NetworkType` (the enum) is what some APIs accept as a third form — -`NetworkType.Mainnet`, `NetworkType.Testnet`, etc. They all describe the -same thing; pick whichever the call site reads cleanly with. +`NetworkId.network_type` and `NetworkId.suffix` return the parts. +[`NetworkType`](../reference/Enums/NetworkType.md) is a third form +some APIs accept (`NetworkType.Mainnet`, `NetworkType.Testnet`). All +three describe the same thing; pick whichever reads cleanly at the +call site. ## What changes between networks - **Address prefix.** A key derived under `mainnet` produces - `kaspa:...`; the same key under `testnet-10` produces `kaspatest:...`. - See [Addresses](addresses.md). -- **Genesis and chain state.** Every network has its own UTXO set; - funds on one network don't exist on another. -- **Resolver pool.** A `Resolver` only returns nodes for the - `network_id` of the client it was given to. + `kaspa:...`; the same key under `testnet-10` produces + `kaspatest:...`. See [Addresses](addresses.md). +- **Genesis and chain state.** Each network has its own UTXO set; funds + on one don't exist on another. +- **Resolver pool.** A + [`Resolver`](rpc/resolver.md) only returns nodes for the configured + `network_id`. - **Maturity depths.** Coinbase maturity differs by network; the SDK applies the right value automatically. ## Picking one for development - **Writing examples / docs / tests:** use `testnet-10`. It's stable, - there's a faucet, and addresses look obviously test-shaped + has a faucet, and addresses are obviously test-shaped (`kaspatest:...`). -- **Performance experiments:** use `testnet-11`. Block rate is higher, - so UTXO churn and event volume look more like a stress test. +- **Performance experiments:** use `testnet-11`. Higher block rate + means UTXO churn and event volume resemble a stress test. - **Production code paths under CI:** parametrise the network — keep test runs on testnet, mainnet only on a release pipeline. -- **Anything that ever touches mainnet:** read +- **Anything touching mainnet:** read [Security](../getting-started/security.md) first. ## Where to next - [Addresses](addresses.md) — what the prefix encodes and how versions fit in. -- [Wallet → Initialize](wallet/initialize.md) — `network_id` is a - required constructor argument. +- [Wallet → Lifecycle](wallet/lifecycle.md#construct) — `network_id` + is a required constructor argument. - [RPC → Resolver](rpc/resolver.md) — how a `Resolver` finds a node for the configured network. diff --git a/docs/learn/transactions/inputs.md b/docs/learn/transactions/inputs.md index fe968bf4..456270cb 100644 --- a/docs/learn/transactions/inputs.md +++ b/docs/learn/transactions/inputs.md @@ -1,8 +1,8 @@ # Inputs A transaction's inputs say *which UTXOs are being spent*. Each input -points at a previous output by `(transaction_id, index)` and carries the -script that proves the spender is allowed to claim it. +points at a previous output by `(transaction_id, index)` and carries +the script that proves the spender is allowed to claim it. ## Types involved @@ -15,19 +15,19 @@ TransactionInput utxo: UtxoEntryReference # optional, but you almost always want it set ``` -- **`TransactionOutpoint`** — `(transaction_id, index)`. The pointer to - the output you're spending. -- **`UtxoEntryReference`** — the cached copy of the *spent output*: its - amount, its lockup script, the block DAA score it landed in, and - whether it's a coinbase. See [UTXO Context](../wallet-sdk/utxo-context.md) - for how the SDK tracks these. -- **`signature_script`** — the unlocking script. Empty string at build - time; filled when you sign. See [Signing](signing.md). -- **`sequence`** — sequence number; leave at `0` unless you have a +- **`TransactionOutpoint`** — `(transaction_id, index)`. The pointer + to the output being spent. +- **`UtxoEntryReference`** — a cached copy of the *spent output*: its + amount, lockup script, block DAA score, and coinbase flag. See + [UTXO Context](../wallet-sdk/utxo-context.md) for how the SDK + tracks these. +- **`signature_script`** — the unlocking script. Empty string at + build time; filled when you sign. See [Signing](signing.md). +- **`sequence`** — sequence number. Leave at `0` unless you have a specific protocol-level reason. -- **`sig_op_count`** — how many signature operations this input - performs (`1` for a normal Schnorr or ECDSA spend, `>1` for multisig). - This feeds into mass calculation. +- **`sig_op_count`** — number of signature operations this input + performs (`1` for Schnorr/ECDSA, `>1` for multisig). Feeds into + mass calculation. ## Build an input @@ -53,20 +53,20 @@ inp = TransactionInput( ## Why inputs carry a UtxoEntryReference Kaspa signs over the spent output's amount and lockup, not just the -outpoint. The SDK can't sign an input correctly without that context, so -`TransactionInput.utxo` exists to *attach* it directly — no node -round-trip needed. +outpoint. The SDK can't sign correctly without that context, so +`TransactionInput.utxo` *attaches* it directly — no node round-trip +needed. -A few practical consequences: +Practical consequences: -- If you build inputs by hand and forget the `utxo=...` arg, signing - will fail. Always set it. -- A signed transaction can be moved between processes (offline signer, - co-signer, relay) without the receiving side needing access to the - source node, because every input carries what's needed. -- The Generator does this for you — you hand it a list of - `UtxoEntryReference`s (or a [`UtxoContext`](../wallet-sdk/utxo-context.md)) - and it picks and wraps inputs internally. +- Forgetting `utxo=...` when building manually breaks signing. Always + set it. +- A signed transaction can move between processes (offline signer, + co-signer, relay) without the receiver needing the source node — + every input carries what's needed. +- The Generator handles this — pass a list of `UtxoEntryReference`s + (or a [`UtxoContext`](../wallet-sdk/utxo-context.md)) and it picks + and wraps inputs internally. ## UTXO selection @@ -77,11 +77,11 @@ building manually: - Sum the input values you intend to spend. - Subtract output amounts and fee — the leftover becomes the change output. -- Order matters only insofar as your downstream consumers care; protocol - rules don't impose an order. +- Order only matters to your downstream consumers; the protocol + doesn't impose one. -For input ordering rules, signature aggregation, or "spend exactly these -UTXOs first" semantics, see the Generator's `priority_entries` option. +For input ordering rules, signature aggregation, or "spend exactly +these UTXOs first", see the Generator's `priority_entries` option. ## Reading inputs back @@ -94,12 +94,14 @@ for inp in tx.inputs: print(inp.utxo.amount, inp.utxo.script_public_key) ``` -`signature_script_as_hex` returns the unlocking script after signing as -a hex string (or `None` if the input hasn't been signed yet). +`signature_script_as_hex` returns the unlocking script after signing +as a hex string, or `None` if not yet signed. ## Where to next - [Outputs](outputs.md) — the other half of a transaction. - [Signing](signing.md) — what "filled at sign time" actually does. +- [Mass & Fees](mass-and-fees.md) — `sig_op_count` feeds the mass + calculator. - [UTXO Context](../wallet-sdk/utxo-context.md) — managed UTXO state the SDK keeps in sync with the chain. diff --git a/docs/learn/transactions/mass-and-fees.md b/docs/learn/transactions/mass-and-fees.md index 7b7dba4e..30847eda 100644 --- a/docs/learn/transactions/mass-and-fees.md +++ b/docs/learn/transactions/mass-and-fees.md @@ -1,26 +1,25 @@ # Mass & fees -Kaspa uses a **mass-based fee model**. Every transaction has a *mass* — -a number derived from its byte size, its compute cost, and a storage -component that depends on input and output values. The required fee is -`mass × fee_rate`, where `fee_rate` is the prevailing rate the network -sets based on congestion. +Kaspa uses a **mass-based fee model**. Every transaction has a *mass* +— a number derived from its byte size, compute cost, and a storage +component tied to input and output values. The required fee is +`mass × fee_rate`, where `fee_rate` is the network's current rate. -For the protocol-level view of why mass exists, see -[Kaspa Concepts](../concepts.md). This page covers the SDK helpers. +For the protocol view of why mass exists, see +[Kaspa Concepts](../concepts.md#mass-and-the-fee-market). This page +covers the SDK helpers. ## The two kinds of mass -- **Compute / size mass** — derived from the transaction's serialized - size and signature operations. -- **Storage mass** — derived from input and output *values*, specifically - to discourage outputs that bloat the UTXO set (many tiny outputs from - one large input). +- **Compute / size mass** — from the transaction's serialized size + and signature operations. +- **Storage mass** — from input and output *values*, specifically to + discourage UTXO-set bloat (many tiny outputs from one large input). -The total mass is the larger of the two. You don't compute the parts +Total mass is the larger of the two. You don't compute the parts separately for normal use — `calculate_transaction_mass` and -`update_transaction_mass` handle it. `calculate_storage_mass` is exposed -for when you want to inspect the storage component on its own. +`update_transaction_mass` handle it. `calculate_storage_mass` is +exposed when you want to inspect the storage component alone. ## Compute mass for a transaction @@ -38,17 +37,17 @@ print(maximum_standard_transaction_mass()) # protocol upper bound `calculate_transaction_mass(network_id, tx)` returns the mass without mutating the transaction. To write it onto the transaction itself -(required before signing or serializing), use: +(required before signing or serializing): ```python update_transaction_mass("mainnet", tx) print(tx.mass) ``` -**Order matters.** Run `update_transaction_mass` after your inputs and -outputs are settled but *before* signing or serializing. The mass field -is part of the signed payload — sign first and you'll be signing over -mass=0. +**Order matters.** Run `update_transaction_mass` after inputs and +outputs are settled but *before* signing or serializing. Mass is part +of the signed payload — sign first and you'll be signing over +`mass=0`. For multisig estimation, both calls take an optional `minimum_signatures` to size the signature script correctly: @@ -69,7 +68,7 @@ storage_mass = calculate_storage_mass( ) ``` -Useful for sizing change outputs: if your "tiny change output" pushes +Useful for sizing change outputs: if a tiny change output pushes storage mass through the roof, fold it into the fee instead. ## Fees @@ -81,14 +80,14 @@ fee = calculate_transaction_fee("mainnet", tx) print(fee) # required fee in sompi ``` -`calculate_transaction_fee` returns the minimum required fee for the -transaction at the network's current rate. The result is a sompi int (or -`None` if the calculation can't be performed — typically because the -transaction is malformed). +`calculate_transaction_fee` returns the minimum required fee at the +network's current rate. The result is a sompi int, or `None` if the +calculation can't be performed (typically a malformed transaction). ## Querying the fee rate -The network exposes a fee estimator over RPC: +The network exposes a fee estimator over RPC — see +[RPC → Calls → Fees](../rpc/calls.md#fees): ```python estimate = await client.get_fee_estimate({}) @@ -98,33 +97,32 @@ print(estimate["estimate"]["lowBuckets"]) ``` Each bucket carries a `feerate` (sompi-per-gram-of-mass) and an -`estimatedSeconds` for "how long until this rate gets you confirmed". -Pick a bucket based on how much you care about latency, multiply by -mass, and you have a fee. +`estimatedSeconds` for time-to-confirmation at that rate. Pick a +bucket by how much you care about latency, multiply by mass, and +you have a fee. -The Wallet wraps this as `fee_rate_estimate()` (see -[Send Transaction](../wallet/send-transaction.md)) and the Generator -picks a sensible default if you don't pass `fee_rate=` explicitly. +The Wallet wraps this as +[`fee_rate_estimate()`](../wallet/send-transaction.md#fees) and the +Generator picks a sensible default if you don't pass `fee_rate=` +explicitly. ## When to set fees explicitly -The [Generator](../wallet-sdk/tx-generator.md) picks a fee rate, computes -mass, and folds the leftover into a change output. You only need to -override: +The [Generator](../wallet-sdk/tx-generator.md) picks a fee rate, +computes mass, and folds the leftover into change. Override only: -- **`fee_rate=`** — when you have a specific sompi-per-gram you want to - pay. -- **`priority_fee=`** — when you want to add a flat surcharge on top of - the computed fee. -- **Manual path** — when you're building the transaction yourself and - need to size change outputs around the fee. +- **`fee_rate=`** — when you have a specific sompi-per-gram in mind. +- **`priority_fee=`** — to add a flat surcharge on top of the computed + fee. +- **Manual path** — when building the transaction yourself and sizing + change outputs around the fee. For typical sends, the defaults are fine. ## Where to next - [Signing](signing.md) — runs after mass, before submission. -- [Submission](submission.md) — `submit_transaction` and what counts as - confirmed. -- [Kaspa Concepts → Mass and the fee market](../concepts.md) — protocol - background on why mass is shaped this way. +- [Submission](submission.md) — `submit_transaction` and what counts + as confirmed. +- [Kaspa Concepts → Mass and the fee market](../concepts.md#mass-and-the-fee-market) + — protocol background on why mass is shaped this way. diff --git a/docs/learn/transactions/metadata.md b/docs/learn/transactions/metadata.md index 681470bc..8aa60093 100644 --- a/docs/learn/transactions/metadata.md +++ b/docs/learn/transactions/metadata.md @@ -1,9 +1,10 @@ # Metadata fields -Beyond inputs and outputs, a `Transaction` carries five fields that -affect how it's interpreted on-chain. For typical sends they all take -defaults — this page documents what they are so you know what to leave -alone and what to set when you're doing something specific. +Beyond inputs and outputs, a +[`Transaction`](../../reference/Classes/Transaction.md) carries five +fields that affect how it's interpreted on-chain. Typical sends take +the defaults — this page documents what they are so you know what to +leave alone and what to set deliberately. ```python Transaction( @@ -20,50 +21,49 @@ Transaction( ## `version` -Transaction format version. Use `0` — the only currently-defined version -on Kaspa. The field exists so the protocol can introduce future -formats; until then there's nothing to choose. +Transaction format version. Use `0` — the only currently-defined +version on Kaspa. The field exists for future formats; until then, +there's nothing to choose. ## `lock_time` -The earliest moment a transaction is allowed into a block. Encoded as a -DAA-score threshold. `0` means "no lock" and is what you want unless -you're building a time-locked construct (e.g. a refund branch in a -contract). +The earliest moment a transaction is allowed into a block, encoded as +a DAA-score threshold. `0` means "no lock" — what you want unless +building a time-locked construct (e.g. a refund branch). ```python Transaction(..., lock_time=0, ...) ``` -If you set this, the transaction is rejected from blocks whose DAA score -is below the threshold. See [Kaspa Concepts → Virtual chain and DAA -score](../concepts.md) for what DAA score is. +A non-zero value rejects the transaction from blocks whose DAA score +is below the threshold. See +[Kaspa Concepts → Virtual chain and DAA score](../concepts.md#virtual-chain-and-daa-score). ## `subnetwork_id` The subnetwork the transaction belongs to. Most transactions live on -the default subnetwork — id all zeros — and that's what you should pass -when building manually: +the default subnetwork — id all zeros — and that's what you pass when +building manually: ```python subnetwork_id="0000000000000000000000000000000000000000" ``` -The field exists for protocol extensions; non-default subnetwork IDs are -reserved for specific protocol-level transaction kinds (coinbase, etc.) -that you generally don't construct from the SDK. +Non-default subnetwork IDs are reserved for protocol-level transaction +kinds (coinbase, etc.) that you generally don't construct from the +SDK. ## `gas` -Reserved for subnetwork transactions that have a compute-cost component. -On the default subnetwork it must be `0`. Pair it with +Reserved for subnetwork transactions with a compute-cost component. +On the default subnetwork it must be `0`. Pair with `subnetwork_id="00...0"` and forget about it. ## `payload` Arbitrary bytes attached to the transaction. The closest analog in Bitcoin terms is `OP_RETURN`-style data, but `payload` lives at the -transaction level rather than inside a script. +transaction level, not inside a script. ```python Transaction(..., payload="68656c6c6f", ...) # hex string @@ -72,13 +72,13 @@ Transaction(..., payload=b"hello", ...) # or raw bytes Use cases: -- **Application-level metadata** that needs to ride alongside a payment - (an invoice ID, a memo, a reference number). -- **Protocol-level data** for systems that build on top of Kaspa +- **Application-level metadata** riding alongside a payment (invoice + ID, memo, reference number). +- **Protocol-level data** for systems built on top of Kaspa transactions. -What it's not for: a substitute for cryptographic state. Payload bytes -get hashed into the transaction ID and signed over, but they don't bind +It's not a substitute for cryptographic state. Payload bytes are +hashed into the transaction ID and signed over, but they don't bind the transaction to anything off-chain on their own. The Generator accepts `payload=` directly: @@ -89,16 +89,16 @@ Generator(..., payload=b"invoice-12345") ## `mass` -The transaction's mass. Set to `0` at construction; populate it with -`update_transaction_mass(network_id, tx)` after inputs and outputs are -finalized, before signing or serializing. See +The transaction's mass. `0` at construction; populate with +`update_transaction_mass(network_id, tx)` after inputs and outputs +are finalized and before signing or serializing. See [Mass & fees](mass-and-fees.md). ## Where to next -- [Mass & fees](mass-and-fees.md) — the one metadata field you *do* - have to update. -- [Serialization](serialization.md) — how these fields ride through - `to_dict()` / `from_dict()`. +- [Mass & fees](mass-and-fees.md) — the one metadata field you *must* + update. +- [Serialization](serialization.md) — how these fields round-trip + through `to_dict()` / `from_dict()`. - [Kaspa Concepts](../concepts.md) — subnetworks, DAA score, virtual chain. diff --git a/docs/learn/transactions/outputs.md b/docs/learn/transactions/outputs.md index a7805dda..6b2eb30f 100644 --- a/docs/learn/transactions/outputs.md +++ b/docs/learn/transactions/outputs.md @@ -1,8 +1,8 @@ # Outputs -A transaction's outputs are the new UTXOs it creates. Each one carries a +A transaction's outputs are the new UTXOs it creates. Each carries a value (in sompi) and a *locking script* — the conditions a future -spender will have to satisfy. +spender must satisfy. ## Types involved @@ -16,15 +16,16 @@ ScriptPublicKey script (hex bytes — the lockup conditions) ``` -`TransactionOutput` pairs the amount with the script that locks it. -`ScriptPublicKey` is the script itself: a version byte plus the encoded -program (the bytes that whoever spends this output later will have to -satisfy). +[`TransactionOutput`](../../reference/Classes/TransactionOutput.md) +pairs the amount with the script that locks it. +[`ScriptPublicKey`](../../reference/Classes/ScriptPublicKey.md) is the +script itself: a version byte plus the encoded program a future +spender must satisfy. ## Build an output Pay-to-address is the common case. Build the lockup script with -`pay_to_address_script`: +[`pay_to_address_script`](../../reference/Functions/pay_to_address_script.md): ```python from kaspa import Address, TransactionOutput, pay_to_address_script @@ -45,16 +46,16 @@ from kaspa import NetworkType, address_from_script_public_key addr = address_from_script_public_key(out.script_public_key, NetworkType.Mainnet) ``` -That second call needs a network argument because the script itself -doesn't carry the prefix; you have to tell the decoder which network -you're displaying for. +That call needs a network argument because the script doesn't carry +the prefix — you have to tell the decoder which network you're +displaying for. ## Pay-to-script-hash -For multisig and other custom scripts, lockups go through a script hash. -`pay_to_script_hash_script(redeem_script)` produces the locking side; -the spender later supplies the redeem script plus signatures via -`pay_to_script_hash_signature_script(...)` at sign time. +For multisig and other custom scripts, lockups go through a script +hash. `pay_to_script_hash_script(redeem_script)` produces the locking +side; the spender later supplies the redeem script plus signatures +via `pay_to_script_hash_signature_script(...)` at sign time. ```python from kaspa import pay_to_script_hash_script @@ -63,13 +64,13 @@ spk = pay_to_script_hash_script(redeem_script_bytes) out = TransactionOutput(value=amount, script_public_key=spk) ``` -For the multisig flow (creating the address, signing with multiple -cosigners, submitting), see the +For the multisig flow (address creation, multi-cosigner signing, +submission), see the [Multi-signature transactions](../../guides/multisig.md) recipe. ## Change outputs -When your selected inputs sum to more than `amount + fee`, the leftover +When selected inputs sum to more than `amount + fee`, the leftover goes to a change output you control: ```python @@ -79,14 +80,14 @@ outputs = [ ] ``` -The [Generator](../wallet-sdk/tx-generator.md) computes `change_amount` -for you (selected total − outputs − fee) and writes the change output -last. When building manually, you do the arithmetic, including -re-checking it after `update_transaction_mass` if the fee shifted. +The [Generator](../wallet-sdk/tx-generator.md) computes +`change_amount` for you (selected total − outputs − fee) and writes +change last. When building manually, do the arithmetic yourself — +including re-checking after `update_transaction_mass` if the fee +shifted. -If `change_amount` is too small to be worth a separate output, fold it -into the fee instead — paying a slightly inflated fee beats producing -dust. +If `change_amount` is too small to be worth a separate output, fold +it into the fee — paying a slightly inflated fee beats dust. ## Sompi vs KAS @@ -100,8 +101,8 @@ kaspa_to_sompi(1.5) # 150_000_000 sompi_to_kaspa(150_000_000) # 1.5 ``` -Convert at the UI boundary only. Don't store KAS as a float anywhere -internal; everything in the SDK assumes integer sompi. +Convert only at the UI boundary. Don't store KAS as a float +internally — everything in the SDK assumes integer sompi. ## Reading outputs back diff --git a/docs/learn/transactions/overview.md b/docs/learn/transactions/overview.md index 5179b1f0..53eaa8f4 100644 --- a/docs/learn/transactions/overview.md +++ b/docs/learn/transactions/overview.md @@ -1,18 +1,21 @@ # Transactions -A transaction in Kaspa is the same shape as in any UTXO chain: a list of -inputs (each spending a previous output), a list of outputs, and a few -metadata fields. The SDK exposes the underlying types — `Transaction`, -`TransactionInput`, `TransactionOutput`, `TransactionOutpoint`, -`UtxoEntryReference` — and the helpers that build, sign, mass, and -serialise them. +A Kaspa transaction has the same shape as on any UTXO chain: a list +of inputs (each spending a previous output), a list of outputs, and +a few metadata fields. The SDK exposes the underlying types — +[`Transaction`](../../reference/Classes/Transaction.md), +[`TransactionInput`](../../reference/Classes/TransactionInput.md), +[`TransactionOutput`](../../reference/Classes/TransactionOutput.md), +[`TransactionOutpoint`](../../reference/Classes/TransactionOutpoint.md), +[`UtxoEntryReference`](../../reference/Classes/UtxoEntryReference.md) +— and helpers that build, sign, mass, and serialise them. Most of the time you'll use the higher-level -[Transaction Generator](../wallet-sdk/tx-generator.md) (or the managed -[Wallet](../wallet/send-transaction.md) on top of it). The pages in this -section cover the primitives underneath, so you can build manually when -you need to — custom lockup scripts, exact input ordering, payload data, -offline signing. +[Transaction Generator](../wallet-sdk/tx-generator.md) (or the +managed [Wallet](../wallet/send-transaction.md) on top of it). This +section covers the primitives underneath, so you can build manually +when you need custom lockup scripts, exact input ordering, payload +data, or offline signing. ## Anatomy @@ -34,18 +37,18 @@ TransactionOutput script_public_key (lockup script) ``` -A few things that distinguish Kaspa from a Bitcoin-shaped chain: +What sets Kaspa apart from a Bitcoin-shaped chain: - **Inputs carry their own UTXO context** via `UtxoEntryReference`, so - the signer doesn't have to re-fetch the spent output to know its amount + the signer doesn't have to re-fetch the spent output for its amount and lockup. See [Inputs](inputs.md). -- **Mass replaces "byte size × rate"** as the fee model. You compute mass - on the transaction (including a storage component derived from input - and output values), then multiply by the prevailing fee rate. See - [Mass & fees](mass-and-fees.md). -- **The atomic unit is the sompi**: `1 KAS = 100_000_000 sompi`. Every - amount in the transaction surface is a sompi int — convert at the UI - boundary only. +- **Mass replaces "byte size × rate"** as the fee model. Compute mass + on the transaction (including a storage component derived from + input and output values), then multiply by the prevailing fee rate. + See [Mass & fees](mass-and-fees.md). +- **The atomic unit is the sompi**: `1 KAS = 100_000_000 sompi`. + Every amount in the transaction surface is a sompi int — convert + only at the UI boundary. ## End-to-end (manual path) @@ -92,16 +95,17 @@ await client.submit_transaction({ ``` This is what the [Generator](../wallet-sdk/tx-generator.md) does -internally — it picks UTXOs, computes mass, signs, and yields one or more -ready-to-submit `PendingTransaction`s. Reach for the manual path when you -need control the Generator doesn't expose. +internally — it picks UTXOs, computes mass, signs, and yields one or +more ready-to-submit `PendingTransaction`s. Reach for the manual path +when you need control the Generator doesn't expose. ## In this section -- **[Inputs](inputs.md)** — `TransactionInput`, `TransactionOutpoint`, - `UtxoEntryReference`, and why inputs carry their UTXO context. -- **[Outputs](outputs.md)** — `TransactionOutput`, `ScriptPublicKey`, the - lockup scripts that pay to an address. +- **[Inputs](inputs.md)** — `TransactionInput`, + `TransactionOutpoint`, `UtxoEntryReference`, and why inputs carry + their UTXO context. +- **[Outputs](outputs.md)** — `TransactionOutput`, `ScriptPublicKey`, + the lockup scripts that pay an address. - **[Mass & fees](mass-and-fees.md)** — computing mass, storage mass, the fee market, and when to call `update_transaction_mass`. - **[Signing](signing.md)** — `sign_transaction`, `SighashType`, @@ -109,14 +113,16 @@ need control the Generator doesn't expose. - **[Submission](submission.md)** — `submit_transaction`, what "submitted" means, and how confirmation works. - **[Metadata fields](metadata.md)** — `version`, `lock_time`, - `subnetwork_id`, `gas`, `payload`, the fields you mostly leave alone. + `subnetwork_id`, `gas`, `payload` — the fields you mostly leave + alone. - **[Serialization](serialization.md)** — `to_dict()` / `from_dict()` for round-tripping through other systems. ## Where to next -- [Wallet SDK → Transaction Generator](../wallet-sdk/tx-generator.md) — - the high-level coin selector + signer. +- [Wallet SDK → Transaction Generator](../wallet-sdk/tx-generator.md) + — the high-level coin selector + signer. - [Wallet → Send Transaction](../wallet/send-transaction.md) — the managed Wallet's send surface. -- [Kaspa Concepts](../concepts.md) — UTXO model, mass, fees, maturity. +- [Kaspa Concepts](../concepts.md) — UTXO model, mass, fees, + maturity. diff --git a/docs/learn/transactions/serialization.md b/docs/learn/transactions/serialization.md index c1f7fd1f..b1b4d3e2 100644 --- a/docs/learn/transactions/serialization.md +++ b/docs/learn/transactions/serialization.md @@ -1,8 +1,12 @@ # Serialization -Most transaction-shaped types — `Transaction`, `TransactionInput`, -`TransactionOutput`, `TransactionOutpoint`, `UtxoEntryReference` — -support `to_dict()` and `from_dict()`. The dict shape matches the +Most transaction-shaped types — +[`Transaction`](../../reference/Classes/Transaction.md), +[`TransactionInput`](../../reference/Classes/TransactionInput.md), +[`TransactionOutput`](../../reference/Classes/TransactionOutput.md), +[`TransactionOutpoint`](../../reference/Classes/TransactionOutpoint.md), +[`UtxoEntryReference`](../../reference/Classes/UtxoEntryReference.md) +— support `to_dict()` and `from_dict()`. The dict shape matches the wRPC wire format the node accepts and produces. ## Round-tripping @@ -26,39 +30,38 @@ ref_dict = utxo_ref.to_dict() restored_ref = UtxoEntryReference.from_dict(ref_dict) ``` -`to_dict()` produces a fresh Python dict — modifying it doesn't mutate +`to_dict()` returns a fresh Python dict — modifying it doesn't mutate the source object. `from_dict()` raises on malformed input (missing required keys, wrong types, invalid values). ## When you need this -Within a single process, you rarely need to round-trip — pass the -typed objects around. The dict form earns its place at process -boundaries: +Within a single process, you rarely need to round-trip — pass typed +objects around. The dict form earns its place at process boundaries: - **Submission** — `client.submit_transaction({"transaction": - tx.serialize_to_dict(), ...})` takes a dict, not a `Transaction`. See - [Submission](submission.md). -- **Offline signing** — build on an online machine, serialize, sign on - an air-gapped one, serialize again, send back, submit. The dict form + tx.serialize_to_dict(), ...})` takes a dict, not a `Transaction`. + See [Submission](submission.md). +- **Offline signing** — build on an online machine, serialize, sign + on an air-gapped one, serialize again, send back, submit. The dict is the natural transport. - **Co-signer flows** — multisig where each cosigner signs in turn. - Each step ships a dict; the next signer reads it back, signs, and - forwards. -- **Persistence** — saving a pending transaction to disk or a queue for - later submission. Store the dict (as JSON), not the typed object. + Each step ships a dict; the next signer reads, signs, and forwards. +- **Persistence** — saving a pending transaction to disk or a queue. + Store the dict (as JSON), not the typed object. ## `serialize_to_dict` vs `to_dict` Both produce a dict matching the wRPC wire shape. `to_dict` is the general-purpose Python conversion; `serialize_to_dict` (on `Transaction`) is the form `submit_transaction` expects. In practice -they produce equivalent shapes — use `serialize_to_dict` when you're -about to submit, `to_dict` when you're shuttling the object somewhere -else. +they produce equivalent shapes — use `serialize_to_dict` when about +to submit, `to_dict` when shuttling the object somewhere else. ## Where to next - [Submission](submission.md) — where the dict form actually goes. - [Inputs](inputs.md) and [Outputs](outputs.md) — the typed objects these dicts represent. +- [Metadata fields](metadata.md) — how transaction-level fields ride + through serialization. diff --git a/docs/learn/transactions/signing.md b/docs/learn/transactions/signing.md index ee321398..d772e94b 100644 --- a/docs/learn/transactions/signing.md +++ b/docs/learn/transactions/signing.md @@ -1,13 +1,13 @@ # Signing -Signing fills in each input's `signature_script` with proof that the -spender controls the key the corresponding UTXO is locked to. Kaspa -defaults to **Schnorr** signatures over the secp256k1 curve; ECDSA is -also supported for inputs locked to ECDSA addresses. Multisig inputs -combine multiple signatures under a script-hash lockup. +Signing fills each input's `signature_script` with proof that the +spender controls the key the UTXO is locked to. Kaspa defaults to +**Schnorr** signatures over secp256k1; ECDSA is supported for inputs +locked to ECDSA addresses. Multisig inputs combine multiple +signatures under a script-hash lockup. -For Schnorr-vs-ECDSA on the addressing side, see -[Addresses → Versions](../addresses.md). +For Schnorr vs ECDSA on the addressing side, see +[Addresses → Versions](../addresses.md#versions). ## Sign a manually built transaction @@ -18,16 +18,17 @@ update_transaction_mass("mainnet", tx) # do this first — mass is signed signed = sign_transaction(tx, [private_key], verify_sig=True) ``` -`sign_transaction(tx, signers, verify_sig)`: +[`sign_transaction(tx, signers, verify_sig)`](../../reference/Functions/sign_transaction.md): -- `signers` — a list of `PrivateKey`. The signer for each input is - inferred from the input's UTXO lockup; pass every key that any input - needs. -- `verify_sig=True` — verifies each signature after writing it. Cheap - insurance during development; you can disable it in performance- - sensitive paths once you trust the inputs. +- `signers` — a list of + [`PrivateKey`](../../reference/Classes/PrivateKey.md). The signer + for each input is inferred from the input's UTXO lockup; pass every + key any input needs. +- `verify_sig=True` — verify each signature after writing it. Cheap + insurance during development; disable in performance-sensitive + paths once you trust the inputs. -Sign before submission, after mass is filled in. Mass is part of the +Sign after mass is filled in, before submission. Mass is part of the signed payload, so changing inputs, outputs, or mass *after* signing invalidates the signature. @@ -57,9 +58,10 @@ signers (mixed-key wallets, partially-co-signed flows). ## SighashType -The hash that gets signed describes *which parts of the transaction* the -signature commits to. `SighashType.All` is the default and the only one -most code should use. +The hash that gets signed describes *which parts of the transaction* +the signature commits to. +[`SighashType.All`](../../reference/Enums/SighashType.md) is the +default and the only one most code should use. ```python from kaspa import SighashType @@ -71,19 +73,19 @@ print(list(SighashType)) - **`All`** — signs every input and every output. Standard. - **`_None`** — signs inputs only; outputs can be modified. Rare; underscore-prefixed because `None` is a Python keyword. -- **`Single`** — signs the input being spent and the matching output by - index. +- **`Single`** — signs the input being spent and the matching output + by index. - **`*AnyOneCanPay`** — variants that *don't* sign the other inputs, letting cosigners add inputs after the fact. -In practice, leave it at `All` unless you have a specific protocol or -co-signing reason. The non-`All` modes are for advanced flows like -collaborative coin joins. +Leave it at `All` unless you have a specific protocol or co-signing +reason. The non-`All` modes are for advanced flows like collaborative +coin joins. ## Build a signature without filling the input -When you need the raw signature bytes — for example, to send to a -co-signer for aggregation — use `create_input_signature`: +When you need raw signature bytes — e.g. to send to a co-signer for +aggregation — use `create_input_signature`: ```python from kaspa import SighashType, create_input_signature @@ -96,29 +98,29 @@ sig_hex = create_input_signature( ) ``` -The same method exists on `PendingTransaction` (`pending.create_input_signature(...)`) -and you can write the resulting script back with `pending.fill_input(...)`. +The same method exists on `PendingTransaction` +(`pending.create_input_signature(...)`); write the resulting script +back with `pending.fill_input(...)`. ## Multisig and sig_op_count Two fields interact with mass when you sign: -- **`sig_op_count`** on each input — how many signature ops that input - actually performs. `1` for a single-key spend, `M` for an `M`-of-`N` - multisig. -- **`minimum_signatures`** passed to `update_transaction_mass(..., - minimum_signatures=M)` and `calculate_transaction_mass` — tells the - mass calculator how big the signature script will be when it's filled - in. - -If either is wrong, mass will be wrong, and the resulting fee will be -either rejected (too low) or wasted (too high). The Generator handles -this when you pass `sig_op_count` and `minimum_signatures` to the -constructor. - -For the full multisig flow (creating the address, signing with multiple -cosigners, submitting), see the -[Multi-signature transactions](../../guides/multisig.md) recipe. +- **`sig_op_count`** on each input — number of signature ops the + input actually performs. `1` for a single-key spend, `M` for an + `M`-of-`N` multisig. +- **`minimum_signatures`** passed to + `update_transaction_mass(..., minimum_signatures=M)` and + `calculate_transaction_mass` — tells the mass calculator how big + the filled-in signature script will be. + +Wrong values yield wrong mass and either rejected (too low) or wasted +(too high) fees. The Generator handles this when you pass +`sig_op_count` and `minimum_signatures` to the constructor. + +For the full multisig flow (address creation, multi-cosigner signing, +submission), see +[Multi-signature transactions](../../guides/multisig.md). ## Where to next diff --git a/docs/learn/transactions/submission.md b/docs/learn/transactions/submission.md index e6da56e2..49afffd9 100644 --- a/docs/learn/transactions/submission.md +++ b/docs/learn/transactions/submission.md @@ -1,10 +1,11 @@ # Submission & confirmation -Submitting a signed transaction hands it to a node, which gossips it to -the network and includes it in a block when capacity allows. The SDK -gives you two surfaces: `pending.submit(client)` for transactions -produced by the [Generator](../wallet-sdk/tx-generator.md), and -`client.submit_transaction(...)` for everything else. +Submitting a signed transaction hands it to a node, which gossips it +to the network and includes it in a block when capacity allows. Two +surfaces: `pending.submit(client)` for transactions produced by the +[Generator](../wallet-sdk/tx-generator.md), and +[`client.submit_transaction(...)`](../rpc/calls.md#transactions-and-mempool) +for everything else. ## From a PendingTransaction @@ -14,8 +15,8 @@ print(tx_id) ``` `pending.submit` serializes the underlying `Transaction` and calls -`submit_transaction` for you. This is the right path for transactions -the Generator built — including the managed +`submit_transaction` for you. The right path for Generator-built +transactions — including the managed [Wallet](../wallet/send-transaction.md), which is built on the Generator. @@ -31,41 +32,41 @@ print(result["transactionId"]) The request takes: -- **`transaction`** — the wire-format dict you get from - `Transaction.serialize_to_dict()` (or via the equivalent +- **`transaction`** — the wire-format dict from + `Transaction.serialize_to_dict()` (or `pending.transaction.serialize_to_dict()`). -- **`allowOrphan`** — whether to keep the transaction in mempool when - one of its inputs hasn't been seen yet (e.g. you submitted a chain of - transactions out of order). Default to `False` unless you know you're - submitting a chain. +- **`allowOrphan`** — whether to keep the transaction in the mempool + when an input hasn't been seen yet (e.g. submitting a chain out of + order). Default `False` unless you know you're submitting a chain. -The manual path is the right call when you need to ship the transaction -through another system — a co-signer, a relay, an offline signer — -before the node sees it. For round-tripping through that other system, -see [Serialization](serialization.md). +Use the manual path when shipping the transaction through another +system — a co-signer, relay, or offline signer — before the node +sees it. See [Serialization](serialization.md) for round-tripping. ## What "submitted" means Submission is *acceptance into the mempool*, not confirmation. The -return value is the transaction ID; the transaction is now eligible to -be included in a block. +return value is the transaction ID; the transaction is now eligible +for inclusion in a block. A Kaspa transaction lifecycle has three observable states: -- **In mempool** — accepted by the node, waiting for inclusion. The - transaction ID is returned from `submit_transaction`. +- **In mempool** — accepted by the node, waiting for inclusion. ID + returned from `submit_transaction`. - **Virtual-chain accepted** — included in a block that's part of the canonical DAG ordering. Surfaces via the - `virtual-chain-changed` notification. -- **Mature** — confirmed past the maturity threshold; the new UTXOs are + [`virtual-chain-changed`](../rpc/subscriptions.md#virtual-chain-progression) + notification. +- **Mature** — confirmed past the maturity threshold; new UTXOs are spendable. Surfaces via a `Maturity` event on the managed [Wallet](../wallet/transaction-history.md) or the [`UtxoProcessor`](../wallet-sdk/utxo-processor.md). -For confirmation, the right gate is the `Maturity` event for the +The right gate for confirmation is the `Maturity` event for the specific transaction, not "wait N seconds" and not the first time it -appears in a block. See [Kaspa Concepts → Maturity](../concepts.md) for -the protocol view. +appears in a block. See +[Kaspa Concepts → Maturity](../concepts.md#maturity) for the protocol +view. ## Failures and retries @@ -74,19 +75,19 @@ Common reasons: - **`fee too low`** — recompute mass with `update_transaction_mass` *after* any input/output change, then re-sign. -- **`orphan`** — an input references a transaction the node hasn't seen. - Either wait for the parent to land, or set `allowOrphan=True` when you - intentionally submit a chain. +- **`orphan`** — an input references a transaction the node hasn't + seen. Wait for the parent to land, or set `allowOrphan=True` when + intentionally submitting a chain. - **`already in mempool`** — the same `transaction_id` is already pending. Safe to ignore for retries. - **`mass exceeded`** — the transaction is over `maximum_standard_transaction_mass()`. Split the inputs across - multiple transactions; the Generator does this automatically when its - input set is too large. + multiple transactions; the Generator does this automatically when + its input set is too large. -A transaction that was virtual-chain accepted *can* be reorged out — at -which point its outputs are no longer mature. The SDK surfaces this as -a `Reorg` event; see +A virtual-chain-accepted transaction *can* be reorged out — at which +point its outputs are no longer mature. The SDK surfaces this as a +`Reorg` event; see [Wallet → Transaction History](../wallet/transaction-history.md). ## Where to next @@ -95,4 +96,5 @@ a `Reorg` event; see managed Wallet's send surface, which wraps all of this. - [Wallet → Transaction History](../wallet/transaction-history.md) — observing maturity and reorgs. -- [Kaspa Concepts](../concepts.md) — virtual chain, DAA score, maturity. +- [Kaspa Concepts](../concepts.md) — virtual chain, DAA score, + maturity. diff --git a/docs/learn/wallet-sdk/derivation.md b/docs/learn/wallet-sdk/derivation.md index 6a8827ab..9d6b26ae 100644 --- a/docs/learn/wallet-sdk/derivation.md +++ b/docs/learn/wallet-sdk/derivation.md @@ -1,8 +1,8 @@ # Derivation -Once you have an `XPrv` (see [Key Management](key-management.md)), -derivation produces every other key the wallet uses. Kaspa follows -BIP-44 with coin type `111111`: +Once you have an [`XPrv`](../../reference/Classes/XPrv.md) (see +[Key Management](key-management.md)), derivation produces every other +key the wallet uses. Kaspa follows BIP-44 with coin type `111111`: ``` m / 44' / 111111' / account' / chain / address_index @@ -29,7 +29,8 @@ print(xprv.depth) # 0 for the master print(xprv.chain_code) # 32 bytes ``` -`XPub` is the public counterpart — useful for watch-only wallets: +[`XPub`](../../reference/Classes/XPub.md) is the public counterpart — +useful for watch-only wallets: ```python xpub = xprv.to_xpub() @@ -54,8 +55,8 @@ path = DerivationPath("m/44'/111111'/0'/0/0") leaf = xprv.derive_path(path) ``` -`DerivationPath` is mutable — handy when you want to walk a chain -incrementally: +[`DerivationPath`](../../reference/Classes/DerivationPath.md) is +mutable — handy for walking a chain incrementally: ```python path = DerivationPath("m/44'/111111'/0'") @@ -67,8 +68,9 @@ print(path.parent().to_string()) ## `PrivateKeyGenerator` -For everyday "give me address `i`" derivation, use `PrivateKeyGenerator` — -it handles the full BIP-44 path for you: +For everyday "give me address `i`" derivation, use +[`PrivateKeyGenerator`](../../reference/Classes/PrivateKeyGenerator.md) +— it handles the full BIP-44 path for you: ```python from kaspa import PrivateKeyGenerator, NetworkType @@ -89,8 +91,9 @@ change = gen.change_key(0) # m/44'/111111'/0'/1/0 ## `PublicKeyGenerator` (watch-only) -When you only need addresses — no signing — `PublicKeyGenerator` derives -them from an `xpub` alone: +When you only need addresses — no signing — +[`PublicKeyGenerator`](../../reference/Classes/PublicKeyGenerator.md) +derives them from an `xpub` alone: ```python from kaspa import PublicKeyGenerator, NetworkType @@ -107,8 +110,8 @@ addrs = pub.receive_addresses(NetworkType.Mainnet, start=0, end=10) pubkeys = pub.receive_pubkeys(start=0, end=5) ``` -Or, if you have an `XPrv` but want a public-key-only generator (e.g. -a watch-only mode in the same process): +If you have an `XPrv` but want a public-key-only generator (e.g. +watch-only mode in the same process): ```python pub = PublicKeyGenerator.from_master_xprv( @@ -138,9 +141,10 @@ spending from it), see the ## Account-kind tag -`AccountKind` is the metadata type the wallet uses to track which -derivation rules apply. You only construct one explicitly when calling -the wallet's account-creation methods: +[`AccountKind`](../../reference/Classes/AccountKind.md) is the +metadata type the wallet uses to track which derivation rules apply. +Construct one explicitly only when calling the wallet's +account-creation methods: ```python from kaspa import AccountKind diff --git a/docs/learn/wallet-sdk/key-management.md b/docs/learn/wallet-sdk/key-management.md index 7d798ed9..4acb31c1 100644 --- a/docs/learn/wallet-sdk/key-management.md +++ b/docs/learn/wallet-sdk/key-management.md @@ -1,10 +1,11 @@ # Key Management -Everything in this page is BIP-39-compatible. The SDK gives you `Mnemonic` -for the human-readable phrase, the seed bytes that come out of it, and -`XPrv` for the master extended private key the seed produces. From an -`XPrv` you derive child keys — that's the next page, -[Derivation](derivation.md). +Everything on this page is BIP-39 compatible. The SDK gives you +[`Mnemonic`](../../reference/Classes/Mnemonic.md) for the +human-readable phrase, the seed bytes it produces, and +[`XPrv`](../../reference/Classes/XPrv.md) for the master extended +private key from that seed. From an `XPrv` you derive child keys — +that's the next page, [Derivation](derivation.md). Read [Security](../../getting-started/security.md) before generating a real mnemonic. @@ -20,7 +21,8 @@ print(m.phrase) ``` 24 words is the recommended default — more entropy, lower brute-force -risk. 12 words is supported for compatibility with tools that emit them. +risk. 12 words is supported for compatibility with tools that emit +them. ## Restore from a mnemonic @@ -35,8 +37,8 @@ else: raise ValueError("invalid mnemonic") ``` -`Mnemonic.validate(phrase)` checks word membership, length, and the BIP-39 -checksum. It returns a bool — it does not raise. +`Mnemonic.validate(phrase)` checks word membership, length, and the +BIP-39 checksum. Returns a bool; never raises. ## Validation @@ -60,15 +62,15 @@ xprv = XPrv(seed) ``` !!! info "Passphrase" - The optional passphrase (sometimes called the "25th word") changes - the seed. The same mnemonic with different passphrases produces - different wallets. An attacker who recovers the mnemonic alone gets - nothing without the passphrase. + The optional passphrase (the "25th word") changes the seed. The + same mnemonic with different passphrases produces different + wallets. An attacker who recovers the mnemonic alone gets nothing + without the passphrase. ## Inspect entropy -The `entropy` property exposes the underlying random bits as a hex string -— the raw input the BIP-39 phrase encodes: +The `entropy` property exposes the underlying random bits as a hex +string — the raw input the BIP-39 phrase encodes: ```python m = Mnemonic.random() @@ -76,7 +78,7 @@ print(m.entropy) # hex m.entropy = "" # advanced; rebuilds the phrase ``` -You rarely need to set `entropy` directly. The two cases that come up: +You rarely need to set `entropy` directly. Two cases that come up: re-creating a `Mnemonic` from entropy emitted by another tool, and debugging a vector mismatch against a third-party implementation. @@ -88,13 +90,13 @@ from kaspa import Mnemonic, Language m = Mnemonic(phrase, Language.English) ``` -English is the default and is what every Kaspa example uses. The other -BIP-39 wordlists exist on the enum but are rarely used in practice — if -you don't know you need a non-English wordlist, use English. +English is the default and what every Kaspa example uses. Other +BIP-39 wordlists exist on the enum but are rarely used — if you don't +know you need a non-English wordlist, use English. ## Hex private keys (`SecretKey`) -For one-key accounts (a single secp256k1 secret with no derivation), +For one-key accounts (a single secp256k1 secret, no derivation), skip the mnemonic entirely: ```python @@ -105,8 +107,9 @@ addr = key.to_address("testnet-10") ``` The 64-char hex string is what -[Wallet → Keypair Accounts](../wallet/keypair.md) takes as the `secret` -input to `prv_key_data_create(kind=PrvKeyDataVariantKind.SecretKey)`. +[Wallet → Keypair Accounts](../wallet/keypair.md) takes as the +`secret` input to +`prv_key_data_create(kind=PrvKeyDataVariantKind.SecretKey)`. ## Where to next diff --git a/docs/learn/wallet-sdk/overview.md b/docs/learn/wallet-sdk/overview.md index b3551951..c8725bcf 100644 --- a/docs/learn/wallet-sdk/overview.md +++ b/docs/learn/wallet-sdk/overview.md @@ -1,10 +1,10 @@ # Wallet SDK -The **Wallet SDK** section is the layer beneath the managed -[Wallet](../wallet/overview.md). When you don't need on-disk file storage, -multi-account management, or the wallet's event multiplexer — when you -just want to derive a key, build a transaction, or track UTXOs for a few -addresses — drop down here. +The **Wallet SDK** is the layer beneath the managed +[Wallet](../wallet/overview.md). Drop down here when you don't need +on-disk file storage, multi-account management, or the wallet's event +multiplexer — when you just want to derive a key, build a transaction, +or track UTXOs for a few addresses. ## What lives here @@ -25,12 +25,12 @@ addresses — drop down here. | Derive an address from a mnemonic without persisting anything | Wallet SDK ([Key Management](key-management.md), [Derivation](derivation.md)) | | Watch a fixed set of addresses for incoming UTXOs without a wallet file | Wallet SDK ([UTXO Processor](utxo-processor.md), [UTXO Context](utxo-context.md)) | -The managed `Wallet` is built from these pieces — every primitive on this -page is what `Wallet` wraps internally. +The managed `Wallet` is built from these pieces — every primitive on +this page is what `Wallet` wraps internally. ## Where to next If you're new here, start with [Key Management](key-management.md) → [Derivation](derivation.md) → [Transaction Generator](tx-generator.md). -That sequence walks the typical "make a key, build a transaction, send -it" flow without any file I/O. +That sequence walks the typical "make a key, build a transaction, +send it" flow without any file I/O. diff --git a/docs/learn/wallet-sdk/tx-generator.md b/docs/learn/wallet-sdk/tx-generator.md index 5c0080ac..3ffe35da 100644 --- a/docs/learn/wallet-sdk/tx-generator.md +++ b/docs/learn/wallet-sdk/tx-generator.md @@ -1,9 +1,11 @@ # Transaction Generator -The `Generator` is the SDK's built-in coin selector and fee calculator. -You hand it UTXOs, a change address, and the outputs you want; it picks -inputs, computes mass and fees, and yields one or more -`PendingTransaction`s ready to sign and submit. +The [`Generator`](../../reference/Classes/Generator.md) is the SDK's +built-in coin selector and fee calculator. You hand it UTXOs, a change +address, and the outputs you want; it picks inputs, computes mass and +fees, and yields one or more +[`PendingTransaction`](../../reference/Classes/PendingTransaction.md)s +ready to sign and submit. ## Send a payment, end to end @@ -45,9 +47,9 @@ async def main(): asyncio.run(main()) ``` -A `Generator` is *iterable* — when the input set is too large for a single -transaction's mass budget, it yields a chain of consolidating transactions -followed by the final payment. Loop and submit each. +A `Generator` is *iterable* — when the input set is too large for one +transaction's mass budget, it yields a chain of consolidating +transactions followed by the final payment. Loop and submit each. ## Constructor options @@ -68,8 +70,9 @@ gen = Generator( ) ``` -`entries` accepts a [`UtxoContext`](utxo-context.md) directly — pass the -context and it consumes from the mature set without you copying the list. +`entries` accepts a [`UtxoContext`](utxo-context.md) directly — pass +the context and it consumes from the mature set without you copying +the list. ## Estimate before signing @@ -89,13 +92,12 @@ summary = estimate_transactions( ) ``` -`estimate()` does not consume the generator; you can iterate it for real -afterwards. +`estimate()` doesn't consume the generator — you can iterate it for +real afterwards. ## Pending transactions -Each item the generator yields exposes the proposed transaction's -metadata: +Each yielded item exposes the proposed transaction's metadata: ```python for pending in gen: @@ -106,8 +108,8 @@ for pending in gen: raw_tx = pending.transaction ``` -Use this when you need to surface fee / mass to a user before they -authorize a signature. +Use this to surface fee / mass to a user before they authorize a +signature. ## Signing @@ -143,9 +145,11 @@ result = await client.submit_transaction({ }) ``` -`pending.submit(client)` is the right path. The manual route is for cases -where you need to round-trip the transaction through another system -before submission. +`pending.submit(client)` is the right path. The manual route is for +round-tripping the transaction through another system before +submission. See +[Transactions → Submission](../transactions/submission.md) for the +`allowOrphan` semantics and confirmation states. ## One-shot helpers diff --git a/docs/learn/wallet-sdk/utxo-context.md b/docs/learn/wallet-sdk/utxo-context.md index e4410ad4..9ef62f22 100644 --- a/docs/learn/wallet-sdk/utxo-context.md +++ b/docs/learn/wallet-sdk/utxo-context.md @@ -1,16 +1,16 @@ # UTXO Context -A `UtxoContext` tracks UTXOs for a fixed set of addresses. It's bound to -a [UTXO Processor](utxo-processor.md) and gets fed by it: as the -processor receives notifications from the node, it routes the changes -into whichever contexts have registered the relevant addresses. The -context exposes the resulting UTXO set, balance, and mature/pending -splits. - -If you're using the managed [Wallet](../wallet/overview.md), it manages a -`UtxoContext` per activated account internally — you usually don't -construct one yourself. Drop down here when you want UTXO tracking -without the on-disk wallet file. +A [`UtxoContext`](../../reference/Classes/UtxoContext.md) tracks UTXOs +for a fixed set of addresses. It's bound to a +[UTXO Processor](utxo-processor.md) and fed by it: as the processor +receives notifications from the node, it routes changes to whichever +contexts have registered the relevant addresses. The context exposes +the resulting UTXO set, balance, and mature/pending splits. + +The managed [Wallet](../wallet/overview.md) creates a `UtxoContext` +per activated account internally — you usually don't construct one +yourself. Drop down here when you want UTXO tracking without an +on-disk wallet file. ## Build one @@ -27,8 +27,8 @@ context = UtxoContext(processor) await context.track_addresses(["kaspatest:qr0lr4ml..."]) ``` -`UtxoContext(processor, id=...)` accepts an optional 32-byte hex id. If -you omit it, one is generated. Use the explicit id when you need the +`UtxoContext(processor, id=...)` accepts an optional 32-byte hex id; +omitted ids are generated. Set an explicit id when you need the context to be addressable across reconnects. ## What it exposes @@ -43,8 +43,8 @@ mature = context.mature_range(from_=0, to=10) # list[UtxoEntryReference] pending = context.pending() # list[UtxoEntryReference] ``` -`balance` is `None` until the first notification arrives. After that it's -a `Balance(mature, pending, outgoing)` in sompi. +`balance` is `None` until the first notification arrives; after that +it's a `Balance(mature, pending, outgoing)` in sompi. ## Add and remove tracked addresses @@ -55,13 +55,13 @@ await context.clear() # forget every address and UTXO ``` `track_addresses` accepts `Address` instances or their string forms. -`current_daa_score=...` is optional — supply it if you want the scan to -ignore confirmations older than that score. +`current_daa_score=...` is optional — supply it to ignore +confirmations older than that score. ## Use as `Generator` input -The [Transaction Generator](tx-generator.md) accepts a `UtxoContext` as -its `entries` argument: +The [Transaction Generator](tx-generator.md) accepts a `UtxoContext` +as its `entries` argument: ```python from kaspa import Generator, PaymentOutput @@ -73,8 +73,8 @@ gen = Generator( ) ``` -This avoids snapshotting the UTXO list yourself; the generator pulls the -current mature set when it iterates. +This avoids snapshotting the UTXO list yourself — the generator pulls +the current mature set when it iterates. ## Where to next diff --git a/docs/learn/wallet-sdk/utxo-processor.md b/docs/learn/wallet-sdk/utxo-processor.md index cdec37c8..f4c88b4e 100644 --- a/docs/learn/wallet-sdk/utxo-processor.md +++ b/docs/learn/wallet-sdk/utxo-processor.md @@ -1,11 +1,11 @@ # UTXO Processor -A `UtxoProcessor` subscribes to a node's UTXO and virtual-chain -notifications and dispatches them to one or more -[UTXO Contexts](utxo-context.md). It's the engine that makes context -tracking work. If you're using the managed -[Wallet](../wallet/overview.md), one is constructed for you. If you're not, -you build one yourself and bind contexts to it. +A [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) +subscribes to a node's UTXO and virtual-chain notifications and +dispatches them to one or more [UTXO Contexts](utxo-context.md). It's +the engine that makes context tracking work. The managed +[Wallet](../wallet/overview.md) builds one internally; otherwise, +build one yourself and bind contexts to it. ## Construction @@ -23,9 +23,9 @@ await processor.stop() await client.disconnect() ``` -`start()` is what activates the processor — it begins subscribing to -node notifications and forwarding them. `stop()` is the matching -shutdown. Without `start()`, contexts bound to it stay empty. +`start()` activates the processor — it subscribes to node +notifications and starts forwarding. `stop()` is the matching +shutdown. Without `start()`, bound contexts stay empty. ## Properties @@ -37,9 +37,10 @@ shutdown. Without `start()`, contexts bound to it stay empty. ## Events -The processor has its own event surface — a smaller, lower-level cousin -of the [managed Wallet's events](../wallet/transaction-history.md). -Listeners use the same shape: +The processor has its own event surface — a smaller, lower-level +cousin of the +[managed Wallet's events](../wallet/transaction-history.md). Listeners +use the same shape: ```python def on_event(event): @@ -64,13 +65,13 @@ Common events: | `balance` | A bound `UtxoContext`'s balance changed. | | `utxo-proc-error`, `error` | Something went wrong. | -`UtxoProcessorEvent` is the enum form if you'd rather pass it as a -typed value than a string. +[`UtxoProcessorEvent`](../../reference/Enums/UtxoProcessorEvent.md) +is the enum form if you prefer a typed value over a string. ## Coordinating with `asyncio` -Listener callbacks may run on a background thread. If you need to -signal an `asyncio.Event` from one, bridge through the loop: +Listener callbacks may run on a background thread. To signal an +`asyncio.Event` from one, bridge through the loop: ```python loop = asyncio.get_running_loop() diff --git a/docs/learn/wallet/accounts.md b/docs/learn/wallet/accounts.md index 866111fa..69aa5168 100644 --- a/docs/learn/wallet/accounts.md +++ b/docs/learn/wallet/accounts.md @@ -1,13 +1,16 @@ # Accounts -A wallet holds N accounts of mixed kinds, each backed by exactly one PKD. -The two everyday kinds are **BIP32** (HD-derived; one mnemonic backs many -accounts at different `account_index`es) and **keypair** (a single -secp256k1 key, one address — see [Keypair Accounts](keypair.md)). +A wallet holds N accounts of mixed kinds, each backed by exactly one +[private key data entry](private-keys.md). The two everyday kinds: + +- **BIP32** — HD-derived; one mnemonic backs many accounts at + different `account_index`es. +- **Keypair** — a single secp256k1 key, one address. See + [Keypair Accounts](keypair.md). ## Account kinds -| Kind | Backing PKD | Address derivation | +| Kind | Backing private key data | Address derivation | | --- | --- | --- | | `bip32` | `Mnemonic`, `Bip39Seed`, or `ExtendedPrivateKey` | HD path `m/44'/111111'/'//` | | `keypair` | `SecretKey` | One address per account (Schnorr or ECDSA) | @@ -66,21 +69,22 @@ for a in await wallet.accounts_enumerate(): print(" change: ", a.change_address) ``` -`AccountDescriptor` exposes `kind`, `account_id`, `account_name`, -`balance`, `prv_key_data_ids`, `receive_address` / `change_address`, and -(for HD only) `account_index`, `xpub_keys`, `ecdsa`, -`receive_address_index`, `change_address_index`. `get_addresses()` returns -every derived address. +[`AccountDescriptor`](../../reference/Classes/AccountDescriptor.md) +exposes `kind`, `account_id`, `account_name`, `balance`, +`prv_key_data_ids`, `receive_address` / `change_address`, and (HD +only) `account_index`, `xpub_keys`, `ecdsa`, `receive_address_index`, +`change_address_index`. `get_addresses()` returns every derived +address. ## Activate -Accounts must be activated before they emit balance events or are usable -for sends. Activation requires a connected wRPC client *and* a synced -wallet — see [Start](start.md). +Accounts must be activated before they emit balance events or accept +sends. Activation requires a connected wRPC client *and* a synced +wallet — see [Sync State](sync-state.md). ```python await wallet.accounts_activate([acct.account_id]) -# or, activate everything in the wallet: +# or, activate every account: await wallet.accounts_activate() ``` @@ -96,16 +100,16 @@ acct = await wallet.accounts_ensure_default( ) ``` -Returns the existing default `bip32` account if there is one, otherwise -creates one (generating a fresh mnemonic when `mnemonic_phrase` is -`None`). Only `bip32` is supported; other kinds raise. +Returns the default `bip32` account if there is one, otherwise creates +one (generating a fresh mnemonic when `mnemonic_phrase` is `None`). +Only `bip32` is supported; other kinds raise. ## Import vs. create `accounts_import_bip32` is the recovery-flow variant: it runs an -address-discovery scan before adding the account, so addresses that have -already received funds are recognised as used. Use it when restoring from -a known-used mnemonic; use `accounts_create_bip32` for fresh accounts. +address-discovery scan before adding the account, so previously-funded +addresses are recognised as used. Use it when restoring a known-used +mnemonic; use `accounts_create_bip32` for fresh accounts. To scan a mnemonic *before* picking an index, see [Wallet Recovery](../../guides/wallet-recovery.md). diff --git a/docs/learn/wallet/addresses.md b/docs/learn/wallet/addresses.md index fa5e8070..5476b33e 100644 --- a/docs/learn/wallet/addresses.md +++ b/docs/learn/wallet/addresses.md @@ -1,9 +1,9 @@ # Addresses -A BIP32 account derives addresses lazily. The wallet records two indices -per account — one for receive, one for change — and `accounts_create_new_address` -advances them. Keypair accounts have a single, fixed address and reject this -call. +A BIP32 account derives addresses lazily. The wallet records two +indices per account — one for receive, one for change — and +`accounts_create_new_address` advances them. Keypair accounts have a +single fixed address and reject this call. ## Read the current addresses @@ -17,8 +17,9 @@ print(acct.receive_address_index) # int, BIP32 only print(acct.change_address_index) # int, BIP32 only ``` -`get_addresses()` returns *every* derived address on the account, which -is what you want for re-subscribing UTXO notifications across all of them. +`get_addresses()` returns *every* derived address on the account — +the right choice for re-subscribing UTXO notifications across all of +them. ## Derive the next address @@ -35,31 +36,30 @@ next_change = await wallet.accounts_create_new_address( The index used is the descriptor's `receive_address_index` or `change_address_index` *before* the call; afterwards the descriptor's -counter advances by one. Each newly derived address is automatically -registered with the account's `UtxoContext`, so funds sent to it will +counter advances by one. Newly derived addresses register +automatically with the account's `UtxoContext`, so funds sent to them appear in the next sync. ## Receive vs. change -- **Receive** addresses are what you give out. Generate one any time you - want a new public-facing address — for billing, for separating +- **Receive** addresses are what you hand out. Generate one any time + you need a new public-facing address — for billing, for separating customers, for a hot/cold split. -- **Change** addresses are where the wallet returns leftover funds when - spending. The `Generator` (used internally by `accounts_send`) picks the - current change address automatically; you usually don't need to advance - the change index manually. +- **Change** addresses are where the wallet returns leftover funds. + The `Generator` (used internally by `accounts_send`) picks the + current change address automatically; you usually don't advance the + change index by hand. -If you're sweeping a UTXO set and want the leftover to stay in the same -account but on a *fresh* change address, advance the change index first — -see [Sweep Funds](sweep.md). +To sweep a UTXO set and leave leftover on a *fresh* change address, +advance the change index first — see [Sweep Funds](sweep.md). ## Address discovery on import -When you `accounts_import_bip32` (rather than `accounts_create_bip32`), -the wallet walks the receive and change chains looking for addresses that -have ever held a UTXO and bumps the indices accordingly. This is what -makes a restored wallet "remember" addresses it had previously handed out. -Without it, `next_recv` would silently re-issue an already-used address. +`accounts_import_bip32` walks the receive and change chains for +addresses that have ever held a UTXO and bumps the indices +accordingly. That's what lets a restored wallet "remember" addresses +it previously handed out. Without it, `next_recv` would silently +re-issue an already-used address. ## Where to next diff --git a/docs/learn/wallet/architecture.md b/docs/learn/wallet/architecture.md index 61fa7e05..86020a87 100644 --- a/docs/learn/wallet/architecture.md +++ b/docs/learn/wallet/architecture.md @@ -1,77 +1,40 @@ # Architecture -A `Wallet` is not a single object — it's a small system of cooperating -pieces. Knowing how they fit together is what makes the rest of this -section make sense, especially the [sync gate](start.md) and the -[transaction-history events](transaction-history.md). +The `Wallet` class has quite a bit going on behind the scenes - it's a small system of cooperating +pieces. Knowing how they fit together is what makes the +[sync gate](sync-state.md) and +[transaction-history events](transaction-history.md) make sense. ## The pieces -``` - ┌──────────────────────────────────────────┐ - │ Wallet │ - │ (lifecycle, file storage, accounts) │ - └───────────┬───────────────┬──────────────┘ - │ │ - owns │ │ owns - ▼ ▼ - ┌────────────────┐ ┌──────────────┐ - │ UtxoProcessor │ │ RpcClient │ - └────────┬───────┘ └──────┬───────┘ - │ │ - fans out to │ pushes notifications - │ │ - ▼ ▼ - ┌────────────────────────────────┐ - │ UtxoContext (one per │ - │ activated account) │ - └────────────────────────────────┘ +```mermaid +flowchart TD + Wallet["Wallet
lifecycle, file storage, accounts"] + UtxoProcessor["UtxoProcessor"] + RpcClient["RpcClient"] + UtxoContext["UtxoContext
one per activated account"] + + Wallet -- owns --> UtxoProcessor + Wallet -- owns --> RpcClient + RpcClient -- pushes notifications --> UtxoProcessor + UtxoProcessor -- fans out to --> UtxoContext ``` | Component | Job | | --- | --- | -| **`Wallet`** | Lifecycle, on-disk file storage, account list, event multiplexer. The thing your code calls. | -| **`RpcClient`** | The wRPC connection. Used internally for calls and as the source of node-pushed notifications. | -| **`UtxoProcessor`** | Subscribes to virtual-chain / UTXO notifications, tracks `synced` state, routes incoming UTXO changes to the right `UtxoContext`. | -| **`UtxoContext`** | One per activated account. Holds the tracked addresses, the per-state balance (`mature`, `pending`, `outgoing`), and the mature UTXO set the coin selector pulls from. | - -The wallet *does not poll* the node for UTXO state. It is **fed**, by the -processor, from notifications. This is why the [sync gate](start.md) -matters — before sync, the processor isn't forwarding anything, so the -contexts stay empty. - -## UTXO maturity - -Every UTXO the processor sees moves through three states: - -- **Pending** — seen, but the chain confirmation depth is below the - maturity threshold. Counted in `Balance.pending`. *Not* spendable. -- **Mature** — confirmed deeply enough to spend. Counted in - `Balance.mature`. Returned by `accounts_get_utxos`. Selectable. -- **Outgoing** — locked because the wallet just spent it in a transaction - it generated. Counted in `Balance.outgoing` until the spend matures or is - reorged out. - -[Send Transaction](send-transaction.md) waits on `Maturity` for this -reason: a `Pending` UTXO is real, but the next `accounts_send` won't see -it as spendable. - -## Why `accounts_get_utxos` can return `[]` - -`accounts_get_utxos` is a read of the in-memory `UtxoContext`. It returns -`[]` when: - -1. The wallet isn't synced yet (the processor isn't forwarding). -2. The account hasn't been activated. -3. No notification for a funding tx has reached the processor yet. +| **[`Wallet`](../../reference/Classes/Wallet.md)** | Lifecycle, on-disk file storage, account list, event multiplexer. The thing your code calls. | +| **[`RpcClient`](../../reference/Classes/RpcClient.md)** | The wRPC connection. Used internally for calls and as the source of node-pushed notifications. | +| **[`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md)** | Subscribes to virtual-chain / UTXO notifications, tracks `synced` state, routes UTXO changes to the right `UtxoContext`. | +| **[`UtxoContext`](../../reference/Classes/UtxoContext.md)** | One per activated account. Holds tracked addresses, per-state balance (`mature`, `pending`, `outgoing`), and the mature UTXO set the coin selector pulls from. | -None of these are "the address has no funds" — they're "the wallet hasn't -been told yet." The fix is to gate UTXO-dependent code on `is_synced` and -to listen for `Maturity` rather than polling. See [Start](start.md) and -[Transaction History](transaction-history.md). +The wallet *does not poll* the node for UTXO state. It is **fed** by +the processor from notifications — see [Sync State](sync-state.md) +for what gates that flow. ## Where to next -- [Lifecycle](lifecycle.md) — the state machine. -- [Start](start.md) — `start → connect → is_synced` and why each step matters. +- [Lifecycle](lifecycle.md) — the state machine and boot sequence. +- [Sync State](sync-state.md) — node IBD vs. processor readiness. +- [UTXO Maturity](utxo-maturity.md) — Pending / Mature / Outgoing + states and why `accounts_get_utxos` can return `[]`. - [Transaction History](transaction-history.md) — the event surface. diff --git a/docs/learn/wallet/initialize.md b/docs/learn/wallet/initialize.md deleted file mode 100644 index a5a3e759..00000000 --- a/docs/learn/wallet/initialize.md +++ /dev/null @@ -1,62 +0,0 @@ -# Initialize - -Constructing a `Wallet` does no I/O. It builds the local file store and an -internal wRPC client, and that's it — `start()` is the next step. - -## Constructor - -```python -from kaspa import Resolver, Wallet - -wallet = Wallet( - network_id="testnet-10", # required - resolver=Resolver(), # discover a public node - # url=... # OR a known node URL - # encoding="borsh", # default; "json" also accepted - # storage_folder=None, # override default ~/.kaspa -) -``` - -| Argument | Required | Notes | -| --- | --- | --- | -| `network_id` | yes | `"mainnet"`, `"testnet-10"`, `"testnet-11"`. Drives both the resolver query and the address encoding. | -| `resolver` | one of | A `Resolver` instance — see [RPC → Resolver](../rpc/resolver.md). | -| `url` | resolver/url | A known wRPC URL (`wss://node.example:17110`). Skip the resolver if you set this. | -| `encoding` | optional | `"borsh"` (default) or `"json"`. Borsh is right for almost everything. | - -`network_id` is **required** — addresses derived from this wallet will be -encoded for that network and the resolver only returns nodes on that -network. - -## Switching networks - -`set_network_id` raises if the wallet is currently connected: - -```python -await wallet.disconnect() -wallet.set_network_id("mainnet") -await wallet.connect() -``` - -Switching network does not invalidate the file store on disk — but the -*addresses* derived from a BIP32 account are network-specific, so a key -created under `testnet-10` produces different (testnet) addresses than the -same key under `mainnet`. - -## Storage folder - -By default the wallet stores files under `~/.kaspa/`. Override with -`storage_folder=...` when: - -- Running tests against a temp directory. -- Running multiple isolated wallets in the same process. -- Sandboxing per-user wallet stores in a multi-tenant service. - -The folder is created on first write; nothing is done at construction -time. - -## Where to next - -- [Start](start.md) — boot the runtime and connect to the node. -- [Open](open.md) — create or open a wallet file. -- [Lifecycle](lifecycle.md) — the full state machine. diff --git a/docs/learn/wallet/keypair.md b/docs/learn/wallet/keypair.md index dc7d2c6c..1b7b3e44 100644 --- a/docs/learn/wallet/keypair.md +++ b/docs/learn/wallet/keypair.md @@ -1,14 +1,15 @@ # Keypair Accounts -A keypair account holds one secp256k1 key and produces one address. It has -no derivation tree — there's no "next address" and no `account_index`. -Use these when you have a single secret you want to manage alongside other -accounts in the same wallet, or when you're moving an existing standalone +A keypair account holds one secp256k1 key and produces one address. +It has no derivation tree — no "next address", no `account_index`. +Use one when you have a single secret to manage alongside other +accounts in the same wallet, or when moving an existing standalone key into managed storage. ## Create a keypair account -A keypair account is backed by a `SecretKey`-variant PKD: +A keypair account is backed by a `SecretKey`-variant +[private key data entry](private-keys.md): ```python from kaspa import PrivateKey, PrvKeyDataVariantKind @@ -26,13 +27,13 @@ secret_pkd = await wallet.prv_key_data_create( kp = await wallet.accounts_create_keypair( wallet_secret=secret, prv_key_data_id=secret_pkd, - ecdsa=False, # False = Schnorr (default), True = ECDSA + ecdsa=False, # False = Schnorr, True = ECDSA account_name="keypair-acct", ) ``` -`ecdsa=True` is for ECDSA-style keypair accounts; the default Schnorr -variant is what most callers want. +`ecdsa` is required. `ecdsa=False` (Schnorr) is what most callers +want; `ecdsa=True` produces an ECDSA-style keypair account. ## What the descriptor looks like @@ -43,32 +44,36 @@ Keypair `AccountDescriptor`s have: | `kind` | `"keypair"` | | `account_id` | stable id | | `receive_address` | the one address | -| `change_address` | the *same* address — there is no separate change chain | +| `change_address` | the *same* address — no separate change chain | | `account_index`, `xpub_keys`, `receive_address_index`, `change_address_index`, `ecdsa` | `None` for the indices; `ecdsa` reflects the constructor flag | -`accounts_create_new_address` raises on a keypair account — there is no -next address to derive. +`accounts_create_new_address` raises on a keypair account — there's +no next address to derive. ## When to use a keypair account -- You generated a key with the standalone `PrivateKey` API or imported one - from another tool, and want it managed inside a wallet file. +- You generated a key with the standalone + [`PrivateKey`](../../reference/Classes/PrivateKey.md) API or + imported one from another tool, and want it managed in a wallet + file. - You want a single-purpose hot wallet — one address, no rotation. -- You're testing and a single deterministic address is easier to reason - about than an HD chain. +- You're testing, and a single deterministic address is easier to + reason about than an HD chain. -For everyday wallets — anything user-facing or anything where address -rotation matters — use a [BIP32 account](accounts.md) instead. +For user-facing or rotation-sensitive wallets, use a +[BIP32 account](accounts.md) instead. ## Import vs. create -`accounts_import_keypair` is the variant for an existing key that may -already have on-chain history. The address-discovery scan path is a no-op -(there's only one address), so it's effectively the same as -`accounts_create_keypair` — use whichever reads better at the call site. +`accounts_import_keypair` is the variant for an existing key with +on-chain history. The address-discovery scan is a no-op (only one +address), so it's effectively the same as `accounts_create_keypair` — +pick whichever reads better at the call site. ## Where to next - [Accounts](accounts.md) — BIP32 accounts. -- [Send Transaction](send-transaction.md) — sending from a keypair account - works the same as from a BIP32 account. +- [Send Transaction](send-transaction.md) — sending from a keypair + account works the same as from a BIP32 account. +- [Wallet SDK → Key Management](../wallet-sdk/key-management.md) — + generating a `PrivateKey` outside the wallet first. diff --git a/docs/learn/wallet/lifecycle.md b/docs/learn/wallet/lifecycle.md index 337b6d37..81edf828 100644 --- a/docs/learn/wallet/lifecycle.md +++ b/docs/learn/wallet/lifecycle.md @@ -1,7 +1,9 @@ # Lifecycle -A `Wallet` moves through five states. Each transition is async and ordered -— calling them out of order raises. +A `Wallet` moves through five states. Each transition is async and +ordered — skipping or repeating steps will either raise (e.g. opening +without `start()`) or leave the wallet in a broken state (e.g. +operating before sync). ```mermaid stateDiagram-v2 @@ -15,7 +17,7 @@ stateDiagram-v2 Started --> [*]: stop() ``` -## Transitions +## States and transitions | Step | Method | Effect | | --- | --- | --- | @@ -25,40 +27,230 @@ stateDiagram-v2 | Open | `await wallet.wallet_create(...)` / `wallet_open(...)` | Decrypts and loads a wallet file; secrets become available in memory. | | Activate | `await wallet.accounts_activate([ids])` | Begins UTXO tracking and event emission for the chosen accounts. | | Close | `await wallet.wallet_close()` | Releases the open wallet; activated accounts stop tracking. | -| Disconnect | `await wallet.disconnect()` | Drops the wRPC connection. The wallet remains started. | +| Disconnect | `await wallet.disconnect()` | Drops the wRPC connection; the wallet remains started. | | Stop | `await wallet.stop()` | Tears down the runtime and event task. | -## Ordering rules - -!!! warning "Preconditions" - - `start()` must precede `connect()`, `wallet_create()`, and `wallet_open()`. - - `wallet_create()` / `wallet_open()` may be called before or after - `connect()`, but `accounts_activate()` requires the wRPC client to be - connected *and* the wallet to be synced (see [Start](start.md)). - - `set_network_id()` raises if the wRPC client is currently connected - — `disconnect()` first, change the network, then `connect()` again. - - `wallet_close()` does not stop the runtime; pair it with `stop()` on - shutdown. - ## Properties | Property | Type | Meaning | | --- | --- | --- | -| `wallet.rpc` | `RpcClient` | The underlying wRPC client. Use it for direct node calls. | +| `wallet.rpc` | [`RpcClient`](../../reference/Classes/RpcClient.md) | The underlying wRPC client. Use for direct node calls. | | `wallet.is_open` | `bool` | `True` between `wallet_open` / `wallet_create` and `wallet_close`. | -| `wallet.is_synced` | `bool` | `True` once the `UtxoProcessor` has caught up. See [Start](start.md). | +| `wallet.is_synced` | `bool` | `True` once the `UtxoProcessor` has caught up. See [Sync State](sync-state.md). | | `wallet.descriptor` | `WalletDescriptor \| None` | Metadata for the open wallet, or `None` when closed. | -## Reload without re-reading +## 1.) Construct + +Constructing a [`Wallet`](../../reference/Classes/Wallet.md) does no +I/O. It builds the local file store and an internal wRPC client — +that's it. + +```python +from kaspa import Resolver, Wallet + +wallet = Wallet( + network_id="testnet-10", # required in practice + resolver=Resolver(), # discover a public node + # url=... # OR a known node URL + # encoding="borsh", # default; "json" also accepted +) +``` + +| Argument | Required | Notes | +| --- | --- | --- | +| `network_id` | effectively yes | `"mainnet"`, `"testnet-10"`, `"testnet-11"`. Drives both resolver query and address encoding. May be omitted at construction and supplied later via `set_network_id`. | +| `resolver` | one of | A [`Resolver`](../rpc/resolver.md) instance. | +| `url` | resolver/url | A known wRPC URL (`wss://node.example:17110`). Skip the resolver if set. | +| `encoding` | optional | `"borsh"` (default) or `"json"`. Borsh is right for almost everything. | + +Addresses derived from this wallet are encoded for `network_id`, and +the resolver only returns nodes on that network — pin it before +`accounts_activate`. + +### Switching networks + +`set_network_id` raises if the wallet is currently connected: + +```python +await wallet.disconnect() +wallet.set_network_id("mainnet") +await wallet.connect() +``` + +Switching network does not invalidate the file store, but BIP32 +account *addresses* are network-specific — a key created under +`testnet-10` produces different (testnet) addresses than the same key +under `mainnet`. + +### Storage location + +Wallet files live in the SDK's local store (under `~/.kaspa/` by +default). The folder is created on first write — nothing happens at +construction time. The current `Wallet` constructor does not expose a +per-instance override; the location is fixed for the process. + +## 2.) Start the runtime + +`start()` boots the wallet's runtime — `UtxoProcessor`, wRPC notifier, +and event-dispatch loop. `connect()` then attaches the wRPC client to +a node. After both, the wallet is ready to *open a file*, but not yet +ready to touch UTXO state. + +```python +wallet = Wallet(network_id="testnet-10", resolver=Resolver()) +await wallet.start() +await wallet.connect() +``` + +Both are required. `start()` without `connect()` leaves the runtime +running but unable to reach the node; `connect()` without a prior +`start()` leaves the wallet runtime unstarted, so account activation +and event dispatch never function. + +### Connect options + +`connect()` takes the same options as +[`RpcClient.connect`](../rpc/connecting.md#connection-options): + +```python +await wallet.connect( + block_async_connect=True, # await readiness before returning + strategy="retry", # "retry" or "fallback" + url=None, # override the resolver-discovered URL + timeout_duration=10_000, # ms + retry_interval=1_000, # ms +) +``` + +If you constructed with a `Resolver`, omit `url` and let it pick a +public node. Pass `url=` to override for one connection (handy for +pinning to a specific node temporarily). + +### Sync gate + +`connect()` resolves as soon as the WebSocket is up — *not* when the +wallet's UTXO processor has caught up. Until `wallet.is_synced` flips +to `True`, UTXO-dependent calls (`AccountDescriptor.balance`, +`accounts_get_utxos`, `accounts_send`) are unusable. Quick polling +form for scripts: + +```python +await wallet.connect(...) +while not wallet.is_synced: + await asyncio.sleep(0.5) +``` + +For the event-driven pattern and the node-vs-processor breakdown of +what "synced" actually means, see [Sync State](sync-state.md). + +## 3.) Open a wallet file + +A wallet file is a single encrypted file on disk. Only one is open at +a time per `Wallet` instance. + +```python +created = await wallet.wallet_create( + wallet_secret="example-secret", + filename="demo", + overwrite_wallet_storage=False, + title="demo", + user_hint="example", +) +``` + +- `filename` — on-disk basename; omit for the SDK default. +- `overwrite_wallet_storage=False` — raises `WalletAlreadyExistsError` + if the file exists; pass `True` to clobber. +- `user_hint` — stored alongside the file as a recoverable password + hint. + +To open an existing file: + +```python +opened = await wallet.wallet_open( + wallet_secret="example-secret", + account_descriptors=True, # include account list in the response + filename="demo", +) +``` + +`account_descriptors=True` returns the account list in the response +so you can pick which to activate without a follow-up +`accounts_enumerate()`. + +### Create-or-open pattern + +`wallet_create` raises `WalletAlreadyExistsError` when the file +exists. The canonical idempotent boot: + +```python +from kaspa.exceptions import WalletAlreadyExistsError + +try: + await wallet.wallet_create( + wallet_secret=secret, filename="demo", overwrite_wallet_storage=False, + ) +except WalletAlreadyExistsError: + await wallet.wallet_open(secret, True, "demo") +``` -`wallet_reload(reactivate)` reboots the account runtime using cached wallet -data — no disk I/O. Pass `reactivate=True` to resume previously active -accounts; pass `False` if you intend to call `accounts_activate` yourself. -A `WalletReload` event fires either way. +For listing, exporting, importing, renaming, and re-encrypting wallet +files, see [Wallet Files](wallet-files.md). + +## Activate accounts + +`accounts_activate` is what makes accounts emit balance events and +accept sends. Activation requires a connected wRPC client *and* a +synced wallet — see [Sync State](sync-state.md). + +```python +await wallet.accounts_activate([acct.account_id]) +# or, activate every account: +await wallet.accounts_activate() +``` + +Account creation, BIP32 derivation, and discovery flows live on +[Accounts](accounts.md). + +## Reload + +`wallet_reload(reactivate)` reboots the account runtime from cached +wallet data — no disk I/O. Pass `reactivate=True` to resume +previously active accounts; pass `False` to call `accounts_activate` +yourself. A `WalletReload` event fires either way. + +## Close, disconnect, stop + +```python +await wallet.wallet_close() # release the open file; secrets leave memory +await wallet.disconnect() # drop the wRPC link; runtime stays alive +await wallet.stop() # tear down the runtime and event task +``` + +`wallet_close` does not stop the runtime — pair it with `stop()` on +shutdown. Skipping `stop()` leaks the notification task; skipping +`disconnect()` leaves the WebSocket open. + +## Ordering rules + +!!! warning "Preconditions" + - `start()` must precede `connect()`, `wallet_create()`, and `wallet_open()`. + - `wallet_create()` / `wallet_open()` may run before or after + `connect()`, but `accounts_activate()` requires the wRPC client to + be connected *and* the wallet to be synced (see [Sync State](sync-state.md)). + - `set_network_id()` raises if the wRPC client is currently + connected — `disconnect()` first, change the network, then + `connect()` again. + - `wallet_close()` does not stop the runtime; pair it with `stop()` + on shutdown. ## Where to next -- [Initialize](initialize.md), [Start](start.md), [Open](open.md) — the - three phases of bringing a wallet up, in order. -- [Architecture](architecture.md) — what `start` / `connect` / `activate` - actually wire up. +- [Sync State](sync-state.md) — node IBD vs. processor readiness. +- [Wallet Files](wallet-files.md) — enumerate, export, import, + rename, change secret. +- [Private Keys](private-keys.md) — the next step after creating a + wallet. +- [Accounts](accounts.md) — derive accounts from stored key data. +- [Architecture](architecture.md) — what `start` / `connect` / + `activate` actually wire up. diff --git a/docs/learn/wallet/overview.md b/docs/learn/wallet/overview.md index 7462f905..4ba2e2b1 100644 --- a/docs/learn/wallet/overview.md +++ b/docs/learn/wallet/overview.md @@ -1,21 +1,19 @@ # Wallet -The `Wallet` class is the SDK's high-level managed wallet. It layers -encrypted on-disk storage, multi-account management, an event bus, and -built-in send / transfer / sweep flows on top of the lower-level primitives -in [Wallet SDK](../wallet-sdk/overview.md). +The [`Wallet`](../../reference/Classes/Wallet.md) class is the SDK's +high-level managed wallet. It layers encrypted on-disk storage, +multi-account management, an event bus, and built-in send / transfer / +sweep flows on top of the primitives in +[Wallet SDK](../wallet-sdk/overview.md). -## When to reach for `Wallet` +Features: -| You want to... | Use | -| --- | --- | -| Persist secrets, manage multiple accounts, react to chain events | `Wallet` | -| Sign one transaction in a script, no on-disk state | The primitives in [Wallet SDK](../wallet-sdk/overview.md) | -| Embed wallet behaviour in your own app | `Wallet` — the listener model and account API are designed for this | - -If you only need a one-shot signer, the primitives are simpler. If you need -the "open file → manage keys → track UTXOs → send" loop, `Wallet` saves -you from re-implementing it. +- Persistent encrypted on-disk storage for keys and account metadata +- Multi-account management across BIP32 and keypair accounts +- Event bus for chain notifications (balance, maturity, reorg) +- Built-in send, transfer, and sweep flows +- Address derivation and discovery +- Transaction history tracking ## A wallet, end to end @@ -51,23 +49,24 @@ async def main(): asyncio.run(main()) ``` -This script creates a wallet file, derives a BIP32 account from a -mnemonic, and activates it. Re-running fails with +This script creates a wallet file on disk, derives a BIP32 account from a +mnemonic, and activates it. Re-running raises `WalletAlreadyExistsError` unless you switch to `wallet_open` — see -[Open](open.md). +[Lifecycle](lifecycle.md#open-a-wallet-file). ## How this section is laid out - [Architecture](architecture.md) — `Wallet` / `UtxoProcessor` / - `UtxoContext` and how notifications flow through them. -- [Lifecycle](lifecycle.md) — the state machine and ordering rules for the - whole class. -- [Initialize](initialize.md), [Start](start.md), [Open](open.md) — each - phase of bringing a wallet up. + `UtxoContext` and how notifications flow. +- [Lifecycle](lifecycle.md) — the full state machine: construct, + start, connect, open, activate, close, stop. +- [Sync State](sync-state.md) — node IBD vs. processor readiness. +- [Wallet Files](wallet-files.md) — enumerate, export, import, + rename, change secret. - [Private Keys](private-keys.md), [Accounts](accounts.md), - [Addresses](addresses.md), [Keypair Accounts](keypair.md) — populating - the wallet. + [Addresses](addresses.md), [Keypair Accounts](keypair.md) — + populating the wallet. - [Send Transaction](send-transaction.md), [Sweep Funds](sweep.md) — outgoing flows. -- [Transaction History](transaction-history.md) — the event surface and - history APIs. +- [Transaction History](transaction-history.md) — events and history + APIs. diff --git a/docs/learn/wallet/private-keys.md b/docs/learn/wallet/private-keys.md index 8a91f414..6b1a0fb7 100644 --- a/docs/learn/wallet/private-keys.md +++ b/docs/learn/wallet/private-keys.md @@ -1,31 +1,39 @@ # Private Keys -A *private key data* (PKD) entry is the encrypted secret that backs one or -more accounts. A wallet file holds zero or more PKDs; each account -references exactly one by `PrvKeyDataId`. +A *private key data* entry is the encrypted secret that backs one or +more accounts. A wallet file holds zero or more private key data +entries; each account references exactly one by +[`PrvKeyDataId`](../../reference/Classes/PrvKeyDataId.md). ## Variants -`PrvKeyDataVariantKind` selects the format of `secret` passed to -`prv_key_data_create`: +[`PrvKeyDataVariantKind`](../../reference/Enums/PrvKeyDataVariantKind.md) +selects the format of `secret` passed to `prv_key_data_create`. The +enum exposes four variants, but only two are accepted by the upstream +wallet today: -| Variant | `secret` format | Typical source | -| --- | --- | --- | -| `Mnemonic` | BIP-39 phrase (12 or 24 words) | New wallets, `Mnemonic.random(...)` | -| `Bip39Seed` | Hex-encoded BIP-39 seed | Pre-derived seeds from another tool | -| `ExtendedPrivateKey` | xprv string | Migrating an existing HD wallet | -| `SecretKey` | 64-char hex secp256k1 key | Single-key (keypair) accounts | +| Variant | `secret` format | Typical source | Status | +| --- | --- | --- | --- | +| `Mnemonic` | BIP-39 phrase (12 or 24 words) | New wallets, `Mnemonic.random(...)` | Supported | +| `SecretKey` | 64-char hex secp256k1 key | Single-key (keypair) accounts | Supported | +| `Bip39Seed` | Hex-encoded BIP-39 seed | Pre-derived seeds from another tool | **Not supported upstream** — `prv_key_data_create` raises | +| `ExtendedPrivateKey` | xprv string | Migrating an existing HD wallet | **Not supported upstream** — `prv_key_data_create` raises | + +The two unsupported variants fall through to a `_` arm in +`kaspa-wallet-core`'s `create_prv_key_data` and surface as +`"Invalid prv key data kind, supported types are Mnemonic and SecretKey"`. +Use `Mnemonic` for HD wallets and `SecretKey` for single-key accounts. ## Surface | Method | Purpose | | --- | --- | -| `prv_key_data_create(...)` | Encrypt and store a new PKD; returns its `PrvKeyDataId`. | -| `prv_key_data_enumerate()` | List `PrvKeyDataInfo` for every stored PKD. | -| `prv_key_data_get(secret, id)` | Fetch metadata for a single PKD. | +| `prv_key_data_create(...)` | Encrypt and store a new entry; returns its `PrvKeyDataId`. | +| `prv_key_data_enumerate()` | List `PrvKeyDataInfo` for every stored entry. | +| `prv_key_data_get(secret, id)` | Fetch metadata for a single entry. | -The wallet must be open. The actual secret never leaves the wallet — only -its metadata is returned. +The wallet must be open. The actual secret never leaves the wallet — +only its metadata is returned. ## Create @@ -41,9 +49,9 @@ prv_key_id = await wallet.prv_key_data_create( ) ``` -`payment_secret`, when set, layers a second password on top of -`wallet_secret`. Every operation that decrypts this PKD (account creation, -signing, export) must supply it. Use `None` for single-password wallets. +`payment_secret` layers a second password on top of `wallet_secret`. +Every operation that decrypts this entry (account creation, signing, +export) must supply it. Use `None` for single-password wallets. ## Enumerate & inspect @@ -61,9 +69,10 @@ for info in await wallet.prv_key_data_enumerate(): `prv_key_data_get(wallet_secret, id)` returns the same metadata for one entry, raising if the id is unknown. -## Using a PKD +## Using a private key data entry -`PrvKeyDataId` is the link between a PKD and the accounts derived from it: +[`PrvKeyDataId`](../../reference/Classes/PrvKeyDataId.md) links a +private key data entry to the accounts derived from it: ```python descriptor = await wallet.accounts_create_bip32( @@ -73,14 +82,15 @@ descriptor = await wallet.accounts_create_bip32( ) ``` -A single PKD can back many accounts — common for BIP32 wallets where -multiple account indices share one mnemonic. See [Accounts](accounts.md) -for the account-creation surface. +A single private key data entry can back many accounts — common for +BIP32 wallets where multiple account indices share one mnemonic. See +[Accounts](accounts.md). ## Where to next -- [Accounts](accounts.md) — derive BIP32 accounts from a PKD. -- [Keypair Accounts](keypair.md) — single-key accounts from `SecretKey` - PKDs. +- [Accounts](accounts.md) — derive BIP32 accounts from a private key + data entry. +- [Keypair Accounts](keypair.md) — single-key accounts from + `SecretKey`-variant private key data entries. - [Wallet Recovery](../../guides/wallet-recovery.md) — BIP-44 scan for accounts already used under a mnemonic. diff --git a/docs/learn/wallet/send-transaction.md b/docs/learn/wallet/send-transaction.md index 4519d24c..0ad367be 100644 --- a/docs/learn/wallet/send-transaction.md +++ b/docs/learn/wallet/send-transaction.md @@ -1,9 +1,9 @@ # Send Transaction Outgoing flows from an activated account. Every method on this page -requires the wallet to be open, the wRPC client connected, the source -account activated, and `wallet.is_synced` to be `True` — see -[Start](start.md). +requires an open wallet, a connected wRPC client, an activated source +account, and `wallet.is_synced == True` — see +[Sync State](sync-state.md). ## Surface @@ -34,7 +34,7 @@ print(result.final_transaction_id, result.fees, result.final_amount) ## Multi-output send -A single `destination` list with N outputs becomes one transaction with +A single `destination` list of N outputs becomes one transaction with N + 1 outputs (the +1 is the change return). ```python @@ -55,25 +55,28 @@ estimate = await wallet.accounts_estimate( priority_fee_sompi=Fees(0, FeeSource.SenderPays), destination=outputs, ) -print(estimate.fees, estimate.final_amount, estimate.aggregated_utxos) +print(estimate.fees, estimate.final_amount, estimate.utxos) ``` `accounts_estimate` and `accounts_send` take the same arguments. -Estimating first is cheap — it surfaces fees and UTXO selection before +Estimating first is cheap and surfaces fees and UTXO selection before signing. ## Fees -`priority_fee_sompi` is a `Fees(amount, FeeSource)` (or equivalent dict): +`priority_fee_sompi` is a `Fees(amount, FeeSource)` (or equivalent +dict): - **`FeeSource.SenderPays`** — fee is added on top of the destination - amount. Used in normal sends. + amount. Standard sends. - **`FeeSource.ReceiverPays`** — fee is deducted from the destination amount. Used to sweep an exact balance with no leftover change (see [Sweep Funds](sweep.md)). -`fee_rate` overrides the resolved sompi-per-gram rate explicitly. Leave -it `None` to let the wallet pick the network-suggested rate. +`fee_rate` overrides the resolved sompi-per-gram rate. Leave it `None` +to use the network-suggested rate. See +[Mass & Fees](../transactions/mass-and-fees.md) for the underlying +model. ```python rates = await wallet.fee_rate_estimate() @@ -90,8 +93,8 @@ wallet.fee_rate_poller_disable() ## Internal transfers -Funds moved between two accounts in the **same wallet** are immediately -spendable on transaction acceptance — no maturity wait: +Funds moved between two accounts in the **same wallet** are spendable +immediately on transaction acceptance — no maturity wait: ```python await wallet.accounts_transfer( @@ -107,16 +110,16 @@ external addresses. ## Waiting for funds and confirmations -Sends submit immediately, but spent UTXOs need to mature before the next -`accounts_send` will see them. Two correct waits, both via +Sends submit immediately, but spent UTXOs need to mature before the +next `accounts_send` will see them. Two correct waits, both via [Transaction History](transaction-history.md): -- **Pending** fires when a UTXO lands but isn't spendable yet — useful - for UI. -- **Maturity** fires when a UTXO crosses the maturity depth and is - spendable. This is the right gate for "send → wait → send again" flows. +- **Pending** — fires when a UTXO lands but isn't yet spendable. + Useful for UI. +- **Maturity** — fires when a UTXO crosses the maturity depth and is + spendable. The right gate for "send → wait → send again" flows. -Polling `accounts_get_utxos` works for one-shot scripts, but a `Maturity` +Polling `accounts_get_utxos` works for one-shot scripts; a `Maturity` listener is the production pattern. ## Where to next diff --git a/docs/learn/wallet/start.md b/docs/learn/wallet/start.md deleted file mode 100644 index f31379ec..00000000 --- a/docs/learn/wallet/start.md +++ /dev/null @@ -1,107 +0,0 @@ -# Start - -`start()` boots the wallet's runtime: the `UtxoProcessor`, the wRPC -notifier task, and the event-dispatch loop. `connect()` then attaches the -wRPC client to the node. After both, the wallet is ready to *open a file* -— but not yet ready to do anything that touches UTXO state. - -## Boot sequence - -```python -wallet = Wallet(network_id="testnet-10", resolver=Resolver()) -await wallet.start() -await wallet.connect() -``` - -Both calls are required. `start()` without `connect()` leaves the runtime -running but unable to talk to the node; `connect()` without `start()` -raises. - -## Connect options - -`connect()` takes the same options as `RpcClient.connect`: - -```python -await wallet.connect( - block_async_connect=True, # await readiness before returning - strategy="retry", # "retry" or "fallback" - url=None, # override the resolver-discovered URL - timeout_duration=10_000, # ms - retry_interval=1_000, # ms -) -``` - -If you constructed with a `Resolver`, omit `url` and let the resolver pick -a public node. Pass `url=` to override for one connection (useful for -pinning to a specific node temporarily). - -## The sync gate - -`connect()` resolves as soon as the WebSocket is up — **not** when the -wallet's UTXO processor has caught up. UTXO-dependent state -(`AccountDescriptor.balance`, `accounts_get_utxos`, `accounts_send`) is -unusable in this gap. - -```python -await wallet.connect(...) - -while not wallet.is_synced: - await asyncio.sleep(0.5) -``` - -After this loop, `accounts_activate` actually attaches a working -`UtxoContext` and node notifications start populating balances and UTXOs. - -## Why the gate is necessary - -Before `is_synced` flips to `True`: - -- The `UtxoProcessor` is not driving per-account `UtxoContext`s. -- `accounts_activate` is a no-op for UTXO discovery — accounts are - registered but the processor isn't pulling state. -- Notifications from the node are buffered or ignored. - -So `accounts_get_utxos` returns `[]` and `AccountDescriptor.balance` is -`None` — not because the address is unfunded, but because the wallet -hasn't started tracking it. See [Architecture](architecture.md). - -## Event-driven wait - -If you've registered a listener (see -[Transaction History](transaction-history.md)), the `SyncState` event -reports progress and `is_synced` flips at the end: - -```python -import asyncio -from kaspa import WalletEventType - -ready = asyncio.Event() - -async def on_event(event): - if event["type"] == WalletEventType.SyncState.name and wallet.is_synced: - ready.set() - -wallet.add_event_listener(WalletEventType.All, on_event) -await wallet.connect(...) -await ready.wait() -``` - -Use this when you want a UI progress indicator alongside the gate. -Polling is fine for scripts. - -## Shutdown - -```python -await wallet.disconnect() # drop the wRPC link; runtime stays alive -await wallet.stop() # tear down the runtime and event task -``` - -Skipping `stop()` leaks the notification task. Skipping `disconnect()` -keeps the WebSocket open. - -## Where to next - -- [Open](open.md) — create or open a wallet file. -- [Architecture](architecture.md) — what `start` actually wires up. -- [Transaction History](transaction-history.md) — `SyncState` and the - rest of the event taxonomy. diff --git a/docs/learn/wallet/sweep.md b/docs/learn/wallet/sweep.md index c25bdbbe..84f280ab 100644 --- a/docs/learn/wallet/sweep.md +++ b/docs/learn/wallet/sweep.md @@ -1,15 +1,14 @@ # Sweep Funds -A sweep consolidates every UTXO in an account into one address. There are -two patterns, and the difference between them is whether you want any -leftover change. +A sweep consolidates every UTXO in an account into one address. Two +patterns; the difference is whether you want any leftover change. ## Pattern 1: sweep to your own change address -Omit `destination` entirely. The wallet routes the full sweepable balance -to the account's current change address and the resulting transaction has -no external recipient — useful for collapsing a long UTXO history into a -single mature output before a high-volume sending flow. +Omit `destination` entirely. The wallet routes the full sweepable +balance to the account's current change address — no external +recipient. Useful for collapsing a long UTXO history into a single +mature output before a high-volume send flow. ```python from kaspa import Fees, FeeSource @@ -22,13 +21,13 @@ await wallet.accounts_send( ) ``` -`SenderPays` is fine here because the change return absorbs the fee — -there's no external recipient to "subtract from." +`SenderPays` works here because the change return absorbs the fee — +there's no external recipient to subtract from. ## Pattern 2: sweep an exact balance to a fresh address -When you want to leave the account at zero and the destination receives -*the exact aggregate balance minus fees*, use `ReceiverPays`: +To leave the account at zero with the destination receiving *the exact +aggregate balance minus fees*, use `ReceiverPays`: ```python from kaspa import Fees, FeeSource, PaymentOutput @@ -44,9 +43,9 @@ await wallet.accounts_send( ) ``` -The destination amount is the *gross* balance; `ReceiverPays` deducts the -network fee from that amount before broadcasting. The result: no -change output, no dust left behind. +The destination amount is the *gross* balance; `ReceiverPays` deducts +the network fee from it before broadcasting. The result: no change +output, no dust. ## Which to use @@ -59,12 +58,13 @@ change output, no dust left behind. ## Big sweeps come back as multiple transactions If the input set is too large for one transaction's mass budget, the -underlying `Generator` produces a series of transactions: each one -consolidates some UTXOs into a single intermediate output, and the final -transaction sends that aggregate to the destination. `accounts_send` -returns the final `GeneratorSummary`. Watch the -[`Maturity` event](transaction-history.md) to know when the chain has -caught up — only the final output is what you'd hand off downstream. +underlying [`Generator`](../wallet-sdk/tx-generator.md) produces a +series of transactions: each consolidates some UTXOs into a single +intermediate output, and the final transaction sends the aggregate to +the destination. `accounts_send` returns the final `GeneratorSummary`. +Watch the [`Maturity` event](transaction-history.md) to know when the +chain has caught up — only the final output is what you'd hand off +downstream. ## Where to next diff --git a/docs/learn/wallet/sync-state.md b/docs/learn/wallet/sync-state.md new file mode 100644 index 00000000..2e090ded --- /dev/null +++ b/docs/learn/wallet/sync-state.md @@ -0,0 +1,169 @@ +# Sync State + +"Sync" in the managed wallet covers two distinct layers: + +1. **Node sync** — has the kaspad you're connected to finished its + own initial block download (IBD)? +2. **Processor sync** — has the wallet's embedded + [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) + finished registering subscriptions and confirmed the node is in a + usable state? + +`accounts_*` calls — balances, UTXO snapshots, sends — wait on (2), +which is itself gated on (1). Treating them as one gives the right +answer most of the time but blurs *what* the wallet is actually +waiting for. This page splits them. + +## Node sync state + +A node that's still in IBD doesn't yet have all blocks/UTXOs, so it +can't answer wallet RPC calls authoritatively. Two surfaces report +this: + +- **`ServerStatus`** event — emitted once after `connect()`, right + after the initial `get_server_info` handshake. Payload: + + ```python + { + "type": "server-status", + "data": { + "networkId": "testnet-10", + "serverVersion": "0.x.y", + "isSynced": True, # node-side flag + "url": "wss://node:17110", + }, + } + ``` + +- **`SyncState`** event with an *IBD substate* — while the node is + still catching up, the wallet derives progress from log lines the + node prints and re-publishes them as `SyncState` events. See + [Reading SyncState payloads](#reading-syncstate-payloads). + +If the node is missing its UTXO index entirely, the processor +short-circuits with a **`UtxoIndexNotEnabled`** event and refuses to +proceed — the only fix is to point at a node that has it. + +## Processor sync state + +Once the node reports synced, the wallet's `UtxoProcessor` does its +own setup: registers UTXO/virtual-chain notification listeners, marks +itself ready, and starts forwarding UTXO changes to per-account +[`UtxoContext`](../../reference/Classes/UtxoContext.md)s. + +- **`wallet.is_synced`** — `True` once the processor finishes that + setup. This is the flag every `accounts_*` call effectively waits + on. Polling it (with `await asyncio.sleep(0.5)`) is fine for + scripts. +- **`SyncState`** event with a *terminal substate* — `Synced` when + the processor flips ready, `NotSynced` when it falls back (e.g. on + reconnect). +- **`UtxoProcStart`** / **`UtxoProcStop`** — fire when the processor + itself starts and stops (lifecycle, not sync per se), but useful as + bookends in event logs. + +The relationship is one-way: the processor cannot be synced if the +node isn't. So `wallet.is_synced` is the single condition you +actually need to gate work on — the node-level signals are useful for +*reporting progress*, not for unblocking calls. + +## Reading SyncState payloads + +The `SyncState` event carries one substate per emission. Payload +shape: + +```python +{ + "type": "sync-state", + "data": { + "syncState": { + "type": "", + "data": { ... }, # variant-specific + }, + }, +} +``` + +The substates fall into three groups: + +| Group | `type` | `data` fields | Layer | +| --- | --- | --- | --- | +| IBD progress | `proof` | `level: int` | Node | +| | `headers` | `headers: int, progress: int` | Node | +| | `blocks` | `blocks: int, progress: int` | Node | +| | `utxo-sync` | `chunks: int, total: int` | Node | +| | `trust-sync` | `processed: int, total: int` | Node | +| Resync | `utxo-resync` | *(none)* | Node | +| Terminal | `not-synced` | *(none)* | Processor | +| | `synced` | *(none)* | Processor | + +The IBD substates make it easy to drive a progress bar without +parsing kaspad logs yourself: + +```python +def on_event(event): + if event["type"] != "sync-state": + return + state = event["data"]["syncState"] + kind = state["type"] + if kind == "headers": + print(f"headers {state['data']['headers']:,} ({state['data']['progress']}%)") + elif kind == "blocks": + print(f"blocks {state['data']['blocks']:,} ({state['data']['progress']}%)") + elif kind == "synced": + print("processor ready") +``` + +## A staged wait + +If you want to surface node IBD separately from processor readiness: + +```python +import asyncio +from kaspa import Resolver, Wallet, WalletEventType + +node_synced = asyncio.Event() +processor_ready = asyncio.Event() + +def on_event(event): + t = event["type"] + if t == "server-status" and event["data"]["isSynced"]: + node_synced.set() + elif t == "sync-state" and event["data"]["syncState"]["type"] == "synced": + processor_ready.set() + +wallet = Wallet(network_id="testnet-10", resolver=Resolver()) +wallet.add_event_listener(WalletEventType.All, on_event) + +await wallet.start() +await wallet.connect() + +await node_synced.wait() # node finished IBD +await processor_ready.wait() # processor registered + ready +assert wallet.is_synced +``` + +For most scripts, the simpler form is fine: + +```python +await wallet.start() +await wallet.connect() +while not wallet.is_synced: + await asyncio.sleep(0.5) +``` + +## Reconnects and `Disconnect` + +A `Disconnect` event flips `wallet.is_synced` back to `False`; the +processor re-runs its handshake on the next `Connect` and re-emits a +fresh `ServerStatus` plus `SyncState` chain. Long-running listeners +should treat the gate as *re-arming*, not one-shot — gate every +`accounts_*` batch on `is_synced`, not on a once-set flag. + +## Where to next + +- [Lifecycle](lifecycle.md#sync-gate) — the polling form of the sync gate, in context of the boot sequence. +- [Architecture](architecture.md) — where the processor and RPC client + sit in the component graph. +- [Transaction History](transaction-history.md) — the full event + taxonomy these events live in. diff --git a/docs/learn/wallet/transaction-history.md b/docs/learn/wallet/transaction-history.md index 4241bb87..5b662f6e 100644 --- a/docs/learn/wallet/transaction-history.md +++ b/docs/learn/wallet/transaction-history.md @@ -1,10 +1,10 @@ # Transaction History -The wallet emits events for every state change the node pushes through. You -register Python callbacks on the wallet, the wallet's event multiplexer -forwards relevant events to them, and you get to react: update a UI, -trigger the next send, log a maturity, ignore a re-org. The history APIs -(`transactions_data_get` and friends) round this out for the +The wallet emits events for every state change the node pushes +through. Register Python callbacks on the wallet; its event +multiplexer forwards relevant events to them, and you react — update +a UI, trigger the next send, log a maturity, handle a reorg. The +history APIs (`transactions_data_get` and friends) cover the "what happened in this account?" question. ## Listener API @@ -14,21 +14,26 @@ def add_event_listener(event, callback, *args, **kwargs) -> None def remove_event_listener(event, callback=None) -> None ``` -- `event`: a `WalletEventType`, its lowercase string name (`"balance"`, - `"connect"`), or `"all"` / `WalletEventType.All` for every event. -- `callback`: invoked as `callback(event, *args, **kwargs)`. Sync or async. -- `args` / `kwargs`: forwarded verbatim to every invocation — handy for - routing context (account id, channel) without closures. -- `remove_event_listener(event)` with no callback clears every listener - for that event. With `"all"` and no callback, clears every listener. +- `event` — a + [`WalletEventType`](../../reference/Enums/WalletEventType.md), its + kebab-case string name (`"balance"`, `"sync-state"`), or `"all"` / + `WalletEventType.All` for every event. +- `callback` — invoked as `callback(*args, event, **kwargs)`. Must be + a regular (synchronous) function; the dispatcher calls it inline and + does not await coroutines, so an `async def` callback's body never + runs. +- `args` / `kwargs` — forwarded verbatim to every invocation. Handy + for routing context (account id, channel) without closures. +- `remove_event_listener(event)` with no callback clears every + listener for that event. With `"all"` and no callback, clears every + listener globally. ## A minimal subscriber ```python -import asyncio from kaspa import Resolver, Wallet, WalletEventType -async def on_event(event): +def on_event(event): print(event["type"], event.get("data")) wallet = Wallet(network_id="testnet-10", resolver=Resolver()) @@ -39,8 +44,9 @@ await wallet.connect() # ... events stream in for the rest of the session ... ``` -Each event is a dict with at least a `type` key (the `WalletEventType` -name as a string) and an optional `data` payload specific to that event. +Each event is a dict with at least a `type` key (the kebab-case kind +name, e.g. `"balance"`, `"sync-state"`, `"fee-rate"`) and an optional +`data` payload specific to that event. ## Event taxonomy @@ -55,15 +61,16 @@ name as a string) and an optional `data` payload specific to that event. The most common subscriptions: -- **`SyncState`** — progress while the `UtxoProcessor` catches up. Pair - with `wallet.is_synced` (see [Start](start.md)). -- **`Balance`** — fires whenever a `UtxoContext` balance changes. The +- **`SyncState`** — progress while the `UtxoProcessor` catches up. + Pair with `wallet.is_synced` — see [Sync State](sync-state.md) for + the substate payload shape. +- **`Balance`** — fires when a `UtxoContext` balance changes. The right signal for live UI updates. -- **`Pending`** — a new UTXO landed for a tracked address but isn't yet - spendable. -- **`Maturity`** — a previously-pending UTXO has reached the maturity +- **`Pending`** — a new UTXO landed for a tracked address but isn't + yet spendable. +- **`Maturity`** — a previously-pending UTXO crossed the maturity depth and is now spendable. The strongest gate for "send-then-wait" - flows: don't trigger the next `accounts_send` on `Pending` alone. + flows — don't trigger the next `accounts_send` on `Pending` alone. - **`Reorg`** / **`Stasis`** — a UTXO was unwound or coinbase-locked. Defensive code for high-value flows. - **`AccountActivation`** / **`AccountDeactivation`** — react to @@ -81,7 +88,7 @@ To pass context to a generic callback: ```python wallet.add_event_listener("balance", on_change, account.account_id, label="primary") -# callback receives: on_change(event, account.account_id, label="primary") +# callback receives: on_change(account.account_id, event, label="primary") ``` ## History queries @@ -96,17 +103,22 @@ data = await wallet.transactions_data_get( end=20, ) # Annotate / re-annotate: -await wallet.transactions_replace_note(account.account_id, tx_id, "rent") -await wallet.transactions_replace_metadata(account.account_id, tx_id, {"tag": "ops"}) +await wallet.transactions_replace_note( + account.account_id, "testnet-10", tx_id, "rent", +) +await wallet.transactions_replace_metadata( + account.account_id, "testnet-10", tx_id, '{"tag": "ops"}', +) ``` -The note and metadata fields are wallet-local — they live in the wallet -file, not on chain. +Note and metadata are free-form strings stored in the wallet file — +not on chain. If you want structured metadata, encode it yourself +(JSON, etc.). ## Cleanup -Listeners outlive the wallet's open file but not its runtime. Always pair -a permanent registration with an explicit removal on shutdown, or use +Listeners outlive the wallet's open file but not its runtime. Pair a +permanent registration with an explicit removal on shutdown, or use `"all"` to clear in one call: ```python @@ -116,7 +128,10 @@ await wallet.stop() ## Where to next -- [Send Transaction](send-transaction.md) — `Maturity` as the right wait - condition. -- [Architecture](architecture.md) — what's actually generating these events. +- [Send Transaction](send-transaction.md) — `Maturity` as the right + wait condition. +- [Architecture](architecture.md) — what's actually generating these + events. - [Lifecycle](lifecycle.md) — when each event group fires. +- [Wallet SDK → UTXO Processor](../wallet-sdk/utxo-processor.md) — + the lower-level event surface beneath the managed wallet. diff --git a/docs/learn/wallet/utxo-maturity.md b/docs/learn/wallet/utxo-maturity.md new file mode 100644 index 00000000..fa68fb07 --- /dev/null +++ b/docs/learn/wallet/utxo-maturity.md @@ -0,0 +1,37 @@ +# UTXO Maturity + +Every UTXO the [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) +sees moves through three states: + +- **Pending** — seen, but confirmation depth is below the maturity + threshold. Counted in `Balance.pending`. *Not* spendable. +- **Mature** — confirmed deeply enough to spend. Counted in + `Balance.mature`. Returned by `accounts_get_utxos`. Selectable. +- **Outgoing** — locked because the wallet just spent it in a + transaction it generated. Counted in `Balance.outgoing` until the + spend matures or is reorged out. + +[Send Transaction](send-transaction.md) waits on `Maturity` for this +reason: a `Pending` UTXO is real, but the next `accounts_send` won't +see it as spendable. + +## Why `accounts_get_utxos` can return `[]` + +`accounts_get_utxos` reads the in-memory `UtxoContext`. It returns +`[]` when: + +1. The wallet isn't synced yet — see [Sync State](sync-state.md). +2. The account hasn't been activated. +3. No notification for a funding tx has reached the processor yet. + +None of these mean "the address has no funds" — they mean "the wallet +hasn't been told yet." Listen for `Maturity` instead of polling. + +## Where to next + +- [Sync State](sync-state.md) — the gate that controls when UTXOs + start flowing. +- [Send Transaction](send-transaction.md) — `Maturity` as the gate for + send-then-wait flows. +- [Transaction History](transaction-history.md) — `Pending`, + `Maturity`, `Reorg`, and `Stasis` events. diff --git a/docs/learn/wallet/open.md b/docs/learn/wallet/wallet-files.md similarity index 50% rename from docs/learn/wallet/open.md rename to docs/learn/wallet/wallet-files.md index ee1bdc7b..ff91dc2a 100644 --- a/docs/learn/wallet/open.md +++ b/docs/learn/wallet/wallet-files.md @@ -1,73 +1,25 @@ -# Open +# Wallet Files -A wallet file is a single encrypted file on disk. Only one is open at a -time per `Wallet` instance. This page covers the file-management surface: -create, open, enumerate, export/import, change-secret, rename. +A wallet file is a single encrypted file on disk. Only one is open at +a time per `Wallet` instance. Creating, opening, and closing a file is +covered in [Lifecycle](lifecycle.md); this page covers the +file-management surface that runs *around* the open file: listing +what's on disk, backing up, restoring, renaming, and rotating the +password. ## Surface | Method | Purpose | | --- | --- | | `wallet_enumerate()` | List every wallet file in the store. | -| `wallet_create(...)` | Create a new encrypted file and open it. | -| `wallet_open(...)` | Decrypt and open an existing file. | -| `wallet_close()` | Release the open file; secrets leave memory. | | `wallet_export(...)` | Dump the encrypted payload as hex. | | `wallet_import(...)` | Materialise a previously exported payload as a new file. | | `wallet_change_secret(...)` | Re-encrypt the open file with a new password. | | `wallet_rename(...)` | Update the title (and/or filename — see warning below). | All methods require `start()`; none require a wRPC connection. - -## Create - -```python -created = await wallet.wallet_create( - wallet_secret="example-secret", - filename="demo", - overwrite_wallet_storage=False, - title="demo", - user_hint="example", -) -``` - -- `filename` is the on-disk basename; omit for the SDK default. -- `overwrite_wallet_storage=False` raises `WalletAlreadyExistsError` if - the file exists; pass `True` to clobber. -- `user_hint` is stored alongside the file as a recoverable password hint. - -A freshly created wallet has no private key data and no accounts — see -[Private Keys](private-keys.md) and [Accounts](accounts.md). - -## Open - -```python -opened = await wallet.wallet_open( - wallet_secret="example-secret", - account_descriptors=True, # include account list in the response - filename="demo", -) -``` - -`account_descriptors=True` returns the account list in the response dict -so you can pick which to activate without a follow-up -`accounts_enumerate()`. - -## Create-or-open pattern - -`wallet_create` raises `WalletAlreadyExistsError` when the file exists. -The canonical idempotent boot is: - -```python -from kaspa.exceptions import WalletAlreadyExistsError - -try: - await wallet.wallet_create( - wallet_secret=secret, filename="demo", overwrite_wallet_storage=False, - ) -except WalletAlreadyExistsError: - await wallet.wallet_open(secret, True, "demo") -``` +`wallet_change_secret` and `wallet_rename` operate on the currently +open file. ## Enumerate @@ -77,8 +29,8 @@ for d in descriptors: print(d.filename, d.title) ``` -Returns a `list[WalletDescriptor]`. Available before any wallet is opened — -useful for showing the user a wallet picker. +Returns a `list[WalletDescriptor]`. Available before any wallet is +opened — useful for a wallet picker UI. ## Export & import @@ -97,9 +49,9 @@ await wallet.wallet_open("example-secret", True, new_filename) ``` The exported payload is borsh-serialized and remains encrypted with -`wallet_secret`; private key material never leaves memory in the clear. -`wallet_import` writes a new file in the store and returns its descriptor — -you still need to `wallet_open` it. +`wallet_secret`; private key material never leaves memory in the +clear. `wallet_import` writes a new file in the store and returns its +descriptor — you still need to `wallet_open` it. ## Change secret @@ -110,8 +62,8 @@ await wallet.wallet_change_secret( ) ``` -Re-encrypts the open file in place. The wallet stays open under the new -secret; future `wallet_open` calls must use the new password. +Re-encrypts the open file in place. The wallet stays open under the +new secret; subsequent `wallet_open` calls must use the new password. ## Rename @@ -131,6 +83,8 @@ await wallet.wallet_rename( ## Where to next -- [Private Keys](private-keys.md) — the next step after creating a wallet. +- [Lifecycle](lifecycle.md) — create, open, close, and the rest of + the state machine. +- [Private Keys](private-keys.md) — populate the open wallet with key + data. - [Accounts](accounts.md) — derive accounts from stored key data. -- [Lifecycle](lifecycle.md) — how these calls fit into start/stop. diff --git a/mkdocs.yml b/mkdocs.yml index 1056e41a..6a0667ff 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -70,7 +70,11 @@ plugins: markdown_extensions: - admonition - pymdownx.details - - pymdownx.superfences + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.highlight: anchor_linenums: true line_spans: __span @@ -124,13 +128,13 @@ nav: - Overview: learn/wallet/overview.md - Architecture: learn/wallet/architecture.md - Lifecycle: learn/wallet/lifecycle.md - - Initialize: learn/wallet/initialize.md - - Start: learn/wallet/start.md - - Open: learn/wallet/open.md + - Sync State: learn/wallet/sync-state.md + - Wallet Files: learn/wallet/wallet-files.md - Private Keys: learn/wallet/private-keys.md - Accounts: learn/wallet/accounts.md - Addresses: learn/wallet/addresses.md - Keypair Accounts: learn/wallet/keypair.md + - UTXO Maturity: learn/wallet/utxo-maturity.md - Send Transaction: learn/wallet/send-transaction.md - Sweep Funds: learn/wallet/sweep.md - Transaction History: learn/wallet/transaction-history.md From 0999bdfdd073b2c09c36d49c3246710f24f31dc0 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 2 May 2026 08:39:11 -0400 Subject: [PATCH 4/7] docs restructure --- README.md | 5 +- docs/CHANGELOG.md | 4 +- docs/examples.md | 40 +++++ docs/getting-started/examples.md | 185 -------------------- docs/getting-started/security.md | 98 +++++++---- docs/guides/custom-derivation.md | 60 ------- docs/guides/message-signing.md | 110 ------------ docs/guides/mnemonics.md | 74 -------- docs/guides/multisig.md | 99 ----------- docs/guides/wallet-recovery.md | 96 ----------- docs/index.md | 48 +++--- docs/learn/addresses.md | 79 +++++---- docs/learn/concepts.md | 109 ------------ docs/learn/index.md | 33 ++-- docs/learn/networks.md | 89 +++++----- docs/learn/rpc/calls.md | 76 +++++--- docs/learn/rpc/connecting.md | 80 +++++---- docs/learn/rpc/overview.md | 72 +++++++- docs/learn/rpc/resolver.md | 28 +-- docs/learn/rpc/subscriptions.md | 138 ++++++++------- docs/learn/transactions/inputs.md | 58 ++----- docs/learn/transactions/mass-and-fees.md | 66 ++++--- docs/learn/transactions/metadata.md | 64 +++---- docs/learn/transactions/outputs.md | 53 ++---- docs/learn/transactions/overview.md | 65 +++---- docs/learn/transactions/scripts.md | 210 +++++++++++++++++++++++ docs/learn/transactions/serialization.md | 54 +++--- docs/learn/transactions/signing.md | 78 ++++----- docs/learn/transactions/submission.md | 59 +++---- docs/learn/wallet-sdk/derivation.md | 141 +++++---------- docs/learn/wallet-sdk/key-management.md | 28 +-- docs/learn/wallet-sdk/overview.md | 97 ++++++++--- docs/learn/wallet-sdk/tx-generator.md | 114 ++++++++---- docs/learn/wallet-sdk/utxo-context.md | 53 ++++-- docs/learn/wallet-sdk/utxo-processor.md | 77 ++++++--- docs/learn/wallet/accounts.md | 127 +++++++++----- docs/learn/wallet/addresses.md | 46 ++--- docs/learn/wallet/architecture.md | 31 ++-- docs/learn/wallet/errors.md | 28 +++ docs/learn/wallet/events.md | 190 ++++++++++++++++++++ docs/learn/wallet/keypair.md | 79 --------- docs/learn/wallet/lifecycle.md | 170 ++++++++---------- docs/learn/wallet/overview.md | 70 +++++--- docs/learn/wallet/private-keys.md | 39 ++--- docs/learn/wallet/send-transaction.md | 142 +++++++++++---- docs/learn/wallet/sweep.md | 46 ++--- docs/learn/wallet/sync-state.md | 115 ++++++------- docs/learn/wallet/transaction-history.md | 161 ++++++----------- docs/learn/wallet/utxo-maturity.md | 37 ---- docs/learn/wallet/wallet-files.md | 41 +++-- docs/llms.txt | 3 +- docs/stylesheets/extra.css | 36 +++- mkdocs.yml | 31 ++-- python/kaspa/__init__.pyi | 164 +----------------- 54 files changed, 1969 insertions(+), 2327 deletions(-) create mode 100644 docs/examples.md delete mode 100644 docs/getting-started/examples.md delete mode 100644 docs/guides/custom-derivation.md delete mode 100644 docs/guides/message-signing.md delete mode 100644 docs/guides/mnemonics.md delete mode 100644 docs/guides/multisig.md delete mode 100644 docs/guides/wallet-recovery.md delete mode 100644 docs/learn/concepts.md create mode 100644 docs/learn/transactions/scripts.md create mode 100644 docs/learn/wallet/errors.md create mode 100644 docs/learn/wallet/events.md delete mode 100644 docs/learn/wallet/keypair.md delete mode 100644 docs/learn/wallet/utxo-maturity.md diff --git a/README.md b/README.md index 5f00a1e5..1f0b4fdd 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ A native extension module, `kaspa`, is built from these bindings using [PyO3](ht This SDK provides features in two primary categories: -- RPC Client - Connect to Kaspa nodes & PNN, perform calls, subscriptions, etc. -- Wallet Management - Wallet related functionality (key management, derivation, addresses, transactions, etc.). +- RPC Client — RPC API for the Kaspa node using WebSockets. +- Wallet SDK — Bindings for wallet-related primitives such as key management, derivation, and transactions. +- Managed Wallet — a fully managed interface for the Rusty Kaspa Wallet API bundled into one Python class. This package strives to mirror the [Kaspa WASM32 SDK](https://kaspa.aspectron.org/docs/) from a feature and API perspective, while respecting Python conventions. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 037cf5b6..153dfff8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -20,9 +20,6 @@ - Wallet-specific exception classes populated into the `kaspa.exceptions` submodule, covering the rusty-kaspa wallet error variants (e.g. `WalletInsufficientFundsError`, `WalletAccountNotFoundError`, `WalletNotSyncedError`, etc.). - Examples under `examples/wallet/` demonstrating wallet usage - Pytest options `--network-id` and `--rpc-url` for targeting integration tests at a specific network / node. -- Documentation site reorganised around [Diataxis](https://diataxis.fr): a sequenced **Learn** section (RPC, Wallet, Wallet SDK, Networks, Addresses, Transactions, Kaspa Concepts) covers the SDK topic by topic, with a focused **Guides** cookbook for cross-cutting recipes (mnemonic restore, message signing, wallet recovery, custom derivation, multisig). -- `docs/getting-started/security.md`: a single canonical page covering secret-handling rules. Other pages link to it instead of duplicating the warning. -- Learn → Transactions split into a dedicated section: Overview, Inputs, Outputs, Mass & Fees, Signing, Submission, Metadata Fields, Serialization. Each page covers one component of a transaction with light Kaspa-protocol context, replacing the single `learn/transactions.md` page. ### Changed - `py_error_map!` macro extended to register wallet exception variants into the `kaspa.exceptions` submodule. @@ -31,6 +28,7 @@ - `pyproject.toml`: set `python-source = "python"` and moved the package stub tree under `python/kaspa/` (`kaspa.pyi` → `python/kaspa/__init__.pyi`). - `Hash` accepts `str` in addition to `Hash` instances wherever it is used as an argument, and gained `to_hex()` and `__repr__` methods. - Added `Balance` `__repr__` method. +- Documentation site reorganization ### Fixed - `AccountDescriptor.__repr__` now correctly renders optional fields. diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 00000000..c90e1762 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,40 @@ +# Examples + +Runnable examples live in the SDK repository, not in these docs. Each +script is self-contained and demonstrates one feature end-to-end. + +[Browse examples on GitHub →](https://github.com/kaspanet/kaspa-python-sdk/tree/main/examples) + +## What's there + +- **[`rpc/`](https://github.com/kaspanet/kaspa-python-sdk/tree/main/examples/rpc)** + — connecting via the resolver, calling RPC methods, subscribing to + notifications. +- **[`wallet/`](https://github.com/kaspanet/kaspa-python-sdk/tree/main/examples/wallet)** + — managed `Wallet` lifecycle: creating, opening, accounts, sending, + export/import. +- **[`transactions/`](https://github.com/kaspanet/kaspa-python-sdk/tree/main/examples/transactions)** + — building transactions with the `Generator`, `UtxoContext`, + multisig, and KRC-20 deploys. +- **[`derivation.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/derivation.py)** + — BIP-32 / BIP-44 key derivation. +- **[`mnemonic.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/mnemonic.py)** + — generating and restoring mnemonics. +- **[`message_signing.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/message_signing.py)** + — signing and verifying messages with a private key. +- **[`addresses.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/addresses.py)** + — encoding, parsing, and validating Kaspa addresses. + +## Running them + +Clone the repo, install the SDK, and run any script directly: + +```bash +git clone https://github.com/kaspanet/kaspa-python-sdk +cd kaspa-python-sdk +pip install kaspa +python examples/rpc/all_calls.py +``` + +See [Installation](getting-started/installation.md) for build-from-source +instructions. diff --git a/docs/getting-started/examples.md b/docs/getting-started/examples.md deleted file mode 100644 index c7e4dc8d..00000000 --- a/docs/getting-started/examples.md +++ /dev/null @@ -1,185 +0,0 @@ -# Examples - -This page contains a handful of brief examples showing core features of the Kaspa Python SDK. - -!!! warning "Handle secrets carefully" - These snippets pass private-key material as inline strings for - readability. That is not how production code should handle secrets — - see [Security](security.md). - -## Examples on Github - -In addition to the examples below, a collection of examples can be found in the Github repository [here](https://github.com/kaspanet/kaspa-python-sdk/tree/master/examples). - -## Kaspa RPC Client - -```python -import asyncio -from kaspa import RpcClient, Resolver, NetworkId - -async def main(): - # Create a resolver to use available PNN nodes - resolver = Resolver() - - # Create RPC client - client = RpcClient( - resolver=resolver, - network_id=NetworkId("mainnet") - ) - - # Connect to the network - await client.connect() - print(f"Connected to: {client.url}") - - # Get BlockDAG info - info = await client.get_block_dag_info() - print(f"BlockDAG Info: {info}") - - await client.disconnect() - -asyncio.run(main()) -``` - -## Check Address Balances - -```python -import asyncio -from kaspa import RpcClient, Resolver, Address - -async def check_balance(address_str: str): - client = RpcClient(resolver=Resolver(), network_id="mainnet") - await client.connect() - - try: - result = await client.get_balance_by_address({ - "address": address_str - }) - - # Balance is in sompi (1 KAS = 100,000,000 sompi) - balance_sompi = result.get("balance", 0) - balance_kas = balance_sompi / 100_000_000 - print(f"Balance: {balance_kas} KAS") - - finally: - await client.disconnect() - -asyncio.run(check_balance("kaspa:qz...")) -``` - -## Creating a Wallet - -```python -from kaspa import Mnemonic, XPrv, PrivateKeyGenerator, NetworkType - -# Generate a new 24-word mnemonic -mnemonic = Mnemonic.random() -print(f"Your seed phrase: {mnemonic.phrase}") - -# IMPORTANT: Store this phrase securely! -# Anyone with this phrase can access your funds. - -# Convert mnemonic to seed -seed = mnemonic.to_seed() - -# Create extended private key from seed -xprv = XPrv(seed) - -# Create a key generator for deriving addresses -key_gen = PrivateKeyGenerator(xprv, False, 0) -``` - -## Generating Addresses - -With the key generator, you can derive addresses: - -```python -# ... continuation of example above - -# Get the first receive address -private_key = key_gen.receive_key(0) -address = private_key.to_address(NetworkType.Mainnet) -print(f"Your address: {address.to_string()}") - -# Generate multiple addresses -for i in range(5): - pk = key_gen.receive_key(i) - addr = pk.to_address(NetworkType.Mainnet) - print(f"Address {i}: {addr.to_string()}") -``` - -## Working with Existing Wallets - -To restore a wallet from an existing seed phrase: - -```python -from kaspa import Mnemonic, XPrv, PrivateKeyGenerator, NetworkType - -# Your existing seed phrase -phrase = "word1 word2 word3 ... word24" - -# Validate and create mnemonic -if Mnemonic.validate(phrase): - mnemonic = Mnemonic(phrase) - seed = mnemonic.to_seed() - xprv = XPrv(seed) - key_gen = PrivateKeyGenerator(xprv, False, 0) - - # Derive your first address - address = key_gen.receive_key(0).to_address(NetworkType.Mainnet) - print(f"Restored address: {address.to_string()}") - - # Derive additional addresses as needed... -else: - print("Invalid seed phrase!") -``` - -## Building a Transaction - -```python -import asyncio -from kaspa import ( - RpcClient, Resolver, Generator, PaymentOutput, - Address, PrivateKey, NetworkId -) - -async def send_transaction(): - client = RpcClient(resolver=Resolver(), network_id="mainnet") - await client.connect() - - try: - # Your private key (keep secret!) - private_key = PrivateKey("your-private-key-hex") - sender_address = private_key.to_address("mainnet") - - # Get UTXOs for your address - utxos_response = await client.get_utxos_by_addresses({ - "addresses": [sender_address.to_string()] - }) - - # Create payment output - recipient = Address("kaspa:recipient-address...") - amount = 100_000_000 # 1 KAS in sompi - payment = PaymentOutput(recipient, amount) - - # Create transaction generator - generator = Generator( - network_id=NetworkId("mainnet"), - entries=utxos_response["entries"], - change_address=sender_address, - outputs=[payment], - ) - - # Generate and sign transactions - for pending_tx in generator: - # Sign the transaction - pending_tx.sign([private_key]) - - # Submit to network - tx_id = await pending_tx.submit(client) - print(f"Transaction submitted: {tx_id}") - - finally: - await client.disconnect() - -asyncio.run(send_transaction()) -``` diff --git a/docs/getting-started/security.md b/docs/getting-started/security.md index 7f2f8502..164e7ac9 100644 --- a/docs/getting-started/security.md +++ b/docs/getting-started/security.md @@ -1,49 +1,77 @@ # Security -Working with cryptocurrency means working with secret material. The same key -that lets you spend funds also lets anyone else who obtains it spend them. The -SDK doesn't try to hide this — it gives you direct access to mnemonics, seeds, -private keys, and wallet files. Treat them with care. +The SDK gives you direct access to mnemonics, seeds, private keys, and wallet +files. Anyone who obtains them can spend your funds. Treat them with care. ## What counts as secret material -| Type | Where it appears | Compromise consequence | -| --- | --- | --- | -| **Mnemonic phrase** | `Mnemonic.phrase`, the words in a `Mnemonic` instance | Full recovery of every wallet derived from it | -| **BIP-39 seed (64 bytes)** | `mnemonic.to_seed(...)`, the input to `XPrv(seed)` | Same as the mnemonic | -| **Extended private key (XPrv)** | `XPrv` instance, `xprv.xprv` string | Full control of every account derived under it | -| **Private key** | `PrivateKey`, `private_key.to_string()`, hex export | Full control of every UTXO that pays its address | -| **Wallet secret** | The password passed to `wallet_create`, `prv_key_data_create`, `accounts_send`, etc. | Decrypts the on-disk wallet file | -| **Wallet file** (`.kaspa/`) | The directory the managed `Wallet` writes to | Encrypted, but a weak password is not enough — the file holds every key | - -## Rules - -1. **Never commit secrets to git.** Add the wallet storage directory (`~/.kaspa` or whatever you configured) and any `*.json`, `*.kaspa`, `seed.txt`, `mnemonic.txt` artefacts to `.gitignore` *before* generating real keys. -2. **Never paste a real mnemonic into source code, an issue, a chat message, an LLM prompt, or a screenshot.** The examples throughout these docs use placeholder phrases for a reason. -3. **Don't print or log secret material in production.** The example snippets print mnemonics for clarity; strip those `print()` calls before shipping. -4. **Don't reuse mainnet keys for testing.** Generate a fresh testnet mnemonic and fund it from the testnet faucet. -5. **Use a wallet passphrase ("25th word") for high-value wallets.** A passphrase changes the seed; an attacker with the mnemonic alone gets nothing without it. -6. **Store backups offline.** Paper, hardware-encrypted USB, hardware wallet — not iCloud Notes. -7. **Generate keys on a machine you trust.** Building a release on a shared CI runner and deriving keys there is not the same as deriving them locally. +| Type | Compromise consequence | +| --- | --- | +| **Mnemonic phrase** / **BIP-39 seed** | Full recovery of every wallet derived from it | +| **Extended private key (XPrv)** | Full control of every account derived under it | +| **Private key** | Full control of every UTXO that pays its address | +| **Wallet secret** (password) | Decrypts the on-disk wallet file | +| **Wallet file** (`.kaspa/`) | Encrypted, but only as strong as the password | -## In these docs +## Why Python makes this hard + +Python wasn't designed for handling secrets. Be aware of: + +- **No secure memory.** `str` and `bytes` are immutable — you can't reliably + zero them after use. Copies linger in interpreter caches, tracebacks, and the + garbage collector until reclaimed. +- **Leaks via `repr` and logging.** Frameworks happily dump local variables in + exception traces, debuggers, and structured logs. A `PrivateKey` in scope + during an unhandled exception can land in your error tracker. +- **Process introspection.** Other code in the same interpreter (debuggers, + profilers, third-party libraries, malicious dependencies) can read your + memory. Supply-chain risk is real — audit what you `pip install`. +- **Shell history and env dumps.** Secrets passed as CLI args show up in + `ps`/`history`; secrets in `os.environ` show up in crash reports and + subprocess inheritance. + +## Handling secrets in Python -Code samples in **Learn** and **Guides** show how the SDK works — they pass -literal hex strings and short passwords inline so the snippet is readable. -**That is not how to handle real secrets.** When you adapt a snippet to -production, replace the inline strings with secrets sourced from your secret -manager, environment variables, hardware wallet, or interactive prompt. +- **Source secrets at runtime, never from source code.** Use environment + variables loaded from a `.env` file (with `python-dotenv`), an OS keyring + (`keyring`), a cloud secret manager (AWS Secrets Manager, GCP Secret + Manager, HashiCorp Vault), or `getpass.getpass()` for interactive prompts. +- **Keep secrets in narrow scope.** Load, use, drop the reference. Don't + attach them to long-lived objects, module globals, or class attributes. +- **Strip `print()` and disable verbose logging in production.** Filter + sensitive fields out of structured logs before they reach disk or a SaaS + collector. +- **Isolate signing.** For high-value keys, sign in a separate process, + container, or hardware device — not the same Python process that handles + network I/O. +- **Pin and audit dependencies.** Use a lockfile, review transitive deps, and + prefer `pip install --require-hashes` for production installs. -If a page documents an operation that touches secret material, it links here -instead of repeating this warning in full. +## Operational rules + +1. **Never commit secrets to git.** Add wallet storage paths and any + `*.json`, `seed.txt`, `mnemonic.txt` artefacts to `.gitignore` *before* + generating real keys. +2. **Never paste a real mnemonic into source code, an issue, a chat message, + an LLM prompt, or a screenshot.** +3. **Don't reuse mainnet keys for testing.** Use a fresh testnet mnemonic. +4. **Use a wallet passphrase ("25th word") for high-value wallets.** +5. **Store backups offline** — paper, hardware-encrypted USB, hardware + wallet. Not iCloud Notes. +6. **Generate keys on a machine you trust** — not a shared CI runner. + +## In these docs -## When something does leak +Code samples pass literal hex strings and short passwords inline for +readability. **That is not how to handle real secrets.** Replace inline +strings with values sourced from a secret manager, environment variable, +hardware wallet, or interactive prompt. -If a mnemonic, seed, or private key leaves your control: +## When something leaks 1. Move every UTXO out of the affected wallet *immediately* — to a freshly - derived wallet from a *new* mnemonic, not the same one. -2. Stop using the leaked mnemonic. Don't try to "rotate the passphrase" or - "skip account 0" — derive a new wallet from new entropy. + derived wallet from a *new* mnemonic. +2. Stop using the leaked mnemonic. Don't "rotate the passphrase" or "skip + account 0" — derive from new entropy. 3. Audit any service that accepted that wallet's signed messages or extended public key. diff --git a/docs/guides/custom-derivation.md b/docs/guides/custom-derivation.md deleted file mode 100644 index 0cd3fd64..00000000 --- a/docs/guides/custom-derivation.md +++ /dev/null @@ -1,60 +0,0 @@ -# Custom derivation paths - -The default -[`PrivateKeyGenerator`](../learn/wallet-sdk/derivation.md#privatekeygenerator) -walks the BIP-44 path `m/44'/111111'/'//`. -When you need something off-spec — a custom path for migrating from -another wallet, a one-off subkey, a non-BIP-44 layout — derive directly -from the `XPrv`. - -## Recipe - -```python -from kaspa import DerivationPath, Mnemonic, NetworkType, XPrv - -m = Mnemonic("<24 words>") -xprv = XPrv(m.to_seed()) - -# Walk an arbitrary path -custom = xprv.derive_path("m/9999'/7'/0/42") -print(custom.private_key.to_string()) -print(custom.private_key.to_address(NetworkType.Mainnet).to_string()) - -# Build paths programmatically -path = DerivationPath("m/44'/111111'/0'") -path.push(0) # → m/44'/111111'/0'/0 -path.push(0) # → m/44'/111111'/0'/0/0 -leaf = xprv.derive_path(path) - -# Step by step (no path string needed) -account_xprv = xprv.derive_child(0, hardened=True) -chain_xprv = account_xprv.derive_child(0) -addr_xprv = chain_xprv.derive_child(0) -``` - -## Notes - -- **Hardened path components** end in `'`. Pass `hardened=True` to - `derive_child` for the same effect. -- **Re-using `DerivationPath`.** It's mutable — `push`, `parent`, - `to_string`, `length`, `is_empty`. Convenient when walking a chain - in a loop. -- **The result of `derive_path` is itself an `XPrv`**, so you can keep - deriving below it. -- **Watch-only with a custom path.** Take `XPub` from any node in the - tree and derive *below it* with `XPub.derive_path(...)` (unhardened - components only — hardened derivation requires the private side). -- **Address encoding.** `derive_path` gives you a key, not an address. - Call `.to_address(NetworkType.X)` to encode for the right network. - -## When *not* to do this - -- For everyday HD wallets, use - [`PrivateKeyGenerator`](../learn/wallet-sdk/derivation.md#privatekeygenerator) - — it produces the addresses the rest of the ecosystem expects. -- For multisig, use the cosigner-aware path (`is_multisig=True`, - `cosigner_index=...`) — see - [Multi-signature transactions](multisig.md). -- A custom path means *you own the recovery story*. Document the path - alongside the mnemonic; "I derived from `m/9999'/7'/0/42`" is not - recoverable without that note. diff --git a/docs/guides/message-signing.md b/docs/guides/message-signing.md deleted file mode 100644 index 1bd5cb23..00000000 --- a/docs/guides/message-signing.md +++ /dev/null @@ -1,110 +0,0 @@ -# Sign and verify a message - -Sign arbitrary bytes with a `PrivateKey` and verify with the -corresponding `PublicKey`. Useful for proving address ownership, -authenticating off-chain actions, or stamping structured payloads. - -Read [Security](../getting-started/security.md) before signing with real -keys. - -## Sign - -```python -from kaspa import PrivateKey, sign_message - -key = PrivateKey("<64-char hex>") -signature = sign_message("Hello, I own this address!", key) -``` - -For deterministic signatures (same message + same key produces the same -signature): - -```python -signature = sign_message(message, key, no_aux_rand=True) -``` - -The default uses fresh auxiliary randomness; that's the right choice -unless a downstream consumer needs determinism. - -## Verify - -```python -from kaspa import PublicKey, verify_message - -pub = PublicKey("02a1b2c3...") -# or: pub = key.to_public_key() - -ok = verify_message(message, signature, pub) -``` - -`verify_message` returns `bool`. It does not raise on a mismatch. - -## Recipe: prove address ownership - -```python -import time -from kaspa import sign_message, verify_message - -def prove_ownership(key, address): - timestamp = int(time.time()) - message = f"I own {address.to_string()} at {timestamp}" - return { - "address": address.to_string(), - "timestamp": timestamp, - "message": message, - "signature": sign_message(message, key), - } - -def verify_ownership(proof, pub): - return verify_message(proof["message"], proof["signature"], pub) -``` - -Include a timestamp so the proof can't be replayed indefinitely; the -verifier should reject anything older than its acceptable window. - -## Recipe: signed JSON payload - -When the thing you're signing is structured, canonicalise first: - -```python -import json -from kaspa import sign_message, verify_message - -def sign_json(data, key): - canonical = json.dumps(data, sort_keys=True, separators=(",", ":")) - return {"data": data, "signature": sign_message(canonical, key)} - -def verify_json(envelope, pub): - canonical = json.dumps(envelope["data"], sort_keys=True, separators=(",", ":")) - return verify_message(canonical, envelope["signature"], pub) -``` - -The canonicalisation matters: any non-deterministic serialisation -(default `json.dumps`, key reordering, whitespace) will produce a -signature that won't verify. - -## Recipe: time-limited auth token - -```python -import time -from kaspa import sign_message, verify_message - -def create_token(key, address, ttl=300): - expires = int(time.time()) + ttl - message = f"auth:{address.to_string()}:{expires}" - return { - "address": address.to_string(), - "expires": expires, - "signature": sign_message(message, key), - } - -def verify_token(token, pub): - if int(time.time()) > token["expires"]: - return False, "expired" - message = f"auth:{token['address']}:{token['expires']}" - return verify_message(message, token["signature"], pub), "ok" -``` - -The signature covers the expiry, so a token can't be forged with a later -expiry without re-signing. Use it as a bearer token in HTTP headers, or -embed it in a session payload. diff --git a/docs/guides/mnemonics.md b/docs/guides/mnemonics.md deleted file mode 100644 index f1ac42d8..00000000 --- a/docs/guides/mnemonics.md +++ /dev/null @@ -1,74 +0,0 @@ -# Generate or restore a mnemonic - -How to produce, validate, and convert BIP-39 mnemonic phrases. For the -teaching version of this material, see -[Wallet SDK → Key Management](../learn/wallet-sdk/key-management.md). - -Read [Security](../getting-started/security.md) before generating real -secrets. - -## Generate - -```python -from kaspa import Mnemonic - -# 24 words (default) -m = Mnemonic.random() - -# 12 words -m12 = Mnemonic.random(word_count=12) - -print(m.phrase) -``` - -## Restore from a phrase - -```python -from kaspa import Mnemonic - -phrase = "abandon abandon abandon ... about" - -if not Mnemonic.validate(phrase): - raise ValueError("invalid mnemonic") - -m = Mnemonic(phrase) -``` - -`Mnemonic.validate(phrase)` returns `True` / `False`; it does not raise. -Pass `Language.English` (or another wordlist) as the second argument to -validate against a specific language. - -## Convert to a seed - -```python -seed = m.to_seed() # 64-byte BIP-39 seed -seed_passphrased = m.to_seed("25th-word") # different seed; same mnemonic -``` - -The optional passphrase changes the seed — the same mnemonic with -different passphrases produces different wallets. An attacker who -captures the mnemonic alone gets nothing without the passphrase. - -## Convert to an `XPrv` - -```python -from kaspa import XPrv -xprv = XPrv(seed) -``` - -From here, derive child keys with -[`PrivateKeyGenerator`](../learn/wallet-sdk/derivation.md) or load the -mnemonic into a managed [Wallet](../learn/wallet/private-keys.md). - -## Raw entropy - -The entropy is exposed as a hex string — useful when round-tripping -through a tool that emits entropy rather than words: - -```python -m = Mnemonic.random() -print(m.entropy) - -m.entropy = "" -print(m.phrase) # rebuilt from the new entropy -``` diff --git a/docs/guides/multisig.md b/docs/guides/multisig.md deleted file mode 100644 index 8711bcf7..00000000 --- a/docs/guides/multisig.md +++ /dev/null @@ -1,99 +0,0 @@ -# Multi-signature transactions - -You have N cosigners and want a 2-of-3 (or M-of-N) wallet. Each cosigner -holds their own xprv; spending requires M signatures. - -This is a how-to. For the underlying derivation and transaction -generation primitives, see -[Wallet SDK → Derivation](../learn/wallet-sdk/derivation.md) and -[Wallet SDK → Transaction Generator](../learn/wallet-sdk/tx-generator.md). - -## Build a 2-of-3 multisig address - -```python -from kaspa import ( - create_multisig_address, NetworkType, PublicKey, - PrivateKeyGenerator, -) - -# Each cosigner derives the same account-level public key, with their -# own cosigner_index. Public keys (not private!) are exchanged. -gen0 = PrivateKeyGenerator(xprv=cosigner_0_xprv, is_multisig=True, - account_index=0, cosigner_index=0) -gen1 = PrivateKeyGenerator(xprv=cosigner_1_xprv, is_multisig=True, - account_index=0, cosigner_index=1) -gen2 = PrivateKeyGenerator(xprv=cosigner_2_xprv, is_multisig=True, - account_index=0, cosigner_index=2) - -pubkeys = [ - PublicKey(gen0.receive_key(0).to_public_key().to_string()), - PublicKey(gen1.receive_key(0).to_public_key().to_string()), - PublicKey(gen2.receive_key(0).to_public_key().to_string()), -] - -multisig = create_multisig_address( - minimum_signatures=2, - keys=pubkeys, - network_type=NetworkType.Mainnet, -) -print(multisig.to_string()) -``` - -Send funds to `multisig.to_string()` and you've created a 2-of-3 UTXO -set. - -## Spend from the multisig - -```python -from kaspa import Generator, NetworkId, PaymentOutput - -gen = Generator( - network_id=NetworkId("mainnet"), - entries=multisig_utxos, # UTXOs paying to multisig.address - change_address=multisig, # change goes back to the multisig - outputs=[PaymentOutput(recipient, amount)], - minimum_signatures=2, # accurate mass calculation -) - -for pending in gen: - # Two of the three cosigners' keys - pending.sign([cosigner_0_key, cosigner_1_key]) - tx_id = await pending.submit(client) -``` - -`minimum_signatures=2` matters: the `Generator` budgets mass for the -expected number of signatures. Skip it and the resulting transaction is -under-massed and the node will reject it. - -## Coordinating signatures across machines - -For a real multisig, the cosigners aren't co-resident. The wire-format -hand-off is: - -1. **Coordinator** runs `gen` and collects each `pending.transaction` (or - the dict form `pending.transaction.to_dict()`). -2. **Each signer** receives the unsigned transaction, signs only their - inputs with `pending.create_input_signature(...)`, and ships the - signature back. -3. **Coordinator** assembles the signatures into the - `signature_script` for each input via `pending.fill_input(i, ...)`, - then submits. - -See `pending.create_input_signature(input_index, private_key, -sighash_type=SighashType.All)` and `pending.fill_input(i, script_bytes)` — -both are documented in -[Wallet SDK → Transaction Generator](../learn/wallet-sdk/tx-generator.md). - -## Notes - -- All cosigners must use the **same `account_index`** — the multisig - address is a function of the public keys at that level, and any - mismatch produces a different address. -- `cosigner_index` differentiates the cosigners; it's not a "first / - second / third signer" ordering, it's a deterministic position used - during derivation. Pick once and document it alongside the wallet. -- Multisig addresses are `ScriptHash`-version (see - [Addresses](../learn/addresses.md)). -- For derivation across more than one address index, every cosigner - derives in lockstep — `gen0.receive_key(i)`, `gen1.receive_key(i)`, - `gen2.receive_key(i)` for the same `i`. diff --git a/docs/guides/wallet-recovery.md b/docs/guides/wallet-recovery.md deleted file mode 100644 index 0f7b78e0..00000000 --- a/docs/guides/wallet-recovery.md +++ /dev/null @@ -1,96 +0,0 @@ -# Recover a wallet (BIP-44 scan) - -You have a 24-word mnemonic and you want to restore the accounts it -backs. `accounts_discovery` scans BIP-44 account and address ranges -against a connected node, returns the highest-used `account_index`, and -gets you a list of indices to import. - -This is a how-to. For background on what these primitives mean, see -[Wallet → Accounts](../learn/wallet/accounts.md) and -[Wallet → Private Keys](../learn/wallet/private-keys.md). - -## Recipe - -```python -import asyncio -from kaspa import ( - AccountsDiscoveryKind, PrvKeyDataVariantKind, Resolver, Wallet, -) - -MNEMONIC = "<24 words>" -SECRET = "" - -async def main(): - wallet = Wallet(network_id="mainnet", resolver=Resolver()) - await wallet.start() - await wallet.connect() - - last_used = await wallet.accounts_discovery( - discovery_kind=AccountsDiscoveryKind.Bip44, - address_scan_extent=20, # consecutive empty addresses before stopping - account_scan_extent=5, # consecutive empty accounts before stopping - bip39_mnemonic=MNEMONIC, - bip39_passphrase=None, - ) - print(f"highest used account_index: {last_used}") - - # Open (or create) a wallet file to hold the imports - await wallet.wallet_create(wallet_secret=SECRET, filename="restored") - - pkd_id = await wallet.prv_key_data_create( - wallet_secret=SECRET, - secret=MNEMONIC, - kind=PrvKeyDataVariantKind.Mnemonic, - ) - - descriptors = [] - for i in range(0, last_used + 1): - d = await wallet.accounts_import_bip32( - wallet_secret=SECRET, - prv_key_data_id=pkd_id, - account_index=i, - ) - descriptors.append(d) - - while not wallet.is_synced: - await asyncio.sleep(0.5) - - await wallet.accounts_activate([d.account_id for d in descriptors]) - - await wallet.wallet_close() - await wallet.disconnect() - await wallet.stop() - -asyncio.run(main()) -``` - -## Tuning the extents - -- **`address_scan_extent`** — how far past the last used receive address - to look before declaring an account empty. BIP-44's recommended value - is 20. -- **`account_scan_extent`** — how many consecutive empty accounts to - tolerate before stopping. Most wallets stop at 5; raise it if the - user is known to have skipped account indices. - -If the discovery returns `-1`, no on-chain history exists under that -mnemonic — treat it as a fresh wallet (use `accounts_create_bip32` -instead of import). - -## Why `_import_*` and not `_create_*` - -`accounts_import_bip32` runs an address-discovery scan as part of the -import — addresses already in use are recognised and the receive / -change indices advance accordingly. The `_create_*` variants don't, so -the next address derived would silently re-issue an already-used one. -For a recovery flow, always import. - -## Notes - -- `accounts_discovery` does not require a wallet file to be open; it - *does* require a connected wRPC client. -- For testnets, set `network_id="testnet-10"` (or `testnet-11`) on both - the discovery and the wallet open. -- A passphrase-protected mnemonic must pass the same passphrase to - `bip39_passphrase=...` and to `prv_key_data_create(...)`. Mismatched - passphrases derive unrelated wallets. diff --git a/docs/index.md b/docs/index.md index 1022dbb6..c5508e4a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,22 +1,27 @@ # Kaspa Python SDK This Python package, `kaspa`, provides an SDK for interacting with the -Kaspa network from Python. +Kaspa network from Python. This SDK provides features in the following main categories: -`kaspa` is a native extension module built from bindings to Rust and -[rusty-kaspa](https://github.com/kaspanet/rusty-kaspa) source. -[PyO3](https://pyo3.rs/) and [Maturin](https://www.maturin.rs/) are used -to create bindings and build the extension module. More information on -the inner workings can be found in the -[Contributing section](contributing/index.md). - -!!! warning "Beta Status" - This project is in beta status. +- [RPC Client](learn/rpc/overview.md) — RPC API for the Kaspa node using WebSockets. +- [Wallet SDK](learn/wallet-sdk/overview.md) — Bindings for wallet-related primitives such as key management, derivation, and transactions. +- [Managed Wallet](learn/wallet/overview.md) — A high-level, single Python class interface to the Rusty Kaspa Wallet API. This provides full wallet functionality in the single Python class: `Wallet`. This project closely mirrors [Kaspa's WASM SDK](https://kaspa.aspectron.org/docs/), while trying to -respect Python conventions. Feature parity with the WASM SDK is a work -in progress; not every feature is available yet in Python. +respect Python conventions. + +## Bindings to Rusty Kaspa + +**`kaspa` is a Python-native extension module built from bindings to Rust and +[rusty-kaspa](https://github.com/kaspanet/rusty-kaspa) source.** [PyO3](https://pyo3.rs/) and [Maturin](https://www.maturin.rs/) are used +to create bindings and build the extension module. + +!!! info "As-thin-as-possible" + This project strives to provide as-thin-as-possible Python-compatible wrappers over rusty-kaspa source. Allowing Python developers leverage the features, stability, and security of rusty-kaspa directly, with minimal reimplementation in Python. + +More information on bindings approach and development notes can be found in the +[Contributing section](contributing/index.md). ## A (Very) Basic Example @@ -35,9 +40,6 @@ if __name__ == "__main__": ## How the docs are organised -This site follows the [Diataxis](https://diataxis.fr) framework. Each -section answers a different question: -
- **[Getting Started](getting-started/installation.md)** @@ -48,25 +50,15 @@ section answers a different question: How the SDK is shaped, taught topic by topic. Connections, wallets, derivation, transactions, the Kaspa concepts behind them. -- **[Guides](guides/mnemonics.md)** - Recipes for specific tasks — mnemonic restore, message signing, - multisig, wallet recovery, custom derivation. +- **[Examples](examples.md)** + Runnable scripts on GitHub covering RPC, wallet, transactions, + derivation, mnemonics, message signing, and addresses. - **[API Reference](reference/index.md)** Every public class, method, and signature. Auto-generated.
-## Where to start - -- **New to the SDK:** [Installation](getting-started/installation.md) → - [Learn → RPC](learn/rpc/overview.md) → - [Learn → Wallet SDK → Key Management](learn/wallet-sdk/key-management.md). -- **Looking for a recipe:** jump to [Guides](guides/mnemonics.md). -- **Looking up an API:** [API Reference](reference/index.md). -- **Generating real keys:** read - [Security](getting-started/security.md) first. - ## License This project is licensed under the ISC License. diff --git a/docs/learn/addresses.md b/docs/learn/addresses.md index a75fd62d..376ff8ca 100644 --- a/docs/learn/addresses.md +++ b/docs/learn/addresses.md @@ -8,9 +8,9 @@ instances. ## Anatomy ``` -kaspa: qz0s9f5p7d3e2c4x8n1b6m9k0j2h4g5f3d7a8s9w0e1r2t3y4u5i6o7p8 -^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -prefix bech32-encoded version + payload + checksum +kaspatest:qrxf48dgrdrm70rsk2nqf9p5xj4d4myrwq8mn3wvxcq8… +^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +prefix bech32-encoded version + payload + checksum ``` | Component | Source | @@ -62,6 +62,12 @@ pub = PublicKey("02a1b2c3...") addr = pub.to_address(NetworkType.Mainnet) ``` +[`NetworkType`](../reference/Enums/NetworkType.md) (no suffix) is enough for address derivation: testnet-10 +and testnet-11 share the same `kaspatest:` prefix, so +`NetworkType.Testnet` produces the right address either way. Pass a +string or [`NetworkId`](../reference/Classes/NetworkId.md) only when an API needs to distinguish the two +testnets. + ## Versions | Version | Pays to | Used by | @@ -72,21 +78,19 @@ addr = pub.to_address(NetworkType.Mainnet) ```python addr = Address("kaspa:qz...") -print(addr.version) +print(addr.version) # "PubKey" / "PubKeyECDSA" / "ScriptHash" ``` -## Network prefixes +`addr.version` returns a string. The +[`AddressVersion`](../reference/Enums/AddressVersion.md) enum exists +if you'd rather pattern-match. -| Prefix | Network | -| --- | --- | -| `kaspa:` | mainnet | -| `kaspatest:` | testnet-10, testnet-11 | -| `kaspadev:` | devnet | -| `kaspasim:` | simnet | +## Re-encoding for a different network -To re-encode an address for a different network — e.g. to display the -testnet equivalent of a mainnet address during testing — overwrite -the prefix: +The `prefix` attribute is writable. Setting it re-encodes the bech32 +string with the new prefix and checksum, but **does not change the +underlying public-key or script-hash bytes** — it's a display-time +operation: ```python addr = Address("kaspa:qz...") @@ -94,37 +98,46 @@ addr.prefix = "kaspatest" print(addr.to_string()) # kaspatest:qz... ``` -This rewrites the prefix only; it does *not* re-derive from a key. For -a real key-to-network conversion, derive again with the right -`NetworkType`. +To get the genuine testnet address for a specific *key*, derive it +again under the testnet network: + +```python +key.to_address(NetworkType.Testnet) +``` ## Scripts and addresses ```python from kaspa import ( - Address, NetworkType, ScriptPublicKey, + Address, NetworkType, address_from_script_public_key, pay_to_address_script, ) +# address → script (the lockup you put in a TransactionOutput) +spk = pay_to_address_script(Address("kaspa:qz...")) +print(spk.version, spk.script) + # script → address -spk = ScriptPublicKey(0, "20a1b2c3...") addr = address_from_script_public_key(spk, NetworkType.Mainnet) - -# address → script -spk = pay_to_address_script(Address("kaspa:qz...")) -print(spk.script) ``` -[`pay_to_address_script`](../reference/Functions/pay_to_address_script.md) -is the lockup script you put in a `TransactionOutput` to pay an -address. See [Transactions → Outputs](transactions/outputs.md). +[`address_from_script_public_key`](../reference/Functions/address_from_script_public_key.md) needs a [`NetworkType`](../reference/Enums/NetworkType.md) because the +script doesn't carry a prefix — you have to tell the decoder which +network you're displaying for. See +[Transactions → Outputs](transactions/outputs.md). ## Multi-signature addresses +Build a multi-signature address with [`create_multisig_address`](../reference/Functions/create_multisig_address.md): + ```python from kaspa import create_multisig_address, NetworkType, PublicKey -pubkeys = [PublicKey("02key1..."), PublicKey("02key2..."), PublicKey("02key3...")] +pubkeys = [ + PublicKey("<33-byte compressed-pubkey hex>"), + PublicKey("<33-byte compressed-pubkey hex>"), + PublicKey("<33-byte compressed-pubkey hex>"), +] multi = create_multisig_address( minimum_signatures=2, keys=pubkeys, @@ -134,13 +147,5 @@ print(multi.to_string()) ``` For the full multisig spend flow (address creation, multi-cosigner -signing, submission), see the -[Multi-signature transactions](../guides/multisig.md) recipe. - -## Where to next - -- [Networks](networks.md) — what each prefix means. -- [Transactions → Outputs](transactions/outputs.md) — using addresses - inside transaction outputs. -- [Wallet SDK → Derivation](wallet-sdk/derivation.md) — deriving many - addresses from one key. +signing, submission), see +[`examples/transactions/multisig.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/transactions/multisig.py). diff --git a/docs/learn/concepts.md b/docs/learn/concepts.md deleted file mode 100644 index a2ec0c8f..00000000 --- a/docs/learn/concepts.md +++ /dev/null @@ -1,109 +0,0 @@ -# Kaspa Concepts - -A fast tour of the protocol concepts you'll bump into while using the -SDK. It's deliberately surface-level — for the full picture, see the -[Kaspa MDBook](https://kaspa-mdbook.aspectron.com/). - -## BlockDAG, not blockchain - -Kaspa orders transactions in a **directed acyclic graph of blocks**, -not a linear chain. Multiple blocks can be produced in parallel and -reference the same parents; consensus emerges from a deterministic DAG -ordering (the *virtual chain*), not a single longest chain. - -What this means in practice: - -- **Block rate is high** (one or more blocks per second on testnet-10), - so [`block-added`](rpc/subscriptions.md#available-events) events fire - far more often than on a Bitcoin-shaped chain. -- **Transactions confirm via the virtual chain.** A transaction is - "accepted" when it enters the virtual-chain ordering, not when it - first lands in a block. -- **Reorgs happen at the DAG-ordering level.** A previously-accepted - transaction can be re-ordered out; the SDK surfaces this as a - [`Reorg` event](wallet/transaction-history.md). - -## UTXO model - -Every spendable balance is a set of *unspent transaction outputs*. To -spend, select UTXOs that sum to at least the amount you need, plus a -change output for the leftover. - -The SDK never asks the chain "what's my balance" — it tracks UTXOs -locally, derives a balance, and updates as new ones land or existing -ones spend. See [Wallet → Architecture](wallet/architecture.md) and -[Wallet SDK → UTXO Context](wallet-sdk/utxo-context.md). - -## Virtual chain and DAA score - -Two ordering scalars show up in the SDK: - -- **DAA score** (Difficulty Adjustment Algorithm) — a monotonic counter - that grows roughly with wall-clock time. Use as an "age of block" - comparator; appears on every UTXO via `block_daa_score`. -- **Virtual chain** — the canonical DAG ordering. The - [`virtual-chain-changed`](rpc/subscriptions.md#virtual-chain-progression) - notification reports updates; a transaction is confirmed by appearing - in it. - -For "wait N seconds", DAA score is roughly right. For "wait until this -transaction is confirmed", use a `Maturity` event (see below). - -## Maturity - -A UTXO moves through three states in the wallet's view: - -- **Pending** — seen, not yet confirmed deeply enough to spend. -- **Mature** — confirmed past the maturity threshold; spendable. -- **Outgoing** — locked because the wallet just spent it; awaits the - spend transaction maturing or being re-orged out. - -[Coinbase outputs](https://kaspa-mdbook.aspectron.com/) have a longer -maturity than regular outputs. The SDK applies the right threshold -automatically — you observe via `Pending` / `Maturity` events on the -[managed Wallet](wallet/transaction-history.md) or the -[`UtxoProcessor`](wallet-sdk/utxo-processor.md). - -The right "wait for confirmation" gate is the `Maturity` event for the -specific transaction — not `Pending`, and not "wait N seconds". - -## Mass and the fee market - -Every transaction has a *mass* — a number derived from byte size, -compute cost, and a storage cost (a function of input and output -values). Mass replaces the "size × byte rate" fee model used by some -other UTXO chains. - -The required fee is `mass × fee_rate`, where `fee_rate` reflects current -congestion. Query the rate via -[`client.get_fee_estimate()`](rpc/calls.md#fees) or the wallet's -[`fee_rate_estimate()`](wallet/send-transaction.md#fees). - -The [Transaction Generator](wallet-sdk/tx-generator.md) handles all of -this — it computes mass, picks a rate, and folds the leftover into -change. Set fees explicitly only for non-standard policies (priority -surcharges, exact-balance sweeps, multisig with custom signature -counts). - -## Sompi and KAS - -The atomic unit is the **sompi**: `1 KAS = 100_000_000 sompi`. Every -amount in the SDK — UTXO value, output amount, fee, balance — is in -sompi. Convert at the UI boundary with `sompi_to_kaspa(...)` / -`kaspa_to_sompi(...)`. Don't store KAS as a float internally; use -sompi ints. - -## Subnetworks - -Most transactions live on the default subnetwork (id all zeros). The -field exists for protocol extensions; leave -`subnetwork_id="0000...0"` unchanged when building manually. - -## Where to next - -- [Networks](networks.md) — picking a chain to talk to. -- [Addresses](addresses.md) and - [Transactions](transactions/overview.md) — the on-chain primitives - in Python. -- [Wallet → Architecture](wallet/architecture.md) — how the SDK turns - these concepts into a working wallet. diff --git a/docs/learn/index.md b/docs/learn/index.md index 985f80b8..a374dcb5 100644 --- a/docs/learn/index.md +++ b/docs/learn/index.md @@ -1,23 +1,18 @@ # Learn -- **[RPC](rpc/overview.md)** — the `RpcClient`: resolver, connection, - calls, notifications. -- **[Wallet](wallet/overview.md)** — the managed high-level `Wallet` - API: lifecycle, file storage, accounts, addresses, sending, history. -- **[Wallet SDK](wallet-sdk/overview.md)** — the lower-level primitives - the managed `Wallet` is built on: key management, transaction - `Generator`, derivation, `UtxoContext`, `UtxoProcessor`. -- **[Networks](networks.md)** — working with mainnet, testnets, and the - rest from this SDK. -- **[Addresses](addresses.md)** — a primer on Kaspa addresses. -- **[Transactions](transactions/overview.md)** — the on-chain - primitives: inputs, outputs, mass and fees, signing, submission, - metadata, serialization. -- **[Kaspa Concepts](concepts.md)** — BlockDAG, UTXO model, mass-based - fees, and maturity. Read this if any of those terms feel fuzzy. +The Learn section on this site is grouped into the following main categories: -## Before you get started +- **Fundamentals** — [Networks](networks.md), [Addresses](addresses.md). +- **[RPC](rpc/overview.md)** — connect, call, subscribe. +- **[Wallet](wallet/overview.md)** — managed high-level API. +- **[Wallet SDK](wallet-sdk/overview.md)** — primitives the `Wallet` is built on. +- **[Transactions](transactions/overview.md)** — on-chain primitives. -Read [Security](../getting-started/security.md). The Learn snippets use -literal mnemonics, hex strings, and short passwords for readability — -**that is not how production code should handle secret material.** +--- + +!!! danger "Secrets handling" + The secrets used in code snippets/examples throughout this site are for illustration only. + They do not implement full or proper secret handling. + Snippets throughout Learn use literal mnemonics, hex strings, and short + passwords for readability. **Never handle production secrets this way.** + See [Security](../getting-started/security.md). diff --git a/docs/learn/networks.md b/docs/learn/networks.md index 8ec671f7..25515738 100644 --- a/docs/learn/networks.md +++ b/docs/learn/networks.md @@ -1,42 +1,53 @@ # Networks -Kaspa runs three live networks: a production mainnet and two testnets. -Every SDK entry point that hits the chain — `RpcClient`, `Wallet`, -`Address`, derivation — needs a network identifier to pick the right +The Kaspa community runs various public networks: a production mainnet and a few testnets. +Every SDK entry point that hits the chain +([`RpcClient`](../reference/Classes/RpcClient.md), +[`Wallet`](../reference/Classes/Wallet.md), +[`Address`](../reference/Classes/Address.md), +[derivation](wallet-sdk/derivation.md), etc.) needs a network identifier to pick the right chain and address prefix. ## The networks -| Network | `network_id` | Address prefix | When to use | -| --- | --- | --- | --- | -| Mainnet | `"mainnet"` | `kaspa:` | Production. Real KAS. | -| Testnet 10 | `"testnet-10"` | `kaspatest:` | Mature testnet. Default for SDK examples; faucets available. | -| Testnet 11 | `"testnet-11"` | `kaspatest:` | Higher block-rate testnet for performance work. | -| Devnet | (operator-defined) | `kaspadev:` | A developer-run private chain. | -| Simnet | (operator-defined) | `kaspasim:` | Simulation / unit tests against a local sim. | +| Network | `network_id` | Address prefix | +| --- | --- | --- | +| Mainnet | `"mainnet"` | `kaspa:` | +| Testnet 10 | `"testnet-10"` | `kaspatest:` | +| Testnet 11 | `"testnet-11"` | `kaspatest:` | -Most readers will only touch mainnet and testnet-10. Use testnet-11 -only when you need its higher block rate — the SDK behaves identically -on it. +Operator-run **devnet** (`kaspadev:`) and **simnet** +(`kaspasim:`) also exist for private chains and simulators. -## `network_id` strings vs `NetworkId` - -Most APIs accept the string form (`"mainnet"`, `"testnet-10"`). -[`NetworkId`](../reference/Classes/NetworkId.md) is the typed form — -useful when you want to hold a value without re-parsing: +## Using the identifier ```python -from kaspa import NetworkId +from kaspa import RpcClient, Resolver -mainnet = NetworkId("mainnet") -testnet = NetworkId("testnet-10") +client = RpcClient(resolver=Resolver(), network_id="testnet-10") ``` -`NetworkId.network_type` and `NetworkId.suffix` return the parts. -[`NetworkType`](../reference/Enums/NetworkType.md) is a third form -some APIs accept (`NetworkType.Mainnet`, `NetworkType.Testnet`). All -three describe the same thing; pick whichever reads cleanly at the -call site. +Most APIs accept the string form (`"mainnet"`, `"testnet-10"`). + +## `network_id` strings, `NetworkId`, and `NetworkType` + +Three forms turn up in different APIs: + +- **Plain strings** (`"testnet-10"`) — what most call sites accept. + Carries the suffix. +- **[`NetworkId`](../reference/Classes/NetworkId.md)** — typed, also + carries the suffix. Useful when you want to hold a value without + re-parsing it. Build with `NetworkId("testnet-10")`; read parts with + `.network_type` and `.suffix`. +- **[`NetworkType`](../reference/Enums/NetworkType.md)** — just the + base kind (`Mainnet`, `Testnet`, `Devnet`, `Simnet`). **No suffix.** + Sufficient anywhere only the address prefix matters (key-to-address + derivation, multisig address creation), since testnet-10 and + testnet-11 share the same `kaspatest:` prefix. Not enough to pick a + specific testnet for an RPC client. + +Rule of thumb: pass strings or [`NetworkId`](../reference/Classes/NetworkId.md) to anything that talks to +the chain; [`NetworkType`](../reference/Enums/NetworkType.md) is fine for derivation and address encoding. ## What changes between networks @@ -48,26 +59,6 @@ call site. - **Resolver pool.** A [`Resolver`](rpc/resolver.md) only returns nodes for the configured `network_id`. -- **Maturity depths.** Coinbase maturity differs by network; the SDK - applies the right value automatically. - -## Picking one for development - -- **Writing examples / docs / tests:** use `testnet-10`. It's stable, - has a faucet, and addresses are obviously test-shaped - (`kaspatest:...`). -- **Performance experiments:** use `testnet-11`. Higher block rate - means UTXO churn and event volume resemble a stress test. -- **Production code paths under CI:** parametrise the network — keep - test runs on testnet, mainnet only on a release pipeline. -- **Anything touching mainnet:** read - [Security](../getting-started/security.md) first. - -## Where to next - -- [Addresses](addresses.md) — what the prefix encodes and how versions - fit in. -- [Wallet → Lifecycle](wallet/lifecycle.md#construct) — `network_id` - is a required constructor argument. -- [RPC → Resolver](rpc/resolver.md) — how a `Resolver` finds a node for - the configured network. +- **Maturity depths and block rate.** Coinbase maturity and block rate + differ by network; the SDK applies the right values automatically. + See [UTXO maturity](wallet/send-transaction.md#utxo-maturity). diff --git a/docs/learn/rpc/calls.md b/docs/learn/rpc/calls.md index f2a93881..5fe32e31 100644 --- a/docs/learn/rpc/calls.md +++ b/docs/learn/rpc/calls.md @@ -19,24 +19,44 @@ supply = await client.get_coin_supply() network = await client.get_current_network() ``` +`get_block_dag_info` returns: + +```python +{ + "network": "kaspa-mainnet", + "blockCount": 12345678, + "headerCount": 12345678, + "tipHashes": ["..."], + "difficulty": 1.23e15, + "pastMedianTime": 1700000000000, + "virtualParentHashes": ["..."], + # ...plus virtualDaaScore, sink, pruningPointHash, etc. +} +``` + ## Balances and UTXOs ```python balance = await client.get_balance_by_address({"address": "kaspa:qz..."}) +# {"balance": 100000000} + balances = await client.get_balances_by_addresses({ "addresses": ["kaspa:qz...", "kaspa:qr..."], }) +# {"entries": [{"address": "kaspa:qz...", "balance": 100000000}, ...]} utxos = await client.get_utxos_by_addresses({"addresses": ["kaspa:qz..."]}) for entry in utxos.get("entries", []): print(entry["outpoint"], entry["utxoEntry"]["amount"]) ``` -Use `get_utxos_by_addresses` for one-shot queries or polling. For +Balance amounts are in sompi (1 KAS = 100,000,000 sompi). + +Use [`get_utxos_by_addresses`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.get_utxos_by_addresses) for one-shot queries or polling. For continuous tracking, subscribe to [`utxos-changed`](subscriptions.md#available-events), or use -[`UtxoContext`](../wallet-sdk/utxo-context.md) for per-address tracking -on top of that subscription. +[`UtxoContext`](../../reference/Classes/UtxoContext.md) for per-address tracking +on top of that subscription (see [UTXO Context](../wallet-sdk/utxo-context.md)). ## Blocks @@ -53,6 +73,10 @@ template = await client.get_block_template({ }) ``` +`get_block` returns `{"block": {...}}` where the inner block has +`header`, `transactions`, and `verboseData` keys; verbose data adds the +block hash, child hashes, and merge-set info. + ## Virtual chain Walk the selected-parent chain forward from a known block. Useful for @@ -84,8 +108,9 @@ For a streaming version, subscribe to ### `get_virtual_chain_from_block_v2` -V2 swaps the boolean flag for a verbosity level and returns richer -per-block data: +**Prefer V2 for new code.** It supersedes the V1 call above: it swaps +the boolean flag for a verbosity level and returns richer per-block +data. V1 is kept for backward compatibility. ```python chain = await client.get_virtual_chain_from_block_v2({ @@ -115,9 +140,11 @@ bandwidth and node CPU. ```python result = await client.submit_transaction({ - "transaction": signed_tx, # a Transaction instance + "transaction": signed_tx, # required: a Transaction instance, NOT a dict "allowOrphan": False, }) +# {"transactionId": "..."} + mempool = await client.get_mempool_entries({ "includeOrphanPool": False, "filterTransactionPool": True, @@ -129,10 +156,16 @@ entry = await client.get_mempool_entry({ }) ``` +[`submit_transaction`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.submit_transaction) is the one call where the request dict embeds a +real Python object: the `transaction` value must be a +[`Transaction`](../../reference/Classes/Transaction.md) instance +(passing a dict raises). Every other call on this page is dict-in, +dict-out. + If you have a [`PendingTransaction`](../../reference/Classes/PendingTransaction.md) from the [Transaction Generator](../wallet-sdk/tx-generator.md), -prefer `pending_tx.submit(client)` — it serialises and submits in one +prefer [`pending_tx.submit(client)`](../../reference/Classes/PendingTransaction.md#kaspa.PendingTransaction.submit) — it serialises and submits in one call. See [Submission](../transactions/submission.md) for the full flow and `allowOrphan` semantics. @@ -140,7 +173,13 @@ flow and `allowOrphan` semantics. ```python fee = await client.get_fee_estimate() -# fee["estimate"]["priorityBucket"] etc. +# { +# "estimate": { +# "priorityBucket": {"feerate": 1.0, "estimatedSeconds": 1.0}, +# "normalBuckets": [...], +# "lowBuckets": [...], +# } +# } fee_x = await client.get_fee_estimate_experimental({"verbose": True}) ``` @@ -180,25 +219,16 @@ metrics = await client.get_metrics({ ## Errors -A failing RPC call raises. Handle it like any other coroutine -exception: - -```python -try: - info = await client.get_balance_by_address({"address": addr}) -except Exception as exc: - print("balance lookup failed:", exc) -``` - +Protocol-level failures (invalid address, malformed request, node-side +errors) raise a plain `Exception` — see +[Errors in the overview](overview.md#errors) for the full picture. Connection-level failures retry automatically (see -[Connecting](connecting.md#reconnects)). The exception surface is for -protocol-level failures: invalid address, malformed request, node-side -errors. +[Connecting → Reconnects](connecting.md#reconnects)). ## Where to next - [Subscriptions](subscriptions.md) — server-pushed notifications. - [Wallet → Send Transaction](../wallet/send-transaction.md) — the - managed Wallet wraps `submit_transaction` with sensible defaults. + managed [`Wallet`](../../reference/Classes/Wallet.md) wraps [`submit_transaction`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.submit_transaction) with sensible defaults. - [Wallet SDK → Transaction Generator](../wallet-sdk/tx-generator.md) — - build the transactions you submit via `submit_transaction`. + build the transactions you submit via [`submit_transaction`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.submit_transaction). diff --git a/docs/learn/rpc/connecting.md b/docs/learn/rpc/connecting.md index 8804bfab..0aed6120 100644 --- a/docs/learn/rpc/connecting.md +++ b/docs/learn/rpc/connecting.md @@ -1,46 +1,54 @@ # Connecting -[`RpcClient.connect()`](../../reference/Classes/RpcClient.md#connect) +[`RpcClient.connect()`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.connect) opens the WebSocket; -[`disconnect()`](../../reference/Classes/RpcClient.md#disconnect) +[`disconnect()`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.disconnect) closes it. While connected, every RPC method is callable and notifications stream in. -You can connect via the Public Node Network (PNN) using -[`Resolver`](resolver.md), or directly to a node URL. +You can connect directly to a node URL, or let the SDK discover one for +you via [`Resolver`](../../reference/Classes/Resolver.md) (covered on its own [page](resolver.md)). -## Example +## Connecting to a known node -Connecting to a PNN mainnet node via `Resolver`: +Pass a URL directly. See [Networks](../networks.md) for the canonical +wRPC ports per network: ```python import asyncio -from kaspa import Resolver, RpcClient +from kaspa import RpcClient async def main(): - client = RpcClient(resolver=Resolver(), network_id="mainnet") + client = RpcClient( + url="ws://node.example.com:17110", + network_id="mainnet", + encoding="borsh", # or "json" + ) await client.connect() - - info = await client.get_block_dag_info() - - await client.disconnect() + try: + info = await client.get_block_dag_info() + print(info["blockCount"]) + finally: + await client.disconnect() asyncio.run(main()) ``` -## Connecting to a known node +## Connecting via Resolver -Pass a URL directly. See [Networks](../networks.md) for the canonical -wRPC ports per network: +Skip the URL and let the SDK pick a Public Node Network (PNN) node for +the network you want: ```python -client = RpcClient( - url="ws://node.example.com:17110", - network_id="mainnet", - encoding="borsh", # or "json" -) +from kaspa import Resolver, RpcClient + +client = RpcClient(resolver=Resolver(), network_id="mainnet") +await client.connect() ``` +See [Resolver](resolver.md) for discovery details and when to run your +own node instead. + ## URL schemes - `ws://` — plaintext WebSocket @@ -48,23 +56,28 @@ client = RpcClient( ## Connection options -`connect()` takes a few behavioural overrides: +[`connect()`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.connect) takes a few behavioural overrides: ```python await client.connect( - block_async_connect=True, - strategy="fallback", + block_async_connect=True, # default + strategy="retry", # default url="ws://node.example.com:17110", timeout_duration=30000, retry_interval=1000, ) ``` -- `block_async_connect` — if `False`, `connect()` returns immediately - and the socket opens in the background. -- `strategy` — `"retry"` (default) loops until a connection succeeds; - `"fallback"` returns on the first failure. Applies to both URL-based - and `Resolver`-driven clients. +- `block_async_connect` — `True` (default) makes `connect()` await + until the socket is open. Set to `False` to return immediately and + let the connection complete in the background; check + `client.is_connected` or listen for the `connect` event to know when + it's ready. +- `strategy` — `"retry"` (default) loops until a connection succeeds, + pausing `retry_interval` between attempts; `"fallback"` returns on + the first failure. `timeout_duration` caps each individual attempt + (not the overall wall-clock); under `"retry"` there is no overall + ceiling. Applies to both URL-based and [`Resolver`](../../reference/Classes/Resolver.md)-driven clients. - `url` — overrides the constructor URL for this attempt only. Lets you retarget a long-lived client without rebuilding it. - `timeout_duration` — per-attempt ceiling, in milliseconds. @@ -83,19 +96,20 @@ print(client.resolver) # the Resolver instance, or None ## Encoding: Borsh vs JSON Borsh (the default) is a compact binary format used natively by the -node. +node. The constructor accepts either the string form (`encoding="borsh"`, +`encoding="json"`) or the [`Encoding`](../../reference/Enums/Encoding.md) +enum (`Encoding.Borsh`, `Encoding.Json`) — pick whichever your codebase +prefers and stick to it. Use `"json"` only to inspect raw frames in a tool that doesn't speak -Borsh, or when targeting a node that doesn't support it. See the -[`Encoding`](../../reference/Enums/Encoding.md) enum for accepted -values. +Borsh, or when targeting a node that doesn't support it. ## Reconnects If the WebSocket drops mid-session, the client reconnects on its own. Calls made *during* the gap raise; calls made *after* a successful reconnect work normally. To stop reconnect attempts, call -`disconnect()` — or use `strategy="fallback"`, which gives up after +[`disconnect()`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.disconnect) — or use `strategy="fallback"`, which gives up after one failed reconnect instead of looping. To track disruptions, listen for the `connect` and `disconnect` events (see [Subscriptions](subscriptions.md#available-events)). diff --git a/docs/learn/rpc/overview.md b/docs/learn/rpc/overview.md index f827e946..c4b08fea 100644 --- a/docs/learn/rpc/overview.md +++ b/docs/learn/rpc/overview.md @@ -1,16 +1,33 @@ # RPC -Kaspa nodes expose an RPC API. This SDK wraps it in -[`RpcClient`](../../reference/Classes/RpcClient.md) — one class for +Kaspa nodes expose an RPC API. This SDK provides +[`RpcClient`](../../reference/Classes/RpcClient.md) for interacting with the RPC API. One class for connection management, request/response calls, and event subscriptions. -Higher-level SDK layers build on `RpcClient`. For example, -[`Wallet`](../wallet/overview.md) submits transactions and tracks UTXO -state through it. +## An example + +A complete script — connect to a public mainnet node, fetch DAG state, +disconnect: + +```python +import asyncio +from kaspa import Resolver, RpcClient + +async def main(): + client = RpcClient(resolver=Resolver(), network_id="mainnet") + await client.connect() + try: + info = await client.get_block_dag_info() + print(f"network={info['network']} blocks={info['blockCount']}") + finally: + await client.disconnect() + +asyncio.run(main()) +``` ## Overview -`RpcClient` is an async WebSocket client. Each instance: +[`RpcClient`](../../reference/Classes/RpcClient.md) is an async WebSocket client. Each instance: - Connects to one node at a time. - Reconnects automatically if the socket drops. @@ -18,6 +35,9 @@ state through it. JSON. Pass `encoding="json"` for the JSON wire format. See the [`Encoding`](../../reference/Enums/Encoding.md) enum. +[`RpcClient`](../../reference/Classes/RpcClient.md) is **not** an async context manager — wrap calls in +`try/finally` (or your own helper) to guarantee [`disconnect()`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.disconnect) runs. + ## Two ways to point a client at a node ```python @@ -31,6 +51,46 @@ client = RpcClient(url="wss://node.example.com:17110", network_id="mainnet") See [Resolver](resolver.md) for node discovery and [Connecting](connecting.md) for the connection lifecycle. +## Naming conventions + +Two styles meet at this API and the seam is worth knowing about up +front: + +- **Method names are Python snake_case**: `get_block_dag_info`, + `subscribe_utxos_changed`. +- **Request and response dict keys are camelCase**: `includeTransactions`, + `addedChainBlockHashes`, `payAddress`. + +The camelCase keys mirror the rusty-kaspa wire protocol. Misspelled or +snake_cased keys (`include_transactions`) raise a `KeyError` at the +binding layer rather than being silently ignored. The one historical +exception is `submit_transaction`'s `allow_orphan`, which is accepted +with a `DeprecationWarning` — prefer `allowOrphan`. + +Request and response shapes are `TypedDict`s in the bundled type +stubs (e.g. `GetBlockDagInfoResponse`), so an IDE will autocomplete +the camelCase keys for you. + +## Errors + +[`RpcClient`](../../reference/Classes/RpcClient.md) raises a plain `Exception` for protocol-, network-, and +validation-level failures (the binding layer doesn't currently expose +typed RPC error subclasses). Catch broadly and inspect the message: + +```python +try: + balance = await client.get_balance_by_address({"address": addr}) +except Exception as exc: + print("rpc call failed:", exc) +``` + +Connection drops are handled automatically — see +[Connecting → Reconnects](connecting.md#reconnects). The typed +exception classes under [`kaspa.exceptions`](../../reference/SUMMARY.md) +([`WalletRpcError`](../../reference/Exceptions/WalletRpcError.md), +[`WalletNotConnectedError`](../../reference/Exceptions/WalletNotConnectedError.md), etc.) come from the wallet layer, not raw +[`RpcClient`](../../reference/Classes/RpcClient.md) calls. + ## RPC methods - **[Calls](calls.md)** — request/response RPCs for network info, diff --git a/docs/learn/rpc/resolver.md b/docs/learn/rpc/resolver.md index cdf947a1..15e3f303 100644 --- a/docs/learn/rpc/resolver.md +++ b/docs/learn/rpc/resolver.md @@ -15,7 +15,8 @@ client = RpcClient(resolver=Resolver(), network_id="mainnet") await client.connect() ``` -**For security critical applications, or to ensure a trusted node, you should consider connecting to your own node.** +**For security-critical applications, you should connect to your own +node.** `network_id` selects the network — `"mainnet"` or a testnet (e.g. `"testnet-10"`). It takes a string or a @@ -25,6 +26,8 @@ nodes. ## Constructor options +[`Resolver`](../../reference/Classes/Resolver.md) accepts a few keyword arguments at construction time: + ```python # Default resolver = Resolver() @@ -43,20 +46,25 @@ resolver = Resolver(urls=["https://resolver1.example.org"]) ## Querying the resolver directly -You can fetch a URL without constructing an `RpcClient`: +You can fetch a URL without constructing an [`RpcClient`](../../reference/Classes/RpcClient.md): ```python -from kaspa import Encoding, NetworkId, Resolver +from kaspa import Resolver resolver = Resolver() -url = await resolver.get_url(Encoding.Borsh, NetworkId("mainnet")) -descriptor = await resolver.get_node(Encoding.Borsh, NetworkId("mainnet")) +url = await resolver.get_url("borsh", "mainnet") +descriptor = await resolver.get_node("borsh", "mainnet") ``` -[`get_url`](../../reference/Classes/Resolver.md#get_url) returns a -WebSocket URL ready for `RpcClient(url=...)`. -[`get_node`](../../reference/Classes/Resolver.md#get_node) returns a +Both arguments accept the string form shown above or the typed +equivalents (`Encoding.Borsh`, `NetworkId("mainnet")`) — see +[`Encoding`](../../reference/Enums/Encoding.md) and +[`NetworkId`](../../reference/Classes/NetworkId.md). + +[`get_url`](../../reference/Classes/Resolver.md#kaspa.Resolver.get_url) returns a +WebSocket URL ready for [`RpcClient(url=...)`](../../reference/Classes/RpcClient.md). +[`get_node`](../../reference/Classes/Resolver.md#kaspa.Resolver.get_node) returns a dict with the node's `uid`, `url`, and other metadata. ## Under the hood @@ -64,12 +72,12 @@ dict with the node's `uid`, `url`, and other metadata. You don't need any of this to use `Resolver` — it's here for anyone running their own infrastructure or debugging connectivity. -A `Resolver` doesn't open WebSockets or hold Kaspa node URLs. It holds +A [`Resolver`](../../reference/Classes/Resolver.md) doesn't open WebSockets or hold Kaspa node URLs. It holds a list of *resolver service* HTTP endpoints (see [aspectron/kaspa-resolver](https://github.com/aspectron/kaspa-resolver)) that track live PNN nodes and load-balance across them. -On `get_url` / `get_node` (called internally by `client.connect()`): +On [`get_url`](../../reference/Classes/Resolver.md#kaspa.Resolver.get_url) / [`get_node`](../../reference/Classes/Resolver.md#kaspa.Resolver.get_node) (called internally by [`client.connect()`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.connect)): 1. Pick a configured resolver-service URL at random. 2. `GET {url}/v2/kaspa/{network_id}/{tls_or_any}/wrpc/{encoding}`. diff --git a/docs/learn/rpc/subscriptions.md b/docs/learn/rpc/subscriptions.md index 31fc949d..aff8ba46 100644 --- a/docs/learn/rpc/subscriptions.md +++ b/docs/learn/rpc/subscriptions.md @@ -10,7 +10,7 @@ invokes the callbacks you registered. Every subscription has two parts: 1. **A listener** — a Python callback registered via - [`add_event_listener("", callback)`](../../reference/Classes/RpcClient.md#add_event_listener). + [`add_event_listener("", callback)`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.add_event_listener). 2. **A subscription** — `await client.subscribe_(...)` tells the node to start streaming. @@ -27,20 +27,22 @@ await client.subscribe_utxos_changed([Address("kaspa:qz...")]) ## Available events -| Event name | Subscribe with | Event payload | -| --- | --- | --- | -| `utxos-changed` | [`subscribe_utxos_changed(addresses)`](../../reference/Classes/RpcClient.md#subscribe_utxos_changed) | [`UtxosChangedEvent`](../../reference/TypedDicts/UtxosChangedEvent.md) | -| `block-added` | [`subscribe_block_added()`](../../reference/Classes/RpcClient.md#subscribe_block_added) | [`BlockAddedEvent`](../../reference/TypedDicts/BlockAddedEvent.md) | -| `virtual-chain-changed` | [`subscribe_virtual_chain_changed(include_accepted_transaction_ids=...)`](../../reference/Classes/RpcClient.md#subscribe_virtual_chain_changed) | [`VirtualChainChangedEvent`](../../reference/TypedDicts/VirtualChainChangedEvent.md) | -| `virtual-daa-score-changed` | [`subscribe_virtual_daa_score_changed()`](../../reference/Classes/RpcClient.md#subscribe_virtual_daa_score_changed) | [`VirtualDaaScoreChangedEvent`](../../reference/TypedDicts/VirtualDaaScoreChangedEvent.md) | -| `sink-blue-score-changed` | [`subscribe_sink_blue_score_changed()`](../../reference/Classes/RpcClient.md#subscribe_sink_blue_score_changed) | [`SinkBlueScoreChangedEvent`](../../reference/TypedDicts/SinkBlueScoreChangedEvent.md) | -| `finality-conflict` | [`subscribe_finality_conflict()`](../../reference/Classes/RpcClient.md#subscribe_finality_conflict) | [`FinalityConflictEvent`](../../reference/TypedDicts/FinalityConflictEvent.md) | -| `finality-conflict-resolved` | [`subscribe_finality_conflict_resolved()`](../../reference/Classes/RpcClient.md#subscribe_finality_conflict_resolved) | [`FinalityConflictResolvedEvent`](../../reference/TypedDicts/FinalityConflictResolvedEvent.md) | -| `new-block-template` | [`subscribe_new_block_template()`](../../reference/Classes/RpcClient.md#subscribe_new_block_template) | [`NewBlockTemplateEvent`](../../reference/TypedDicts/NewBlockTemplateEvent.md) | -| `pruning-point-utxo-set-override` | [`subscribe_pruning_point_utxo_set_override()`](../../reference/Classes/RpcClient.md#subscribe_pruning_point_utxo_set_override) | [`PruningPointUtxoSetOverrideEvent`](../../reference/TypedDicts/PruningPointUtxoSetOverrideEvent.md) | - Each `subscribe_*` has a matching `unsubscribe_*` with the same -argument shape. Event names also map to the +argument shape. + +| Event name | Subscribe call | Arguments | Event payload | +| --- | --- | --- | --- | +| `utxos-changed` | [`subscribe_utxos_changed`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.subscribe_utxos_changed) | `addresses: list[Address]` | [`UtxosChangedEvent`](../../reference/TypedDicts/UtxosChangedEvent.md) | +| `block-added` | [`subscribe_block_added`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.subscribe_block_added) | — | [`BlockAddedEvent`](../../reference/TypedDicts/BlockAddedEvent.md) | +| `virtual-chain-changed` | [`subscribe_virtual_chain_changed`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.subscribe_virtual_chain_changed) | `include_accepted_transaction_ids: bool` | [`VirtualChainChangedEvent`](../../reference/TypedDicts/VirtualChainChangedEvent.md) | +| `virtual-daa-score-changed` | [`subscribe_virtual_daa_score_changed`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.subscribe_virtual_daa_score_changed) | — | [`VirtualDaaScoreChangedEvent`](../../reference/TypedDicts/VirtualDaaScoreChangedEvent.md) | +| `sink-blue-score-changed` | [`subscribe_sink_blue_score_changed`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.subscribe_sink_blue_score_changed) | — | [`SinkBlueScoreChangedEvent`](../../reference/TypedDicts/SinkBlueScoreChangedEvent.md) | +| `finality-conflict` | [`subscribe_finality_conflict`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.subscribe_finality_conflict) | — | [`FinalityConflictEvent`](../../reference/TypedDicts/FinalityConflictEvent.md) | +| `finality-conflict-resolved` | [`subscribe_finality_conflict_resolved`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.subscribe_finality_conflict_resolved) | — | [`FinalityConflictResolvedEvent`](../../reference/TypedDicts/FinalityConflictResolvedEvent.md) | +| `new-block-template` | [`subscribe_new_block_template`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.subscribe_new_block_template) | — | [`NewBlockTemplateEvent`](../../reference/TypedDicts/NewBlockTemplateEvent.md) | +| `pruning-point-utxo-set-override` | [`subscribe_pruning_point_utxo_set_override`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.subscribe_pruning_point_utxo_set_override) | — | [`PruningPointUtxoSetOverrideEvent`](../../reference/TypedDicts/PruningPointUtxoSetOverrideEvent.md) | + +Event names also map to the [`NotificationEvent`](../../reference/Enums/NotificationEvent.md) enum if you prefer typed variants over kebab-case strings. @@ -76,52 +78,62 @@ await client.subscribe_virtual_daa_score_changed() Every callback receives a `dict` with a `"type"` key naming the event. The remaining keys depend on the event. -### `utxos-changed` ([`UtxosChangedEvent`](../../reference/TypedDicts/UtxosChangedEvent.md)) -Top-level `"added"` and `"removed"` lists of - [`RpcUtxosByAddressesEntry`](../../reference/TypedDicts/RpcUtxosByAddressesEntry.md). - This is the only event that does *not* nest its body under `"data"` — - it's flattened so callbacks can read `event["added"]` directly: - - ```python - { - "type": "utxos-changed", - "added": [ - { - "address": "kaspa:qz...", - "outpoint": {"transactionId": "...", "index": 0}, - "utxoEntry": { - "amount": 100000000, - "scriptPublicKey": {"version": 0, "script": "..."}, - "blockDaaScore": 123456789, - "isCoinbase": False, - }, +### utxos-changed + +[`UtxosChangedEvent`](../../reference/TypedDicts/UtxosChangedEvent.md) +is the only event that does *not* nest its body under `"data"` — it's +flattened so callbacks can read `event["added"]` directly. The +`"added"` and `"removed"` lists hold +[`RpcUtxosByAddressesEntry`](../../reference/TypedDicts/RpcUtxosByAddressesEntry.md) +items. + +```python +{ + "type": "utxos-changed", + "added": [ + { + "address": "kaspa:qz...", + "outpoint": {"transactionId": "...", "index": 0}, + "utxoEntry": { + "amount": 100000000, + "scriptPublicKey": {"version": 0, "script": "..."}, + "blockDaaScore": 123456789, + "isCoinbase": False, }, - ], - "removed": [], - } - ``` + }, + ], + "removed": [], +} +``` + +### All other node-pushed events -### All other events A `"data"` key holds the notification body. Each event has a wrapper - TypedDict (e.g. [`BlockAddedEvent`](../../reference/TypedDicts/BlockAddedEvent.md)) - and a body TypedDict (e.g. - [`RpcBlockAddedNotification`](../../reference/TypedDicts/RpcBlockAddedNotification.md)). - See the [Available events](#available-events) table for the full - list. For example, a `virtual-daa-score-changed` callback receives: - - ```python - { - "type": "virtual-daa-score-changed", - "data": { - "virtualDaaScore": 123456789, - }, - } - ``` +TypedDict (e.g. +[`BlockAddedEvent`](../../reference/TypedDicts/BlockAddedEvent.md)) and +a body TypedDict (e.g. +[`RpcBlockAddedNotification`](../../reference/TypedDicts/RpcBlockAddedNotification.md)). +See the [Available events](#available-events) table for the full list. +For example, a `virtual-daa-score-changed` callback receives: + +```python +{ + "type": "virtual-daa-score-changed", + "data": { + "virtualDaaScore": 123456789, + }, +} +``` -### `connect` / `disconnect` - ([`ConnectEvent`](../../reference/TypedDicts/ConnectEvent.md) / - [`DisconnectEvent`](../../reference/TypedDicts/DisconnectEvent.md)): - a `"rpc"` key with the node URL. +### connect / disconnect + +[`ConnectEvent`](../../reference/TypedDicts/ConnectEvent.md) and +[`DisconnectEvent`](../../reference/TypedDicts/DisconnectEvent.md) carry +a single `"rpc"` key holding the node URL as a string: + +```python +{"type": "connect", "rpc": "wss://node.example.com:17110"} +``` The bundled [`Notification`](../../reference/Classes/Notification.md) class wraps @@ -154,8 +166,8 @@ await client.unsubscribe_utxos_changed(addresses) ``` To watch a managed-wallet account instead of raw addresses, use the -wallet's `Balance` and `Maturity` events — see -[Wallet → Transaction History](../wallet/transaction-history.md). +[`Wallet`](../../reference/Classes/Wallet.md)'s `Balance` and `Maturity` events — see +[Wallet → Events](../wallet/events.md). ### Block events @@ -221,9 +233,9 @@ before re-subscribing. - [Calls](calls.md) — the request/response side of the API. - [`RpcClient`](../../reference/Classes/RpcClient.md) — full - `subscribe_*` / `unsubscribe_*` / `add_event_listener` reference. -- [`UtxoProcessor`](../wallet-sdk/utxo-processor.md) and - [`UtxoContext`](../wallet-sdk/utxo-context.md) — higher-level UTXO + `subscribe_*` / `unsubscribe_*` / [`add_event_listener`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.add_event_listener) reference. +- [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) (see [UTXO Processor](../wallet-sdk/utxo-processor.md)) and + [`UtxoContext`](../../reference/Classes/UtxoContext.md) (see [UTXO Context](../wallet-sdk/utxo-context.md)) — higher-level UTXO tracking built on `utxos-changed`. -- [Wallet → Transaction History](../wallet/transaction-history.md) — - the managed Wallet's higher-level event surface. +- [Wallet → Events](../wallet/events.md) — the managed [`Wallet`](../../reference/Classes/Wallet.md)'s + higher-level event surface. diff --git a/docs/learn/transactions/inputs.md b/docs/learn/transactions/inputs.md index 456270cb..25976d81 100644 --- a/docs/learn/transactions/inputs.md +++ b/docs/learn/transactions/inputs.md @@ -15,13 +15,11 @@ TransactionInput utxo: UtxoEntryReference # optional, but you almost always want it set ``` -- **`TransactionOutpoint`** — `(transaction_id, index)`. The pointer +- **[`TransactionOutpoint`](../../reference/Classes/TransactionOutpoint.md)** — `(transaction_id, index)`. The pointer to the output being spent. -- **`UtxoEntryReference`** — a cached copy of the *spent output*: its - amount, lockup script, block DAA score, and coinbase flag. See - [UTXO Context](../wallet-sdk/utxo-context.md) for how the SDK - tracks these. -- **`signature_script`** — the unlocking script. Empty string at +- **[`UtxoEntryReference`](../../reference/Classes/UtxoEntryReference.md)** — a cached copy of the *spent output*: its + amount, lockup script, block DAA score, and coinbase flag. +- **`signature_script`** — the unlocking script. Empty (`""`) at build time; filled when you sign. See [Signing](signing.md). - **`sequence`** — sequence number. Leave at `0` unless you have a specific protocol-level reason. @@ -31,12 +29,13 @@ TransactionInput ## Build an input -From a UTXO dict returned by `client.get_utxos_by_addresses(...)`: +From a UTXO dict returned by [`client.get_utxos_by_addresses(...)`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.get_utxos_by_addresses): ```python from kaspa import TransactionInput, TransactionOutpoint, UtxoEntryReference -utxo = utxos["entries"][0] +resp = await client.get_utxos_by_addresses({"addresses": [my_address]}) +utxo = resp["entries"][0] inp = TransactionInput( previous_outpoint=TransactionOutpoint( @@ -57,51 +56,30 @@ outpoint. The SDK can't sign correctly without that context, so `TransactionInput.utxo` *attaches* it directly — no node round-trip needed. -Practical consequences: +Consequences: -- Forgetting `utxo=...` when building manually breaks signing. Always - set it. +- Forgetting `utxo=...` when building manually breaks signing. - A signed transaction can move between processes (offline signer, co-signer, relay) without the receiver needing the source node — every input carries what's needed. -- The Generator handles this — pass a list of `UtxoEntryReference`s - (or a [`UtxoContext`](../wallet-sdk/utxo-context.md)) and it picks - and wraps inputs internally. +- The [`Generator`](../../reference/Classes/Generator.md) handles this for you. Pass [`UtxoEntryReference`](../../reference/Classes/UtxoEntryReference.md)s (or + a [`UtxoContext`](../../reference/Classes/UtxoContext.md) — see [UTXO Context](../wallet-sdk/utxo-context.md)) and it picks and + wraps inputs internally. -## UTXO selection +## Selecting which UTXOs to spend -Selecting inputs that sum to at least `amount + fee` is what the -[Transaction Generator](../wallet-sdk/tx-generator.md) handles. When -building manually: - -- Sum the input values you intend to spend. -- Subtract output amounts and fee — the leftover becomes the change - output. -- Order only matters to your downstream consumers; the protocol - doesn't impose one. - -For input ordering rules, signature aggregation, or "spend exactly -these UTXOs first", see the Generator's `priority_entries` option. +The [`Generator`](../../reference/Classes/Generator.md) (see [Transaction Generator](../wallet-sdk/tx-generator.md)) handles +selection. When building manually, sum input values until the total +covers `outputs + fee`, then route the remainder to a change output. +See [Outputs → Change](outputs.md#change-outputs). ## Reading inputs back ```python for inp in tx.inputs: print(inp.previous_outpoint.transaction_id, inp.previous_outpoint.index) - print(inp.signature_script_as_hex) # hex string, or None pre-sign + print(inp.signature_script_as_hex) # hex string after signing, None before print(inp.sig_op_count, inp.sequence) if inp.utxo: print(inp.utxo.amount, inp.utxo.script_public_key) ``` - -`signature_script_as_hex` returns the unlocking script after signing -as a hex string, or `None` if not yet signed. - -## Where to next - -- [Outputs](outputs.md) — the other half of a transaction. -- [Signing](signing.md) — what "filled at sign time" actually does. -- [Mass & Fees](mass-and-fees.md) — `sig_op_count` feeds the mass - calculator. -- [UTXO Context](../wallet-sdk/utxo-context.md) — managed UTXO state - the SDK keeps in sync with the chain. diff --git a/docs/learn/transactions/mass-and-fees.md b/docs/learn/transactions/mass-and-fees.md index 30847eda..68fbe4af 100644 --- a/docs/learn/transactions/mass-and-fees.md +++ b/docs/learn/transactions/mass-and-fees.md @@ -5,10 +5,6 @@ Kaspa uses a **mass-based fee model**. Every transaction has a *mass* component tied to input and output values. The required fee is `mass × fee_rate`, where `fee_rate` is the network's current rate. -For the protocol view of why mass exists, see -[Kaspa Concepts](../concepts.md#mass-and-the-fee-market). This page -covers the SDK helpers. - ## The two kinds of mass - **Compute / size mass** — from the transaction's serialized size @@ -16,13 +12,16 @@ covers the SDK helpers. - **Storage mass** — from input and output *values*, specifically to discourage UTXO-set bloat (many tiny outputs from one large input). -Total mass is the larger of the two. You don't compute the parts -separately for normal use — `calculate_transaction_mass` and -`update_transaction_mass` handle it. `calculate_storage_mass` is -exposed when you want to inspect the storage component alone. +A transaction's overall mass combines the two; you don't compute the +parts separately for normal use. ## Compute mass for a transaction +The relevant helpers: +[`calculate_transaction_mass`](../../reference/Functions/calculate_transaction_mass.md), +[`update_transaction_mass`](../../reference/Functions/update_transaction_mass.md), +[`maximum_standard_transaction_mass`](../../reference/Functions/maximum_standard_transaction_mass.md): + ```python from kaspa import ( calculate_transaction_mass, @@ -35,19 +34,20 @@ print(mass) print(maximum_standard_transaction_mass()) # protocol upper bound ``` -`calculate_transaction_mass(network_id, tx)` returns the mass without +[`calculate_transaction_mass(network_id, tx)`](../../reference/Functions/calculate_transaction_mass.md) returns the mass without mutating the transaction. To write it onto the transaction itself (required before signing or serializing): ```python -update_transaction_mass("mainnet", tx) -print(tx.mass) +ok = update_transaction_mass("mainnet", tx) +print(tx.mass, ok) # ok is False if mass exceeds the standard limit ``` -**Order matters.** Run `update_transaction_mass` after inputs and -outputs are settled but *before* signing or serializing. Mass is part -of the signed payload — sign first and you'll be signing over -`mass=0`. +!!! warning "Order matters" + Run [`update_transaction_mass`](../../reference/Functions/update_transaction_mass.md) after inputs and outputs are + settled but **before** signing or serializing. Mass is part of + the signed payload — sign first and you'll be signing over + `mass=0`. For multisig estimation, both calls take an optional `minimum_signatures` to size the signature script correctly: @@ -58,6 +58,8 @@ update_transaction_mass("mainnet", tx, minimum_signatures=2) ## Storage mass on its own +[`calculate_storage_mass`](../../reference/Functions/calculate_storage_mass.md) computes only the storage component: + ```python from kaspa import calculate_storage_mass @@ -77,17 +79,19 @@ storage mass through the roof, fold it into the fee instead. from kaspa import calculate_transaction_fee fee = calculate_transaction_fee("mainnet", tx) -print(fee) # required fee in sompi +print(fee) # required fee in sompi, or None if mass exceeds the standard limit ``` -`calculate_transaction_fee` returns the minimum required fee at the -network's current rate. The result is a sompi int, or `None` if the -calculation can't be performed (typically a malformed transaction). +[`calculate_transaction_fee`](../../reference/Functions/calculate_transaction_fee.md) returns the minimum required fee at the +network's current rate, or `None` if the transaction's mass exceeds +[`maximum_standard_transaction_mass()`](../../reference/Functions/maximum_standard_transaction_mass.md). Split the inputs across +multiple transactions in that case. ## Querying the fee rate The network exposes a fee estimator over RPC — see -[RPC → Calls → Fees](../rpc/calls.md#fees): +[RPC → Calls → Fees](../rpc/calls.md#fees) and +[`get_fee_estimate`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.get_fee_estimate): ```python estimate = await client.get_fee_estimate({}) @@ -101,28 +105,18 @@ Each bucket carries a `feerate` (sompi-per-gram-of-mass) and an bucket by how much you care about latency, multiply by mass, and you have a fee. -The Wallet wraps this as -[`fee_rate_estimate()`](../wallet/send-transaction.md#fees) and the -Generator picks a sensible default if you don't pass `fee_rate=` +The [`Wallet`](../../reference/Classes/Wallet.md) wraps this as +[`fee_rate_estimate()`](../../reference/Classes/Wallet.md#kaspa.Wallet.fee_rate_estimate) (see [the fee model](../wallet/send-transaction.md#fee-model)) and +the [`Generator`](../../reference/Classes/Generator.md) picks a sensible default if you don't pass `fee_rate=` explicitly. ## When to set fees explicitly -The [Generator](../wallet-sdk/tx-generator.md) picks a fee rate, +The [`Generator`](../../reference/Classes/Generator.md) (see [Transaction Generator](../wallet-sdk/tx-generator.md)) picks a fee rate, computes mass, and folds the leftover into change. Override only: - **`fee_rate=`** — when you have a specific sompi-per-gram in mind. -- **`priority_fee=`** — to add a flat surcharge on top of the computed - fee. -- **Manual path** — when building the transaction yourself and sizing - change outputs around the fee. +- **`priority_fee=`** — to add a flat surcharge on top. +- **Manual path** — when sizing change around the fee yourself. For typical sends, the defaults are fine. - -## Where to next - -- [Signing](signing.md) — runs after mass, before submission. -- [Submission](submission.md) — `submit_transaction` and what counts - as confirmed. -- [Kaspa Concepts → Mass and the fee market](../concepts.md#mass-and-the-fee-market) - — protocol background on why mass is shaped this way. diff --git a/docs/learn/transactions/metadata.md b/docs/learn/transactions/metadata.md index 8aa60093..b484722d 100644 --- a/docs/learn/transactions/metadata.md +++ b/docs/learn/transactions/metadata.md @@ -1,10 +1,10 @@ # Metadata fields Beyond inputs and outputs, a -[`Transaction`](../../reference/Classes/Transaction.md) carries five -fields that affect how it's interpreted on-chain. Typical sends take -the defaults — this page documents what they are so you know what to -leave alone and what to set deliberately. +[`Transaction`](../../reference/Classes/Transaction.md) carries six +fields that affect how it's interpreted on-chain. Most take defaults +— this page documents what they are so you know what to leave alone +and what to set deliberately. ```python Transaction( @@ -22,52 +22,41 @@ Transaction( ## `version` Transaction format version. Use `0` — the only currently-defined -version on Kaspa. The field exists for future formats; until then, -there's nothing to choose. +version on Kaspa. ## `lock_time` -The earliest moment a transaction is allowed into a block, encoded as -a DAA-score threshold. `0` means "no lock" — what you want unless +Earliest moment a transaction is allowed into a block, encoded as a +DAA-score threshold. `0` means "no lock" — what you want unless building a time-locked construct (e.g. a refund branch). -```python -Transaction(..., lock_time=0, ...) -``` - -A non-zero value rejects the transaction from blocks whose DAA score -is below the threshold. See -[Kaspa Concepts → Virtual chain and DAA score](../concepts.md#virtual-chain-and-daa-score). - ## `subnetwork_id` The subnetwork the transaction belongs to. Most transactions live on -the default subnetwork — id all zeros — and that's what you pass when -building manually: +the default subnetwork — id all zeros: ```python subnetwork_id="0000000000000000000000000000000000000000" ``` -Non-default subnetwork IDs are reserved for protocol-level transaction -kinds (coinbase, etc.) that you generally don't construct from the -SDK. +Non-default IDs are reserved for protocol-level transaction kinds +(coinbase, etc.) that you generally don't construct from the SDK. ## `gas` -Reserved for subnetwork transactions with a compute-cost component. -On the default subnetwork it must be `0`. Pair with -`subnetwork_id="00...0"` and forget about it. +`0` on the default subnetwork. Reserved for subnetwork transactions +with a compute-cost component. ## `payload` Arbitrary bytes attached to the transaction. The closest analog in Bitcoin terms is `OP_RETURN`-style data, but `payload` lives at the -transaction level, not inside a script. +transaction level, not inside a script. The SDK accepts a hex +string, raw bytes, or a list of byte values: ```python Transaction(..., payload="68656c6c6f", ...) # hex string -Transaction(..., payload=b"hello", ...) # or raw bytes +Transaction(..., payload=b"hello", ...) # raw bytes ``` Use cases: @@ -77,11 +66,11 @@ Use cases: - **Protocol-level data** for systems built on top of Kaspa transactions. -It's not a substitute for cryptographic state. Payload bytes are -hashed into the transaction ID and signed over, but they don't bind -the transaction to anything off-chain on their own. +Payload bytes are hashed into the transaction ID and signed over, +but they don't bind the transaction to anything off-chain on their +own. -The Generator accepts `payload=` directly: +The [`Generator`](../../reference/Classes/Generator.md) (see [Transaction Generator](../wallet-sdk/tx-generator.md)) accepts `payload=` directly: ```python Generator(..., payload=b"invoice-12345") @@ -90,15 +79,6 @@ Generator(..., payload=b"invoice-12345") ## `mass` The transaction's mass. `0` at construction; populate with -`update_transaction_mass(network_id, tx)` after inputs and outputs -are finalized and before signing or serializing. See -[Mass & fees](mass-and-fees.md). - -## Where to next - -- [Mass & fees](mass-and-fees.md) — the one metadata field you *must* - update. -- [Serialization](serialization.md) — how these fields round-trip - through `to_dict()` / `from_dict()`. -- [Kaspa Concepts](../concepts.md) — subnetworks, DAA score, virtual - chain. +[`update_transaction_mass(network_id, tx)`](../../reference/Functions/update_transaction_mass.md) after inputs and outputs +are finalized and **before** signing or serializing — mass is part +of the signed payload. See [Mass & fees](mass-and-fees.md). diff --git a/docs/learn/transactions/outputs.md b/docs/learn/transactions/outputs.md index 6b2eb30f..7f47c8c5 100644 --- a/docs/learn/transactions/outputs.md +++ b/docs/learn/transactions/outputs.md @@ -37,8 +37,9 @@ out = TransactionOutput( ) ``` -For the inverse — recovering the address that an output pays to — use -`address_from_script_public_key`: +For the inverse — recovering the address an output pays to — use +[`address_from_script_public_key`](../../reference/Functions/address_from_script_public_key.md). It needs a network argument because +the script doesn't carry the prefix: ```python from kaspa import NetworkType, address_from_script_public_key @@ -46,16 +47,13 @@ from kaspa import NetworkType, address_from_script_public_key addr = address_from_script_public_key(out.script_public_key, NetworkType.Mainnet) ``` -That call needs a network argument because the script doesn't carry -the prefix — you have to tell the decoder which network you're -displaying for. - ## Pay-to-script-hash -For multisig and other custom scripts, lockups go through a script -hash. `pay_to_script_hash_script(redeem_script)` produces the locking -side; the spender later supplies the redeem script plus signatures -via `pay_to_script_hash_signature_script(...)` at sign time. +For multisig and other custom scripts, the locking side uses a script +hash. See +[`examples/transactions/multisig.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/transactions/multisig.py) +for the full P2SH flow (address creation, multi-cosigner signing, +submission). Build the lockup with [`pay_to_script_hash_script`](../../reference/Functions/pay_to_script_hash_script.md): ```python from kaspa import pay_to_script_hash_script @@ -64,13 +62,9 @@ spk = pay_to_script_hash_script(redeem_script_bytes) out = TransactionOutput(value=amount, script_public_key=spk) ``` -For the multisig flow (address creation, multi-cosigner signing, -submission), see the -[Multi-signature transactions](../../guides/multisig.md) recipe. - ## Change outputs -When selected inputs sum to more than `amount + fee`, the leftover +When selected inputs sum to more than `outputs + fee`, the leftover goes to a change output you control: ```python @@ -80,19 +74,19 @@ outputs = [ ] ``` -The [Generator](../wallet-sdk/tx-generator.md) computes -`change_amount` for you (selected total − outputs − fee) and writes -change last. When building manually, do the arithmetic yourself — -including re-checking after `update_transaction_mass` if the fee -shifted. +The [`Generator`](../../reference/Classes/Generator.md) (see [Transaction Generator](../wallet-sdk/tx-generator.md)) computes +`change_amount` for you (`selected_total − outputs − fee`) and writes +change last. Manually, do the arithmetic yourself — and re-check +after [`update_transaction_mass`](../../reference/Functions/update_transaction_mass.md) if the fee shifted. -If `change_amount` is too small to be worth a separate output, fold -it into the fee — paying a slightly inflated fee beats dust. +If a tiny change output would inflate storage mass more than it's +worth, fold it into the fee instead. See +[Mass & fees](mass-and-fees.md#storage-mass-on-its-own) for sizing. ## Sompi vs KAS -Every value in the output surface (and everywhere else in the -transaction API) is a **sompi int**. `1 KAS = 100_000_000 sompi`. +Every value in the transaction API is a **sompi int**. +`1 KAS = 100_000_000 sompi`. Convert with [`kaspa_to_sompi`](../../reference/Functions/kaspa_to_sompi.md) and [`sompi_to_kaspa`](../../reference/Functions/sompi_to_kaspa.md): ```python from kaspa import kaspa_to_sompi, sompi_to_kaspa @@ -101,8 +95,7 @@ kaspa_to_sompi(1.5) # 150_000_000 sompi_to_kaspa(150_000_000) # 1.5 ``` -Convert only at the UI boundary. Don't store KAS as a float -internally — everything in the SDK assumes integer sompi. +Convert only at the UI boundary — don't store KAS as a float. ## Reading outputs back @@ -112,11 +105,3 @@ for out in tx.outputs: print(out.script_public_key.version) print(out.script_public_key.script) # hex ``` - -## Where to next - -- [Addresses](../addresses.md) — what a `pay_to_address_script` is - actually pointing at. -- [Inputs](inputs.md) — the other half of a transaction. -- [Mass & fees](mass-and-fees.md) — output values feed into the - storage-mass component of the fee. diff --git a/docs/learn/transactions/overview.md b/docs/learn/transactions/overview.md index 53eaa8f4..2dcb1128 100644 --- a/docs/learn/transactions/overview.md +++ b/docs/learn/transactions/overview.md @@ -10,12 +10,12 @@ a few metadata fields. The SDK exposes the underlying types — [`UtxoEntryReference`](../../reference/Classes/UtxoEntryReference.md) — and helpers that build, sign, mass, and serialise them. -Most of the time you'll use the higher-level -[Transaction Generator](../wallet-sdk/tx-generator.md) (or the -managed [Wallet](../wallet/send-transaction.md) on top of it). This -section covers the primitives underneath, so you can build manually -when you need custom lockup scripts, exact input ordering, payload -data, or offline signing. +!!! tip "Most callers don't need this section" + The [Transaction Generator](../wallet-sdk/tx-generator.md) (and + the managed [`Wallet`](../../reference/Classes/Wallet.md) (see [Wallet](../wallet/send-transaction.md)) on top of it) + handles UTXO selection, mass, signing, and submission for you. + Reach for the primitives below only when you need custom lockup + scripts, exact input ordering, payload data, or offline signing. ## Anatomy @@ -39,7 +39,7 @@ TransactionOutput What sets Kaspa apart from a Bitcoin-shaped chain: -- **Inputs carry their own UTXO context** via `UtxoEntryReference`, so +- **Inputs carry their own UTXO context** via [`UtxoEntryReference`](../../reference/Classes/UtxoEntryReference.md), so the signer doesn't have to re-fetch the spent output for its amount and lockup. See [Inputs](inputs.md). - **Mass replaces "byte size × rate"** as the fee model. Compute mass @@ -47,11 +47,18 @@ What sets Kaspa apart from a Bitcoin-shaped chain: input and output values), then multiply by the prevailing fee rate. See [Mass & fees](mass-and-fees.md). - **The atomic unit is the sompi**: `1 KAS = 100_000_000 sompi`. - Every amount in the transaction surface is a sompi int — convert - only at the UI boundary. + Every amount in the transaction surface is a sompi int. See + [`kaspa_to_sompi`](../../reference/Functions/kaspa_to_sompi.md) and + [`sompi_to_kaspa`](../../reference/Functions/sompi_to_kaspa.md). ## End-to-end (manual path) +This walks the manual flow using +[`sign_transaction`](../../reference/Functions/sign_transaction.md), +[`pay_to_address_script`](../../reference/Functions/pay_to_address_script.md), +and +[`update_transaction_mass`](../../reference/Functions/update_transaction_mass.md): + ```python from kaspa import ( Transaction, TransactionInput, TransactionOutput, TransactionOutpoint, @@ -59,6 +66,9 @@ from kaspa import ( update_transaction_mass, ) +resp = await client.get_utxos_by_addresses({"addresses": [my_address]}) +my_utxos = resp["entries"] + inputs = [ TransactionInput( previous_outpoint=TransactionOutpoint( @@ -85,44 +95,15 @@ tx = Transaction( gas=0, payload="", mass=0, ) -update_transaction_mass("mainnet", tx) +update_transaction_mass("mainnet", tx) # mass is signed over — fill before signing signed = sign_transaction(tx, [private_key], verify_sig=True) await client.submit_transaction({ - "transaction": signed.serialize_to_dict(), + "transaction": signed, "allowOrphan": False, }) ``` -This is what the [Generator](../wallet-sdk/tx-generator.md) does +This is what the [`Generator`](../../reference/Classes/Generator.md) (see [Transaction Generator](../wallet-sdk/tx-generator.md)) does internally — it picks UTXOs, computes mass, signs, and yields one or -more ready-to-submit `PendingTransaction`s. Reach for the manual path -when you need control the Generator doesn't expose. - -## In this section - -- **[Inputs](inputs.md)** — `TransactionInput`, - `TransactionOutpoint`, `UtxoEntryReference`, and why inputs carry - their UTXO context. -- **[Outputs](outputs.md)** — `TransactionOutput`, `ScriptPublicKey`, - the lockup scripts that pay an address. -- **[Mass & fees](mass-and-fees.md)** — computing mass, storage mass, - the fee market, and when to call `update_transaction_mass`. -- **[Signing](signing.md)** — `sign_transaction`, `SighashType`, - per-input signing, multi-key flows. -- **[Submission](submission.md)** — `submit_transaction`, what - "submitted" means, and how confirmation works. -- **[Metadata fields](metadata.md)** — `version`, `lock_time`, - `subnetwork_id`, `gas`, `payload` — the fields you mostly leave - alone. -- **[Serialization](serialization.md)** — `to_dict()` / `from_dict()` - for round-tripping through other systems. - -## Where to next - -- [Wallet SDK → Transaction Generator](../wallet-sdk/tx-generator.md) - — the high-level coin selector + signer. -- [Wallet → Send Transaction](../wallet/send-transaction.md) — the - managed Wallet's send surface. -- [Kaspa Concepts](../concepts.md) — UTXO model, mass, fees, - maturity. +more ready-to-submit [`PendingTransaction`](../../reference/Classes/PendingTransaction.md)s. diff --git a/docs/learn/transactions/scripts.md b/docs/learn/transactions/scripts.md new file mode 100644 index 00000000..dc397c5b --- /dev/null +++ b/docs/learn/transactions/scripts.md @@ -0,0 +1,210 @@ +# Scripts + +A [`ScriptPublicKey`](../../reference/Classes/ScriptPublicKey.md) is the lock on every output. Most of the time it's +generated for you — [`pay_to_address_script`](../../reference/Functions/pay_to_address_script.md) builds the standard +pay-to-pubkey lock, and both the +[`Generator`](../../reference/Classes/Generator.md) (see [Transaction Generator](../wallet-sdk/tx-generator.md)) and the high-level +[`Wallet`](../../reference/Classes/Wallet.md) (see [Wallet](../wallet/overview.md)) API use it under the hood. This page +is for the cases where you need a non-standard lock: multisig redeem +scripts, KRC‑20 / inscription envelopes, time-locked spends, covenant +prototypes. + +The fluent builder is +[`ScriptBuilder`](../../reference/Classes/ScriptBuilder.md); the +opcode set lives in +[`Opcodes`](../../reference/Enums/Opcodes.md). + +## When you'd reach for this + +- **Multisig.** Building the redeem script behind an `M`-of-`N` + pay-to-script-hash address. Worked example: + [`examples/transactions/multisig.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/transactions/multisig.py). +- **KRC‑20 / inscription envelopes.** Embedding token-protocol JSON + in a commit-reveal script. Worked example: + [`examples/transactions/krc20_deploy.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/transactions/krc20_deploy.py). +- **Custom locking conditions.** Time-locked or hash-locked spends + and covenant prototypes. Advanced — you should already know what + opcodes you need. + +## Building a script + +[`ScriptBuilder`](../../reference/Classes/ScriptBuilder.md) is a chained-builder: every method returns the +builder so you can compose a script in one expression. + +```python +from kaspa import Opcodes, ScriptBuilder + +script = ScriptBuilder()\ + .add_data(pubkey_xonly_hex)\ + .add_op(Opcodes.OpCheckSig) + +print(script.to_string()) # hex of the assembled script +``` + +The four input families: + +- **[`add_op(op)`](../../reference/Classes/ScriptBuilder.md) / [`add_ops([op, ...])`](../../reference/Classes/ScriptBuilder.md)** — push a single opcode or a + sequence. `op` is an [`Opcodes`](../../reference/Enums/Opcodes.md) enum member or its integer value. +- **[`add_data(bytes_or_hex)`](../../reference/Classes/ScriptBuilder.md)** — push raw bytes (signatures, public + keys, payloads). The builder picks the right `OP_PUSHDATA*` variant + for the size automatically. Accepts a hex string, `bytes`, or a + list of ints. +- **[`add_i64(n)`](../../reference/Classes/ScriptBuilder.md)** — push a signed integer. Used for `M` and `N` in + multisig, and for sequence / lock-time scalars. +- **[`add_lock_time(daa_score)`](../../reference/Classes/ScriptBuilder.md) / [`add_sequence(seq)`](../../reference/Classes/ScriptBuilder.md)** — push + DAA-score and sequence values, for time-locked spends. + +For the full opcode catalog see the +[`Opcodes`](../../reference/Enums/Opcodes.md) reference. + +## Wrapping in P2SH + +The output side commits to the *hash* of the redeem script, not the +script itself. [`create_pay_to_script_hash_script`](../../reference/Classes/ScriptBuilder.md) produces the +locking +[`ScriptPublicKey`](../../reference/Classes/ScriptPublicKey.md); +[`pay_to_script_hash_script`](../../reference/Functions/pay_to_script_hash_script.md) +is the equivalent free function when you have the redeem-script bytes +directly: + +```python +from kaspa import address_from_script_public_key + +spk = script.create_pay_to_script_hash_script() +p2sh_address = address_from_script_public_key(spk, "testnet-10") +``` + +Place `spk` on a [`TransactionOutput`](../../reference/Classes/TransactionOutput.md), or share `p2sh_address` so +funds can be sent to it. See [`address_from_script_public_key`](../../reference/Functions/address_from_script_public_key.md) for the network argument. + +When *spending* a P2SH output, the input's `signature_script` must +reveal the redeem script *and* a satisfying signature. For the +single-signature case, the convenience helper writes the witness for +you ([`create_input_signature`](../../reference/Classes/PendingTransaction.md), [`encode_pay_to_script_hash_signature_script`](../../reference/Classes/ScriptBuilder.md), [`fill_input`](../../reference/Classes/PendingTransaction.md)): + +```python +sig = pending.create_input_signature(input_index=0, private_key=key) +witness = script.encode_pay_to_script_hash_signature_script(sig) +pending.fill_input(0, witness) +``` + +[`pay_to_script_hash_signature_script(redeem_script, signature)`](../../reference/Functions/pay_to_script_hash_signature_script.md) +is the equivalent functional form when you no longer have the original +builder in scope. For multisig — where the witness needs more than +one signature — you build the `signature_script` manually with +[`ScriptBuilder`](../../reference/Classes/ScriptBuilder.md); see the example linked below. + +## Two real shapes + +### Multisig redeem (M-of-N) + +[`create_multisig_address`](../../reference/Functions/create_multisig_address.md) produces the same lockup as a one-shot helper; this section +shows what's happening underneath: + +```python +redeem = ScriptBuilder()\ + .add_i64(2)\ + .add_data(pub_a.to_x_only_public_key().to_string())\ + .add_data(pub_b.to_x_only_public_key().to_string())\ + .add_data(pub_c.to_x_only_public_key().to_string())\ + .add_i64(3)\ + .add_op(Opcodes.OpCheckMultiSig) + +spk = redeem.create_pay_to_script_hash_script() +``` + +This is a 2-of-3 Schnorr multisig: integer `M`, the public keys +([`XOnlyPublicKey`](../../reference/Classes/XOnlyPublicKey.md) for Schnorr), integer `N`, then [`Opcodes.OpCheckMultiSig`](../../reference/Enums/Opcodes.md). ECDSA +multisig uses [`Opcodes.OpCheckMultiSigECDSA`](../../reference/Enums/Opcodes.md) and full (compressed) [`PublicKey`](../../reference/Classes/PublicKey.md)s +instead. The mass calculator needs to know about the multiple +signatures the input will eventually hold — pass `sig_op_count=N` per +input and `minimum_signatures=M` to the +[`Generator`](../../reference/Classes/Generator.md) (see [Transaction Generator](../wallet-sdk/tx-generator.md)). See +[Signing → Multisig](signing.md#multisig-and-sig_op_count) for how +those fields feed mass. + +The spending side (collecting `M` signatures and packing them into +each input's `signature_script`) is non-trivial. The full flow is in +[`examples/transactions/multisig.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/transactions/multisig.py). + +### KRC‑20 / inscription envelope + +```python +import json + +script = ScriptBuilder()\ + .add_data(pub.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(payload, separators=(",", ":")).encode())\ + .add_op(Opcodes.OpEndIf) +``` + +The `OpFalse` / `OpIf` block is unreachable execution — a +conventional way to embed protocol data without affecting whether +the script can be satisfied. The reveal-stage input later spends a +P2SH output committing to this script, putting the embedded payload +on-chain. + +The two-stage commit/reveal flow itself (fund the P2SH address in a +commit transaction, then spend it back to yourself in a reveal +transaction once the commit is mature) lives in +[`examples/transactions/krc20_deploy.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/transactions/krc20_deploy.py). +The managed [`Wallet`](../../reference/Classes/Wallet.md) wraps this as +[`accounts_commit_reveal`](../../reference/Classes/Wallet.md) / +[`accounts_commit_reveal_manual`](../../reference/Classes/Wallet.md), keyed by a +[`CommitRevealAddressKind`](../../reference/Enums/CommitRevealAddressKind.md). + +## Inspecting an unknown script + +When you hold a [`ScriptPublicKey`](../../reference/Classes/ScriptPublicKey.md) from somewhere else — a UTXO +returned by [`get_utxos_by_addresses`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.get_utxos_by_addresses), an output read off-chain — the +classification predicates +([`is_script_pay_to_pubkey`](../../reference/Functions/is_script_pay_to_pubkey.md), +[`is_script_pay_to_pubkey_ecdsa`](../../reference/Functions/is_script_pay_to_pubkey_ecdsa.md), +[`is_script_pay_to_script_hash`](../../reference/Functions/is_script_pay_to_script_hash.md)) +tell you which lockup family it is: + +```python +from kaspa import ( + is_script_pay_to_pubkey, + is_script_pay_to_pubkey_ecdsa, + is_script_pay_to_script_hash, +) + +script_bytes = utxo.script_public_key.script +if is_script_pay_to_pubkey(script_bytes): + ... # Schnorr P2PK +elif is_script_pay_to_pubkey_ecdsa(script_bytes): + ... # ECDSA P2PK +elif is_script_pay_to_script_hash(script_bytes): + ... # P2SH — needs the redeem script to spend +``` + +Use them to pick the signing path, filter UTXOs the wallet can +actually spend, or audit a transaction's outputs. + +## SighashType and advanced flows + +Scripts and sighash variants interact when you write protocols that +intentionally let signed transactions be amended. +[`SighashType.All`](../../reference/Enums/SighashType.md) — the +default — commits to every input and every output and is the only +one ordinary scripts should use. The `_None`, `Single`, and +`*AnyOneCanPay` variants exist for coinjoins and partial co-signing +flows; don't reach for them without a spec to follow. See +[Signing → SighashType](signing.md#sighashtype). + +## What this page didn't cover + +- The full multisig orchestration (deriving cosigner keys, exchanging + partial signatures, submission): + [`examples/transactions/multisig.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/transactions/multisig.py). +- Commit-reveal mechanics (two-stage submission, fee budget, the + maturity gate between commit and reveal): + [`examples/transactions/krc20_deploy.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/transactions/krc20_deploy.py). +- The full opcode catalog: + [`Opcodes`](../../reference/Enums/Opcodes.md) reference. diff --git a/docs/learn/transactions/serialization.md b/docs/learn/transactions/serialization.md index b1b4d3e2..752c8f54 100644 --- a/docs/learn/transactions/serialization.md +++ b/docs/learn/transactions/serialization.md @@ -1,6 +1,6 @@ # Serialization -Most transaction-shaped types — +The transaction-shaped types — [`Transaction`](../../reference/Classes/Transaction.md), [`TransactionInput`](../../reference/Classes/TransactionInput.md), [`TransactionOutput`](../../reference/Classes/TransactionOutput.md), @@ -17,31 +17,34 @@ restored = Transaction.from_dict(tx_dict) assert restored == tx ``` -The same shape works for the component types: +[`to_dict()`](../../reference/Classes/Transaction.md) returns a fresh Python dict — modifying it doesn't +mutate the source object. [`from_dict()`](../../reference/Classes/Transaction.md) raises on malformed input +(missing required keys, wrong types, invalid values). -```python -inp_dict = inputs[0].to_dict() -restored_inp = TransactionInput.from_dict(inp_dict) - -out_dict = outputs[0].to_dict() -restored_out = TransactionOutput.from_dict(out_dict) +## What the dict looks like -ref_dict = utxo_ref.to_dict() -restored_ref = UtxoEntryReference.from_dict(ref_dict) +```python +{ + "id": "ab12...", # transaction id, hex + "version": 0, + "inputs": [{ "previousOutpoint": {...}, "signatureScript": "...", "sequence": 0, "sigOpCount": 1 }, ...], + "outputs": [{ "value": 500000000, "scriptPublicKey": {"version": 0, "script": "..."} }, ...], + "lockTime": 0, + "subnetworkId": "0000000000000000000000000000000000000000", + "gas": 0, + "payload": "", # hex string + "mass": 12345, +} ``` -`to_dict()` returns a fresh Python dict — modifying it doesn't mutate -the source object. `from_dict()` raises on malformed input (missing -required keys, wrong types, invalid values). +Field names use camelCase (wRPC convention), unlike the snake_case +Python class attributes. ## When you need this Within a single process, you rarely need to round-trip — pass typed objects around. The dict form earns its place at process boundaries: -- **Submission** — `client.submit_transaction({"transaction": - tx.serialize_to_dict(), ...})` takes a dict, not a `Transaction`. - See [Submission](submission.md). - **Offline signing** — build on an online machine, serialize, sign on an air-gapped one, serialize again, send back, submit. The dict is the natural transport. @@ -50,18 +53,7 @@ objects around. The dict form earns its place at process boundaries: - **Persistence** — saving a pending transaction to disk or a queue. Store the dict (as JSON), not the typed object. -## `serialize_to_dict` vs `to_dict` - -Both produce a dict matching the wRPC wire shape. `to_dict` is the -general-purpose Python conversion; `serialize_to_dict` (on -`Transaction`) is the form `submit_transaction` expects. In practice -they produce equivalent shapes — use `serialize_to_dict` when about -to submit, `to_dict` when shuttling the object somewhere else. - -## Where to next - -- [Submission](submission.md) — where the dict form actually goes. -- [Inputs](inputs.md) and [Outputs](outputs.md) — the typed objects - these dicts represent. -- [Metadata fields](metadata.md) — how transaction-level fields ride - through serialization. +For submission itself you can pass either a [`Transaction`](../../reference/Classes/Transaction.md) or a dict +to [`client.submit_transaction({"transaction": ...})`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.submit_transaction); the dict form +is only required when the transaction has already been serialized +elsewhere. See [Submission](submission.md). diff --git a/docs/learn/transactions/signing.md b/docs/learn/transactions/signing.md index d772e94b..6fb24f7b 100644 --- a/docs/learn/transactions/signing.md +++ b/docs/learn/transactions/signing.md @@ -11,10 +11,12 @@ For Schnorr vs ECDSA on the addressing side, see ## Sign a manually built transaction +Run [`update_transaction_mass`](../../reference/Functions/update_transaction_mass.md) first, then [`sign_transaction`](../../reference/Functions/sign_transaction.md): + ```python from kaspa import sign_transaction, update_transaction_mass -update_transaction_mass("mainnet", tx) # do this first — mass is signed over +update_transaction_mass("mainnet", tx) # mass is signed over — fill first signed = sign_transaction(tx, [private_key], verify_sig=True) ``` @@ -24,16 +26,18 @@ signed = sign_transaction(tx, [private_key], verify_sig=True) [`PrivateKey`](../../reference/Classes/PrivateKey.md). The signer for each input is inferred from the input's UTXO lockup; pass every key any input needs. -- `verify_sig=True` — verify each signature after writing it. Cheap - insurance during development; disable in performance-sensitive - paths once you trust the inputs. +- `verify_sig=True` — verify each signature after writing it. Raises + on mismatch (a corrupt input or wrong key). Cheap insurance during + development; disable in performance-sensitive paths once you trust + the inputs. -Sign after mass is filled in, before submission. Mass is part of the -signed payload, so changing inputs, outputs, or mass *after* signing -invalidates the signature. +Changing inputs, outputs, or mass *after* signing invalidates the +signature — sign last. ## Sign a generator-produced PendingTransaction +A [`PendingTransaction`](../../reference/Classes/PendingTransaction.md) yielded by [`Generator`](../../reference/Classes/Generator.md) (see [Transaction Generator](../wallet-sdk/tx-generator.md)) signs in one call: + ```python for pending in gen: pending.sign([key]) # all inputs at once @@ -49,11 +53,11 @@ pending.sign([key1, key2, key3]) For per-input control: ```python -for i, _ in enumerate(pending.get_utxo_entries()): +for i in range(len(pending.get_utxo_entries())): pending.sign_input(i, key_for(i)) ``` -`sign_input` is the right surface when different inputs need different +[`sign_input`](../../reference/Classes/PendingTransaction.md) is the right surface when different inputs need different signers (mixed-key wallets, partially-co-signed flows). ## SighashType @@ -61,31 +65,18 @@ signers (mixed-key wallets, partially-co-signed flows). The hash that gets signed describes *which parts of the transaction* the signature commits to. [`SighashType.All`](../../reference/Enums/SighashType.md) is the -default and the only one most code should use. +default and the only one most code should use — it signs every input +and every output. -```python -from kaspa import SighashType - -print(list(SighashType)) -# All, _None, Single, AllAnyOneCanPay, NoneAnyOneCanPay, SingleAnyOneCanPay -``` - -- **`All`** — signs every input and every output. Standard. -- **`_None`** — signs inputs only; outputs can be modified. Rare; - underscore-prefixed because `None` is a Python keyword. -- **`Single`** — signs the input being spent and the matching output - by index. -- **`*AnyOneCanPay`** — variants that *don't* sign the other inputs, - letting cosigners add inputs after the fact. - -Leave it at `All` unless you have a specific protocol or co-signing -reason. The non-`All` modes are for advanced flows like collaborative -coin joins. +The other variants (`_None`, `Single`, and the `*AnyOneCanPay` +flavors) exist for advanced collaborative flows like coinjoins, +where cosigners add inputs or outputs after one party signs. If +you don't have a specific protocol reason, leave it at `All`. ## Build a signature without filling the input When you need raw signature bytes — e.g. to send to a co-signer for -aggregation — use `create_input_signature`: +aggregation — use [`create_input_signature`](../../reference/Functions/create_input_signature.md): ```python from kaspa import SighashType, create_input_signature @@ -98,9 +89,9 @@ sig_hex = create_input_signature( ) ``` -The same method exists on `PendingTransaction` -(`pending.create_input_signature(...)`); write the resulting script -back with `pending.fill_input(...)`. +The same method exists on [`PendingTransaction`](../../reference/Classes/PendingTransaction.md) +([`pending.create_input_signature(...)`](../../reference/Classes/PendingTransaction.md)); write the resulting script +back with [`pending.fill_input(...)`](../../reference/Classes/PendingTransaction.md). ## Multisig and sig_op_count @@ -110,21 +101,14 @@ Two fields interact with mass when you sign: input actually performs. `1` for a single-key spend, `M` for an `M`-of-`N` multisig. - **`minimum_signatures`** passed to - `update_transaction_mass(..., minimum_signatures=M)` and - `calculate_transaction_mass` — tells the mass calculator how big + [`update_transaction_mass(..., minimum_signatures=M)`](../../reference/Functions/update_transaction_mass.md) and + [`calculate_transaction_mass`](../../reference/Functions/calculate_transaction_mass.md) — tells the mass calculator how big the filled-in signature script will be. -Wrong values yield wrong mass and either rejected (too low) or wasted -(too high) fees. The Generator handles this when you pass -`sig_op_count` and `minimum_signatures` to the constructor. - -For the full multisig flow (address creation, multi-cosigner signing, -submission), see -[Multi-signature transactions](../../guides/multisig.md). - -## Where to next +Wrong values yield wrong mass — the transaction is rejected (mass +too low) or pays more fee than it needs (mass too high). The +[`Generator`](../../reference/Classes/Generator.md) handles this when you pass `sig_op_count` and +`minimum_signatures` to the constructor. -- [Submission](submission.md) — what to do with a signed transaction. -- [Mass & fees](mass-and-fees.md) — fill mass before signing, not after. -- [Multi-signature transactions](../../guides/multisig.md) — cosigner - flow end to end. +For the full multisig flow, see +[`examples/transactions/multisig.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/transactions/multisig.py). diff --git a/docs/learn/transactions/submission.md b/docs/learn/transactions/submission.md index 49afffd9..b8bc6287 100644 --- a/docs/learn/transactions/submission.md +++ b/docs/learn/transactions/submission.md @@ -2,9 +2,9 @@ Submitting a signed transaction hands it to a node, which gossips it to the network and includes it in a block when capacity allows. Two -surfaces: `pending.submit(client)` for transactions produced by the -[Generator](../wallet-sdk/tx-generator.md), and -[`client.submit_transaction(...)`](../rpc/calls.md#transactions-and-mempool) +surfaces: [`pending.submit(client)`](../../reference/Classes/PendingTransaction.md#kaspa.PendingTransaction.submit) for transactions produced by the +[`Generator`](../../reference/Classes/Generator.md) (see [Transaction Generator](../wallet-sdk/tx-generator.md)), and +[`client.submit_transaction(...)`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.submit_transaction) (see [RPC → Calls](../rpc/calls.md#transactions-and-mempool)) for everything else. ## From a PendingTransaction @@ -14,17 +14,16 @@ tx_id = await pending.submit(client) print(tx_id) ``` -`pending.submit` serializes the underlying `Transaction` and calls -`submit_transaction` for you. The right path for Generator-built -transactions — including the managed -[Wallet](../wallet/send-transaction.md), which is built on the -Generator. +[`pending.submit`](../../reference/Classes/PendingTransaction.md#kaspa.PendingTransaction.submit) calls [`submit_transaction`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.submit_transaction) for you. The right path +for [`Generator`](../../reference/Classes/Generator.md)-built transactions — including the managed +[`Wallet`](../../reference/Classes/Wallet.md) (see [Send Transaction](../wallet/send-transaction.md)), which is built on the +[`Generator`](../../reference/Classes/Generator.md). ## Manual submission ```python result = await client.submit_transaction({ - "transaction": signed_tx.serialize_to_dict(), + "transaction": signed_tx, "allowOrphan": False, }) print(result["transactionId"]) @@ -32,17 +31,13 @@ print(result["transactionId"]) The request takes: -- **`transaction`** — the wire-format dict from - `Transaction.serialize_to_dict()` (or - `pending.transaction.serialize_to_dict()`). +- **`transaction`** — a signed [`Transaction`](../../reference/Classes/Transaction.md) (or its dict form via + [`Transaction.to_dict()`](../../reference/Classes/Transaction.md) when shipping through another system; see + [Serialization](serialization.md)). - **`allowOrphan`** — whether to keep the transaction in the mempool when an input hasn't been seen yet (e.g. submitting a chain out of order). Default `False` unless you know you're submitting a chain. -Use the manual path when shipping the transaction through another -system — a co-signer, relay, or offline signer — before the node -sees it. See [Serialization](serialization.md) for round-tripping. - ## What "submitted" means Submission is *acceptance into the mempool*, not confirmation. The @@ -52,49 +47,37 @@ for inclusion in a block. A Kaspa transaction lifecycle has three observable states: - **In mempool** — accepted by the node, waiting for inclusion. ID - returned from `submit_transaction`. + returned from [`submit_transaction`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.submit_transaction). - **Virtual-chain accepted** — included in a block that's part of the canonical DAG ordering. Surfaces via the [`virtual-chain-changed`](../rpc/subscriptions.md#virtual-chain-progression) notification. - **Mature** — confirmed past the maturity threshold; new UTXOs are spendable. Surfaces via a `Maturity` event on the managed - [Wallet](../wallet/transaction-history.md) or the - [`UtxoProcessor`](../wallet-sdk/utxo-processor.md). + [`Wallet`](../../reference/Classes/Wallet.md) (see [Wallet → Events](../wallet/events.md)) or the + [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) (see [UTXO Processor](../wallet-sdk/utxo-processor.md)). The right gate for confirmation is the `Maturity` event for the specific transaction, not "wait N seconds" and not the first time it -appears in a block. See -[Kaspa Concepts → Maturity](../concepts.md#maturity) for the protocol -view. +appears in a block. ## Failures and retries -`submit_transaction` raises if the node rejects the transaction. +[`submit_transaction`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.submit_transaction) raises if the node rejects the transaction. Common reasons: -- **`fee too low`** — recompute mass with `update_transaction_mass` +- **`fee too low`** — recompute mass with [`update_transaction_mass`](../../reference/Functions/update_transaction_mass.md) *after* any input/output change, then re-sign. - **`orphan`** — an input references a transaction the node hasn't seen. Wait for the parent to land, or set `allowOrphan=True` when intentionally submitting a chain. - **`already in mempool`** — the same `transaction_id` is already - pending. Safe to ignore for retries. + pending. Safe to ignore. - **`mass exceeded`** — the transaction is over - `maximum_standard_transaction_mass()`. Split the inputs across - multiple transactions; the Generator does this automatically when - its input set is too large. + [`maximum_standard_transaction_mass()`](../../reference/Functions/maximum_standard_transaction_mass.md). Split the inputs across + multiple transactions; the [`Generator`](../../reference/Classes/Generator.md) does this automatically. A virtual-chain-accepted transaction *can* be reorged out — at which point its outputs are no longer mature. The SDK surfaces this as a `Reorg` event; see -[Wallet → Transaction History](../wallet/transaction-history.md). - -## Where to next - -- [Wallet → Send Transaction](../wallet/send-transaction.md) — the - managed Wallet's send surface, which wraps all of this. -- [Wallet → Transaction History](../wallet/transaction-history.md) — - observing maturity and reorgs. -- [Kaspa Concepts](../concepts.md) — virtual chain, DAA score, - maturity. +[Wallet → Events](../wallet/events.md). diff --git a/docs/learn/wallet-sdk/derivation.md b/docs/learn/wallet-sdk/derivation.md index 9d6b26ae..1298c8e0 100644 --- a/docs/learn/wallet-sdk/derivation.md +++ b/docs/learn/wallet-sdk/derivation.md @@ -8,69 +8,21 @@ key the wallet uses. Kaspa follows BIP-44 with coin type `111111`: m / 44' / 111111' / account' / chain / address_index ``` -`chain` is `0` for receive addresses, `1` for change. `account` and -`address_index` are unhardened relative to the account-level node. +A trailing `'` denotes a *hardened* level — derived from the parent's +private key, so the corresponding `xpub` cannot derive its children. +Non-hardened (no `'`) levels can be derived from the `xpub` alone, +which is what makes watch-only wallets possible. `chain` is `0` for +receive addresses, `1` for change. See the [Kaspa MDBook page on derivation](https://kaspa-mdbook.aspectron.com/wallets/addresses.html) -for the protocol-level details. - -## Extended keys - -```python -from kaspa import Mnemonic, XPrv - -seed = Mnemonic.random().to_seed() -xprv = XPrv(seed) - -print(xprv.xprv) # serialized xprv string -print(xprv.private_key) # the master secp256k1 secret -print(xprv.depth) # 0 for the master -print(xprv.chain_code) # 32 bytes -``` - -[`XPub`](../../reference/Classes/XPub.md) is the public counterpart — -useful for watch-only wallets: - -```python -xpub = xprv.to_xpub() -print(xpub.xpub) -print(xpub.to_public_key()) -``` - -## Deriving child keys directly - -```python -from kaspa import DerivationPath - -# By child number -child = xprv.derive_child(0) -hardened = xprv.derive_child(0, hardened=True) - -# By path string -account_xprv = xprv.derive_path("m/44'/111111'/0'") - -# By DerivationPath instance -path = DerivationPath("m/44'/111111'/0'/0/0") -leaf = xprv.derive_path(path) -``` - -[`DerivationPath`](../../reference/Classes/DerivationPath.md) is -mutable — handy for walking a chain incrementally: - -```python -path = DerivationPath("m/44'/111111'/0'") -path.push(0) # → m/44'/111111'/0'/0 -path.push(0) # → m/44'/111111'/0'/0/0 -print(path.to_string(), path.length(), path.is_empty()) -print(path.parent().to_string()) -``` +for protocol-level details. ## `PrivateKeyGenerator` -For everyday "give me address `i`" derivation, use -[`PrivateKeyGenerator`](../../reference/Classes/PrivateKeyGenerator.md) -— it handles the full BIP-44 path for you: +For "give me address `i`" derivation, use +[`PrivateKeyGenerator`](../../reference/Classes/PrivateKeyGenerator.md). +It walks the full BIP-44 path for you: ```python from kaspa import PrivateKeyGenerator, NetworkType @@ -89,29 +41,26 @@ for i in range(5): change = gen.change_key(0) # m/44'/111111'/0'/1/0 ``` -## `PublicKeyGenerator` (watch-only) +`xprv` accepts either an [`XPrv`](../../reference/Classes/XPrv.md) instance or its xprv string form; +[`NetworkType`](../../reference/Enums/NetworkType.md) selects the address prefix. + +## `PublicKeyGenerator` — watch-only -When you only need addresses — no signing — +When you only need addresses (no signing), [`PublicKeyGenerator`](../../reference/Classes/PublicKeyGenerator.md) -derives them from an `xpub` alone: +derives them from an [`xpub`](../../reference/Classes/XPub.md): ```python from kaspa import PublicKeyGenerator, NetworkType pub = PublicKeyGenerator.from_xpub("xpub...") -# A single address -addr = pub.receive_address(NetworkType.Mainnet, 0) - -# A range +addr = pub.receive_address(NetworkType.Mainnet, 0) addrs = pub.receive_addresses(NetworkType.Mainnet, start=0, end=10) - -# Public keys (not addresses) -pubkeys = pub.receive_pubkeys(start=0, end=5) +keys = pub.receive_pubkeys(start=0, end=5) ``` -If you have an `XPrv` but want a public-key-only generator (e.g. -watch-only mode in the same process): +A public-only generator can be built from an existing [`XPrv`](../../reference/Classes/XPrv.md): ```python pub = PublicKeyGenerator.from_master_xprv( @@ -121,43 +70,43 @@ pub = PublicKeyGenerator.from_master_xprv( ) ``` -`PublicKeyGenerator` exposes `change_addresses(...)` and the -`*_as_strings` variants that skip the `Address` wrapper. +`change_addresses(...)` and the `*_as_strings` variants are also +available. -## Multi-signature derivation +## Manual derivation -Each cosigner has their own `cosigner_index`: +For non-standard paths or one-off derivations, drop down to the +extended-key APIs directly. Most users won't need this — reach for +[`PrivateKeyGenerator`](../../reference/Classes/PrivateKeyGenerator.md) first. ```python -gen0 = PrivateKeyGenerator(xprv=our_xprv, is_multisig=True, - account_index=0, cosigner_index=0) -gen1 = PrivateKeyGenerator(xprv=their_xprv, is_multisig=True, - account_index=0, cosigner_index=1) -``` - -For the full multisig wallet flow (creating the multisig address, -spending from it), see the -[Multi-signature transactions](../../guides/multisig.md) recipe. +from kaspa import DerivationPath, Mnemonic, XPrv -## Account-kind tag +xprv = XPrv(Mnemonic.random().to_seed()) -[`AccountKind`](../../reference/Classes/AccountKind.md) is the -metadata type the wallet uses to track which derivation rules apply. -Construct one explicitly only when calling the wallet's -account-creation methods: +print(xprv.xprv, xprv.depth, xprv.chain_code) -```python -from kaspa import AccountKind +# Public counterpart — useful for watch-only wallets. +xpub = xprv.to_xpub() -bip32 = AccountKind("bip32") -print(bip32.to_string()) # "bip32" +# Direct derivation +child = xprv.derive_child(0) # non-hardened +hardened = xprv.derive_child(0, hardened=True) +account_xprv = xprv.derive_path("m/44'/111111'/0'") +leaf = xprv.derive_path(DerivationPath("m/44'/111111'/0'/0/0")) ``` +[`DerivationPath`](../../reference/Classes/DerivationPath.md) is +mutable — handy for walking a chain incrementally with `push`, +`parent`, `length`, etc. See +[`examples/derivation.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/derivation.py) +for runnable derivation snippets. + ## Where to next -- [Transaction Generator](tx-generator.md) — sign and submit transactions - with the keys you just derived. +- [UTXO Processor](utxo-processor.md) — set up the live event pipeline + for the addresses you derived. +- [Transaction Generator](tx-generator.md) — sign and submit using + these keys. - [Wallet → Accounts](../wallet/accounts.md) — the managed Wallet's - higher-level account API uses these primitives internally. -- [Custom derivation paths](../../guides/custom-derivation.md) — recipe - for non-standard paths. + account API uses these primitives internally. diff --git a/docs/learn/wallet-sdk/key-management.md b/docs/learn/wallet-sdk/key-management.md index 4acb31c1..bfe90fdc 100644 --- a/docs/learn/wallet-sdk/key-management.md +++ b/docs/learn/wallet-sdk/key-management.md @@ -4,14 +4,15 @@ Everything on this page is BIP-39 compatible. The SDK gives you [`Mnemonic`](../../reference/Classes/Mnemonic.md) for the human-readable phrase, the seed bytes it produces, and [`XPrv`](../../reference/Classes/XPrv.md) for the master extended -private key from that seed. From an `XPrv` you derive child keys — -that's the next page, [Derivation](derivation.md). +private key from that seed. From an [`XPrv`](../../reference/Classes/XPrv.md) you [derive](derivation.md) child keys. Read [Security](../../getting-started/security.md) before generating a real mnemonic. ## Generate a mnemonic +[`Mnemonic.random()`](../../reference/Classes/Mnemonic.md) generates a fresh phrase: + ```python from kaspa import Mnemonic @@ -20,10 +21,6 @@ m12 = Mnemonic.random(word_count=12) # 12 words print(m.phrase) ``` -24 words is the recommended default — more entropy, lower brute-force -risk. 12 words is supported for compatibility with tools that emit -them. - ## Restore from a mnemonic ```python @@ -37,8 +34,11 @@ else: raise ValueError("invalid mnemonic") ``` -`Mnemonic.validate(phrase)` checks word membership, length, and the -BIP-39 checksum. Returns a bool; never raises. +[`Mnemonic.validate(phrase)`](../../reference/Classes/Mnemonic.md) checks word membership, length, and the +BIP-39 checksum, and returns a bool. The [`Mnemonic(phrase)`](../../reference/Classes/Mnemonic.md) constructor +raises on the same conditions, so the explicit [`validate()`](../../reference/Classes/Mnemonic.md) call is +only useful when you want to surface a friendlier error than the +underlying exception. ## Validation @@ -49,6 +49,8 @@ ok = Mnemonic.validate(phrase) # English assumed ok_en = Mnemonic.validate(phrase, Language.English) # explicit ``` +The [`Language`](../../reference/Enums/Language.md) enum names the BIP-39 wordlist (English by default). + ## Convert to a seed ```python @@ -94,7 +96,7 @@ English is the default and what every Kaspa example uses. Other BIP-39 wordlists exist on the enum but are rarely used — if you don't know you need a non-English wordlist, use English. -## Hex private keys (`SecretKey`) +## Hex private keys ([`PrivateKey`](../../reference/Classes/PrivateKey.md)) For one-key accounts (a single secp256k1 secret, no derivation), skip the mnemonic entirely: @@ -106,10 +108,10 @@ key = PrivateKey("<64-char hex>") addr = key.to_address("testnet-10") ``` -The 64-char hex string is what -[Wallet → Keypair Accounts](../wallet/keypair.md) takes as the -`secret` input to -`prv_key_data_create(kind=PrvKeyDataVariantKind.SecretKey)`. +The wallet's keypair accounts use the same 64-char hex string. When +calling [`prv_key_data_create`](../../reference/Classes/Wallet.md#kaspa.Wallet.prv_key_data_create), pass it as `secret` with +[`kind=PrvKeyDataVariantKind.SecretKey`](../../reference/Enums/PrvKeyDataVariantKind.md) — see +[Wallet → Accounts → Keypair accounts](../wallet/accounts.md#keypair-accounts). ## Where to next diff --git a/docs/learn/wallet-sdk/overview.md b/docs/learn/wallet-sdk/overview.md index c8725bcf..d6303f44 100644 --- a/docs/learn/wallet-sdk/overview.md +++ b/docs/learn/wallet-sdk/overview.md @@ -1,36 +1,87 @@ # Wallet SDK -The **Wallet SDK** is the layer beneath the managed -[Wallet](../wallet/overview.md). Drop down here when you don't need -on-disk file storage, multi-account management, or the wallet's event -multiplexer — when you just want to derive a key, build a transaction, -or track UTXOs for a few addresses. +The **Wallet SDK** is the set of primitives the managed +[`Wallet`](../../reference/Classes/Wallet.md) (see [Wallet](../wallet/overview.md)) is built on — and the toolkit you +reach for whenever you don't want a managed wallet at all (custom +signers, indexers, watch-only flows, hot-path services that keep +everything in memory). + +This page is your section index. Every page below is independently +useful; read in order if you're new. ## What lives here | Page | What it covers | | --- | --- | -| [Key Management](key-management.md) | `Mnemonic`, BIP-39 seed, `XPrv`, hex import/export. | -| [Derivation](derivation.md) | `PrivateKeyGenerator`, `PublicKeyGenerator`, `DerivationPath`, BIP-44. | -| [Transaction Generator](tx-generator.md) | The `Generator` class — UTXO selection, fees, signing, submission. | -| [UTXO Context](utxo-context.md) | `UtxoContext`: per-address UTXO tracking. | -| [UTXO Processor](utxo-processor.md) | `UtxoProcessor`: the engine that drives `UtxoContext`s. | +| [Key Management](key-management.md) | [`Mnemonic`](../../reference/Classes/Mnemonic.md), BIP-39 seed, [`XPrv`](../../reference/Classes/XPrv.md), hex import/export. | +| [Derivation](derivation.md) | [`PrivateKeyGenerator`](../../reference/Classes/PrivateKeyGenerator.md), [`PublicKeyGenerator`](../../reference/Classes/PublicKeyGenerator.md), BIP-44 paths. | +| [UTXO Processor](utxo-processor.md) | The engine: opens a node listener and dispatches events to contexts. | +| [UTXO Context](utxo-context.md) | Per-address UTXO tracking on top of a processor. | +| [Transaction Generator](tx-generator.md) | UTXO selection, fees, signing, submission. | -## Wallet vs. Wallet SDK in one table +## End-to-end without a managed wallet -| You want to... | Use | -| --- | --- | -| Open a file, manage many accounts, track them long-term | [Wallet](../wallet/overview.md) | -| Sign one transaction in a script with a key you already have | Wallet SDK ([Transaction Generator](tx-generator.md)) | -| Derive an address from a mnemonic without persisting anything | Wallet SDK ([Key Management](key-management.md), [Derivation](derivation.md)) | -| Watch a fixed set of addresses for incoming UTXOs without a wallet file | Wallet SDK ([UTXO Processor](utxo-processor.md), [UTXO Context](utxo-context.md)) | +This is the canonical "primitives only" flow — a mnemonic, a derived +address, live UTXO tracking, and a signed-and-submitted send. No +`Wallet`, no on-disk file. + +```python +import asyncio +from kaspa import ( + Generator, Mnemonic, NetworkId, NetworkType, PaymentOutput, + PrivateKeyGenerator, Resolver, RpcClient, UtxoContext, UtxoProcessor, + XPrv, +) + +async def main(): + network = NetworkId("testnet-10") + + # 1. Key material + xprv = XPrv(Mnemonic("<24-word phrase>").to_seed()) + keys = PrivateKeyGenerator(xprv=xprv, is_multisig=False, account_index=0) + receive_key = keys.receive_key(0) + receive_addr = receive_key.to_address(NetworkType.Testnet) + + # 2. Connect and start a processor + client = RpcClient(resolver=Resolver(), network_id="testnet-10") + await client.connect() + try: + processor = UtxoProcessor(client, network) + await processor.start() + + # 3. Track the address — seeds the mature set and subscribes + context = UtxoContext(processor) + await context.track_addresses([receive_addr]) + + # 4. Build, sign, submit + gen = Generator( + network_id=network, + entries=context, # mature UTXOs flow from here + change_address=receive_addr, + outputs=[PaymentOutput(receive_addr, 100_000_000)], # 1 KAS + ) + for pending in gen: + pending.sign([receive_key]) + print("submitted:", await pending.submit(client)) + + await processor.stop() + finally: + await client.disconnect() + +asyncio.run(main()) +``` -The managed `Wallet` is built from these pieces — every primitive on -this page is what `Wallet` wraps internally. +For the same flow with the managed [`Wallet`](../../reference/Classes/Wallet.md) (mnemonic stored on disk, +account state persisted), see +[Wallet → Send Transaction](../wallet/send-transaction.md). ## Where to next -If you're new here, start with [Key Management](key-management.md) → -[Derivation](derivation.md) → [Transaction Generator](tx-generator.md). -That sequence walks the typical "make a key, build a transaction, -send it" flow without any file I/O. +- [Key Management](key-management.md) — start here if you have only a + mnemonic or hex private key. +- [Derivation](derivation.md) — turn an `XPrv` into addresses. +- [UTXO Processor](utxo-processor.md) — set up the live UTXO event + pipeline. +- [UTXO Context](utxo-context.md) — track UTXOs for a specific address + set. +- [Transaction Generator](tx-generator.md) — build, sign, and submit. diff --git a/docs/learn/wallet-sdk/tx-generator.md b/docs/learn/wallet-sdk/tx-generator.md index 3ffe35da..7ba795d2 100644 --- a/docs/learn/wallet-sdk/tx-generator.md +++ b/docs/learn/wallet-sdk/tx-generator.md @@ -7,13 +7,42 @@ fees, and yields one or more [`PendingTransaction`](../../reference/Classes/PendingTransaction.md)s ready to sign and submit. -## Send a payment, end to end +## Idiomatic: feed it a `UtxoContext` + +In a long-running process, build a [`UtxoContext`](../../reference/Classes/UtxoContext.md) (see [UTXO Context](utxo-context.md)) +once and pass it as `entries`. The generator pulls the current mature +set on iteration — no manual UTXO snapshot, no stale data. [`PaymentOutput`](../../reference/Classes/PaymentOutput.md) and [`NetworkId`](../../reference/Classes/NetworkId.md) describe the destination. + +```python +gen = Generator( + network_id=NetworkId("mainnet"), + entries=context, # UtxoContext + change_address=my_addr, + outputs=[PaymentOutput(recipient, 100_000_000)], # 1 KAS +) +for pending in gen: + pending.sign([key]) + print("submitted:", await pending.submit(client)) +``` + +A [`Generator`](../../reference/Classes/Generator.md) is *iterable* — when the input set is too large for one +transaction's mass budget, it yields a chain of consolidating +transactions followed by the final payment. Loop and [`submit`](../../reference/Classes/PendingTransaction.md#kaspa.PendingTransaction.submit) each. + +For the full processor → context → generator wiring, see +[Wallet SDK → Overview](overview.md#end-to-end-without-a-managed-wallet). + +## One-shot: raw UTXO list + +Without a context (one-shot scripts, ad-hoc tools), pass the entries +list straight from +[`get_utxos_by_addresses`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.get_utxos_by_addresses) (see [RPC → Calls](../rpc/calls.md#balances-and-utxos)): ```python import asyncio from kaspa import ( RpcClient, Resolver, Generator, PaymentOutput, - Address, PrivateKey, NetworkId, + PrivateKey, NetworkId, ) async def main(): @@ -27,18 +56,15 @@ async def main(): "addresses": [my_addr.to_string()], }) - recipient = Address("kaspa:...") gen = Generator( network_id=NetworkId("mainnet"), entries=utxos["entries"], change_address=my_addr, - outputs=[PaymentOutput(recipient, 500_000_000)], # 5 KAS + outputs=[PaymentOutput(my_addr, 100_000_000)], # 1 KAS ) - for pending in gen: pending.sign([key]) - tx_id = await pending.submit(client) - print("submitted:", tx_id) + print("submitted:", await pending.submit(client)) print(gen.summary().fees, gen.summary().transactions) finally: @@ -47,10 +73,6 @@ async def main(): asyncio.run(main()) ``` -A `Generator` is *iterable* — when the input set is too large for one -transaction's mass budget, it yields a chain of consolidating -transactions followed by the final payment. Loop and submit each. - ## Constructor options ```python @@ -70,20 +92,24 @@ gen = Generator( ) ``` -`entries` accepts a [`UtxoContext`](utxo-context.md) directly — pass +`entries` accepts a [`UtxoContext`](../../reference/Classes/UtxoContext.md) directly — pass the context and it consumes from the mature set without you copying the list. ## Estimate before signing -```python -from kaspa import estimate_transactions +Two entry points, same answer. Use [`gen.estimate()`](../../reference/Classes/Generator.md) if you already +have a [`Generator`](../../reference/Classes/Generator.md); use [`estimate_transactions()`](../../reference/Functions/estimate_transactions.md) to quote a hypothetical +send without constructing one. -# via a Generator +```python +# You already have a Generator — most common case. summary = gen.estimate() print(summary.fees, summary.transactions, summary.utxos) -# via the standalone function +# Standalone — no Generator yet. +from kaspa import estimate_transactions + summary = estimate_transactions( network_id="mainnet", entries=utxos, @@ -92,7 +118,7 @@ summary = estimate_transactions( ) ``` -`estimate()` doesn't consume the generator — you can iterate it for +[`estimate()`](../../reference/Classes/Generator.md) doesn't consume the generator — you can iterate it for real afterwards. ## Pending transactions @@ -113,18 +139,30 @@ signature. ## Signing +The everyday path: sign every input with one or more keys. + ```python -# All inputs at once with one or more keys pending.sign([key]) pending.sign([key1, key2, key3]) # multisig +``` -# Per-input control -for i, _ in enumerate(pending.get_utxo_entries()): - pending.sign_input(i, key) +### Advanced: per-input and custom scripts -# Custom signature scripts (advanced) +Reach for these only when single-call signing isn't enough — usually +mixed-key sets, hardware-signer integrations, or non-standard scripts. +See [`examples/transactions/multisig.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/transactions/multisig.py) +for a full multisig run. + +[`SighashType`](../../reference/Enums/SighashType.md) selects which parts of the transaction the signature commits to: + +```python from kaspa import SighashType +# Per-input control with different keys +for i, _ in enumerate(pending.get_utxo_entries()): + pending.sign_input(i, key) + +# Custom signature scripts sig = pending.create_input_signature( input_index=0, private_key=key, @@ -140,12 +178,12 @@ tx_id = await pending.submit(client) # Or manually: result = await client.submit_transaction({ - "transaction": pending.transaction.serialize_to_dict(), + "transaction": pending.transaction, "allowOrphan": False, }) ``` -`pending.submit(client)` is the right path. The manual route is for +[`pending.submit(client)`](../../reference/Classes/PendingTransaction.md#kaspa.PendingTransaction.submit) is the right path. The manual route is for round-tripping the transaction through another system before submission. See [Transactions → Submission](../transactions/submission.md) for the @@ -153,25 +191,33 @@ submission. See ## One-shot helpers -When the loop-and-submit pattern is more code than you need: +Two free functions for when the loop-and-submit pattern is more code +than you need: + +- **[`create_transaction`](../../reference/Functions/create_transaction.md)** (singular) — builds a single + [`Transaction`](../../reference/Classes/Transaction.md). Use it when you know the input set fits in one tx. +- **[`create_transactions`](../../reference/Functions/create_transactions.md)** (plural) — wraps [`Generator`](../../reference/Classes/Generator.md) end-to-end + and returns `{"transactions": [...], "summary":`[`GeneratorSummary`](../../reference/Classes/GeneratorSummary.md)`}`, + matching the chain you'd get from iterating [`Generator`](../../reference/Classes/Generator.md) yourself. ```python from kaspa import create_transaction, create_transactions +# Singular — one transaction. tx = create_transaction( utxo_entry_source=utxos, - outputs=[{"address": "kaspa:...", "amount": 100_000_000}], + outputs=[{"address": "kaspa:...", "amount": 100_000_000}], # 1 KAS priority_fee=1000, ) +# Plural — Generator-equivalent. result = create_transactions( network_id="mainnet", entries=utxos, change_address=my_addr, - outputs=[{"address": "kaspa:...", "amount": 100_000_000}], + outputs=[{"address": "kaspa:...", "amount": 100_000_000}], # 1 KAS priority_fee=1000, ) - for pending in result["transactions"]: pending.sign([key]) await pending.submit(client) @@ -181,9 +227,11 @@ print(result["summary"]) ## Where to next -- [UTXO Context](utxo-context.md) — pass a context as `entries` instead - of a raw list. +- [UTXO Context](utxo-context.md) — what to pass as `entries` in a + long-running process. - [Wallet → Send Transaction](../wallet/send-transaction.md) — the - managed Wallet wraps `Generator` with sensible defaults. -- [Multi-signature transactions](../../guides/multisig.md) — full multisig - recipe including `minimum_signatures`. + managed [`Wallet`](../../reference/Classes/Wallet.md) wraps [`Generator`](../../reference/Classes/Generator.md) with sensible defaults. +- [Transactions → Submission](../transactions/submission.md) — + `allowOrphan` semantics, confirmation states. +- [Examples](../../examples.md) — runnable scripts including a full + multisig flow. diff --git a/docs/learn/wallet-sdk/utxo-context.md b/docs/learn/wallet-sdk/utxo-context.md index 9ef62f22..bca460a1 100644 --- a/docs/learn/wallet-sdk/utxo-context.md +++ b/docs/learn/wallet-sdk/utxo-context.md @@ -2,15 +2,15 @@ A [`UtxoContext`](../../reference/Classes/UtxoContext.md) tracks UTXOs for a fixed set of addresses. It's bound to a -[UTXO Processor](utxo-processor.md) and fed by it: as the processor +[`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) (see [UTXO Processor](utxo-processor.md)) and fed by it: as the processor receives notifications from the node, it routes changes to whichever contexts have registered the relevant addresses. The context exposes the resulting UTXO set, balance, and mature/pending splits. -The managed [Wallet](../wallet/overview.md) creates a `UtxoContext` -per activated account internally — you usually don't construct one -yourself. Drop down here when you want UTXO tracking without an -on-disk wallet file. +The managed [`Wallet`](../../reference/Classes/Wallet.md) (see [Wallet](../wallet/overview.md)) creates one [`UtxoContext`](../../reference/Classes/UtxoContext.md) +per activated account internally. + +[`UtxoContext`](../../reference/Classes/UtxoContext.md) can be used when you want UTXO tracking outside of [`Wallet`](../../reference/Classes/Wallet.md). ## Build one @@ -34,7 +34,7 @@ context to be addressable across reconnects. ## What it exposes ```python -print(context.is_active) # bool — processor running? +print(context.is_active) # bool — see "Lifecycle" below print(context.balance) # Balance | None print(context.balance_strings) # BalanceStrings | None (formatted) print(context.mature_length) # int — number of spendable UTXOs @@ -44,23 +44,44 @@ pending = context.pending() # list[UtxoEntryReference] ``` `balance` is `None` until the first notification arrives; after that -it's a `Balance(mature, pending, outgoing)` in sompi. - -## Add and remove tracked addresses +it's a [`Balance(mature, pending, outgoing)`](../../reference/Classes/Balance.md) in sompi +([`balance_strings`](../../reference/Classes/BalanceStrings.md) returns the formatted form); +[`mature_range`](../../reference/Classes/UtxoContext.md) and [`pending`](../../reference/Classes/UtxoContext.md) return [`UtxoEntryReference`](../../reference/Classes/UtxoEntryReference.md)s. + +## Lifecycle + +A [`UtxoContext`](../../reference/Classes/UtxoContext.md) has no separate active/inactive state of its own — +`context.is_active` mirrors the bound processor. Implications: + +- The context's address set and in-memory UTXOs persist as long as + the Python object lives, even if the processor is stopped or the + socket dropped. On reconnect the processor re-registers them + automatically. +- [`track_addresses(addresses, current_daa_score=None)`](../../reference/Classes/UtxoContext.md) adds addresses, + subscribes the processor to `UtxosChanged` for them, and seeds the + mature set via a single [`get_utxos_by_addresses`](../../reference/Classes/RpcClient.md#kaspa.RpcClient.get_utxos_by_addresses) RPC. Pass + `current_daa_score` only if you need the maturity classification of + the seeded UTXOs to use a DAA score other than the processor's + current one — for example, when reconstructing balances at a + specific historical point. +- [`unregister_addresses([...])`](../../reference/Classes/UtxoContext.md) removes those addresses and stops the + matching `UtxosChanged` subscription. UTXOs they contributed remain + in the in-memory set until evicted by a notification or by [`clear()`](../../reference/Classes/UtxoContext.md). +- [`clear()`](../../reference/Classes/UtxoContext.md) unregisters every tracked address (unsubscribing from the + node) and drops every cached UTXO. The context is reusable — + [`track_addresses`](../../reference/Classes/UtxoContext.md) again to rehydrate it. ```python await context.track_addresses(["kaspatest:..."]) await context.unregister_addresses(["kaspatest:..."]) -await context.clear() # forget every address and UTXO +await context.clear() ``` -`track_addresses` accepts `Address` instances or their string forms. -`current_daa_score=...` is optional — supply it to ignore -confirmations older than that score. +[`track_addresses`](../../reference/Classes/UtxoContext.md) accepts [`Address`](../../reference/Classes/Address.md) instances or their string forms. ## Use as `Generator` input -The [Transaction Generator](tx-generator.md) accepts a `UtxoContext` +The [`Generator`](../../reference/Classes/Generator.md) (see [Transaction Generator](tx-generator.md)) accepts a [`UtxoContext`](../../reference/Classes/UtxoContext.md) as its `entries` argument: ```python @@ -78,9 +99,9 @@ the current mature set when it iterates. ## Where to next -- [UTXO Processor](utxo-processor.md) — the engine the context is bound - to. - [Transaction Generator](tx-generator.md) — sending using a `UtxoContext` as input. +- [UTXO Processor](utxo-processor.md) — the engine the context is + bound to. - [Wallet → Architecture](../wallet/architecture.md) — how the managed Wallet uses `UtxoContext`s internally. diff --git a/docs/learn/wallet-sdk/utxo-processor.md b/docs/learn/wallet-sdk/utxo-processor.md index f4c88b4e..113619b2 100644 --- a/docs/learn/wallet-sdk/utxo-processor.md +++ b/docs/learn/wallet-sdk/utxo-processor.md @@ -1,14 +1,18 @@ # UTXO Processor -A [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) -subscribes to a node's UTXO and virtual-chain notifications and -dispatches them to one or more [UTXO Contexts](utxo-context.md). It's -the engine that makes context tracking work. The managed -[Wallet](../wallet/overview.md) builds one internally; otherwise, -build one yourself and bind contexts to it. +A [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) is the +engine that drives live UTXO tracking: it owns the wRPC subscription +and dispatches address-scoped UTXO events to one or more +[`UtxoContext`](../../reference/Classes/UtxoContext.md)s (see [UTXO Context](utxo-context.md)). The managed +[`Wallet`](../../reference/Classes/Wallet.md) (see [Wallet](../wallet/overview.md)) builds one internally; otherwise, you +build one and bind contexts to it. + +**Read this page first**, then [UTXO Context](utxo-context.md). ## Construction +Build a [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) on top of an [`RpcClient`](../../reference/Classes/RpcClient.md) and a [`NetworkId`](../../reference/Classes/NetworkId.md): + ```python from kaspa import NetworkId, Resolver, RpcClient, UtxoProcessor @@ -23,37 +27,51 @@ await processor.stop() await client.disconnect() ``` -`start()` activates the processor — it subscribes to node -notifications and starts forwarding. `stop()` is the matching -shutdown. Without `start()`, bound contexts stay empty. +`start()` runs a short handshake against the node: + +1. Calls `get_server_info`, validates RPC version + network match, + bails with `UtxoIndexNotEnabled` if the node has no UTXO index. +2. Opens a single wRPC listener and subscribes to + `VirtualDaaScoreChanged` only — **no** `UtxosChanged` subscription + yet. UTXO subscriptions are opened later, per-address, when a + [`UtxoContext`](../../reference/Classes/UtxoContext.md) calls `track_addresses(...)`. +3. Emits `utxo-proc-start`. + +`stop()` is the matching shutdown. Without `start()`, bound contexts +stay empty. ## Properties | Property | Meaning | | --- | --- | -| `processor.rpc` | The `RpcClient` it's reading from. | -| `processor.network_id` | The `NetworkId` it was constructed with. | +| `processor.rpc` | The [`RpcClient`](../../reference/Classes/RpcClient.md) it's reading from. | +| `processor.network_id` | The [`NetworkId`](../../reference/Classes/NetworkId.md) it was constructed with. | | `processor.is_active` | `True` after `start()`, `False` after `stop()` or before. | ## Events The processor has its own event surface — a smaller, lower-level cousin of the -[managed Wallet's events](../wallet/transaction-history.md). Listeners -use the same shape: +[managed Wallet's events](../wallet/events.md). ```python def on_event(event): print(event["type"], event.get("data")) +# Single event +processor.add_event_listener("maturity", on_event) + +# A list of events — supported here (the wallet's listener takes one +# event at a time) processor.add_event_listener( ["utxo-proc-start", "utxo-proc-stop", "pending", "maturity", "reorg", "stasis", "discovery", "balance", "utxo-proc-error", "error"], on_event, ) -``` -Common events: +# Every event +processor.add_event_listener("all", on_event) +``` | Event | When it fires | | --- | --- | @@ -62,11 +80,26 @@ Common events: | `maturity` | A previously-pending UTXO crossed the maturity depth. | | `reorg`, `stasis` | A UTXO was unwound or coinbase-locked. | | `discovery` | A scan-time discovery hit. | -| `balance` | A bound `UtxoContext`'s balance changed. | +| `balance` | A bound [`UtxoContext`](../../reference/Classes/UtxoContext.md)'s balance changed. | | `utxo-proc-error`, `error` | Something went wrong. | [`UtxoProcessorEvent`](../../reference/Enums/UtxoProcessorEvent.md) -is the enum form if you prefer a typed value over a string. +is the enum form if you prefer typed values over kebab strings. + +## Reconnects + +The underlying [`RpcClient`](../../reference/Classes/RpcClient.md) reconnects automatically when the WebSocket +drops. The processor reacts: + +- On disconnect: emits `utxo-proc-stop`. Bound contexts stay alive + — their address sets and in-memory UTXOs are kept. +- On the next successful reconnect: re-runs the handshake, re-opens + the DAA-score subscription, and re-registers `UtxosChanged` for + every address that's still tracked across all bound contexts. + Re-emits `utxo-proc-start`. + +You don't need to rebuild contexts on reconnect. Gate work that needs +fresh state on `processor.is_active` (or on `utxo-proc-start`). ## Coordinating with `asyncio` @@ -86,12 +119,12 @@ await processor.start() await got_start.wait() ``` -This is the same pattern the managed Wallet uses internally. +This is the same pattern the managed [`Wallet`](../../reference/Classes/Wallet.md) uses internally. ## Where to next -- [UTXO Context](utxo-context.md) — bind a context to the processor. -- [Transaction Generator](tx-generator.md) — pass that context as +- [UTXO Context](utxo-context.md) — bind a context to this processor. +- [Transaction Generator](tx-generator.md) — pass a bound context as `entries`. -- [Wallet → Architecture](../wallet/architecture.md) — how the managed - Wallet drives a `UtxoProcessor` for you. +- [Wallet → Sync State](../wallet/sync-state.md) — the same handshake + surfaced one level up, with the full `SyncState` payload reference. diff --git a/docs/learn/wallet/accounts.md b/docs/learn/wallet/accounts.md index 69aa5168..956f9cd1 100644 --- a/docs/learn/wallet/accounts.md +++ b/docs/learn/wallet/accounts.md @@ -1,51 +1,48 @@ # Accounts -A wallet holds N accounts of mixed kinds, each backed by exactly one -[private key data entry](private-keys.md). The two everyday kinds: - -- **BIP32** — HD-derived; one mnemonic backs many accounts at - different `account_index`es. -- **Keypair** — a single secp256k1 key, one address. See - [Keypair Accounts](keypair.md). +A wallet can hold multiple accounts of mixed kinds, each backed by one +[private key data entry](private-keys.md). ## Account kinds | Kind | Backing private key data | Address derivation | | --- | --- | --- | -| `bip32` | `Mnemonic`, `Bip39Seed`, or `ExtendedPrivateKey` | HD path `m/44'/111111'/'//` | +| `bip32` | `Mnemonic` | HD path `m/44'/111111'/'//` | | `keypair` | `SecretKey` | One address per account (Schnorr or ECDSA) | | `multisig`, `bip32watch`, `legacy` | — | Specialised; not covered here. | -This page covers BIP32. Keypair accounts have their own page. - ## Surface | Method | Purpose | | --- | --- | -| `accounts_enumerate()` | List `AccountDescriptor` for every account. | -| `accounts_get(id)` | Fetch a single descriptor. | -| `accounts_create_bip32(...)` | Create a new HD account at a given `account_index`. | -| `accounts_import_bip32(...)` | Same, but runs an address-discovery scan first. | -| `accounts_rename(...)` | Update an account's name. | -| `accounts_activate([ids])` | Begin UTXO tracking for the given accounts (or all). | -| `accounts_ensure_default(...)` | Idempotently ensure a default `bip32` account exists. | - -For deriving the next address on an existing account, see +| [`accounts_enumerate()`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_enumerate) | List [`AccountDescriptor`](../../reference/Classes/AccountDescriptor.md) for every account. | +| [`accounts_get(id)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_get) | Fetch a single descriptor. | +| [`accounts_create_bip32(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_create_bip32) | Create a new HD account at a given `account_index`. | +| [`accounts_create_keypair(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_create_keypair) | Create a single-key account. | +| [`accounts_import_bip32(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_import_bip32) | Same as create_bip32, but runs an address-discovery scan first. | +| [`accounts_import_keypair(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_import_keypair) | Same as create_keypair (discovery is a no-op for a single address). | +| [`accounts_rename(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_rename) | Update an account's name. | +| [`accounts_activate([ids])`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_activate) | Begin UTXO tracking for the given accounts (or all). | +| [`accounts_ensure_default(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_ensure_default) | Ensure a default `bip32` account exists, creating one if needed. | + +[`accounts_enumerate`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_enumerate), [`accounts_get`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_get), and the create/import methods +return [`AccountDescriptor`](../../reference/Classes/AccountDescriptor.md) objects (with attribute access). For +deriving the next address on an existing account, see [Addresses](addresses.md). For sending, see [Send Transaction](send-transaction.md). -## Create a BIP32 account +## BIP32 accounts ```python prv_key_id = await wallet.prv_key_data_create( - wallet_secret=secret, + wallet_secret=wallet_secret, secret="<24-word mnemonic>", kind=PrvKeyDataVariantKind.Mnemonic, name="demo-mnemonic-key", ) acct = await wallet.accounts_create_bip32( - wallet_secret=secret, + wallet_secret=wallet_secret, prv_key_data_id=prv_key_id, account_name="demo-acct-0", account_index=0, # omit to use the next free index @@ -56,11 +53,13 @@ A second account from the same mnemonic only changes `account_index`: ```python acct1 = await wallet.accounts_create_bip32( - wallet_secret=secret, prv_key_data_id=prv_key_id, account_index=1, + wallet_secret=wallet_secret, + prv_key_data_id=prv_key_id, + account_index=1, ) ``` -## Inspect +### Inspect ```python for a in await wallet.accounts_enumerate(): @@ -76,10 +75,69 @@ only) `account_index`, `xpub_keys`, `ecdsa`, `receive_address_index`, `change_address_index`. `get_addresses()` returns every derived address. +### Import vs. create + +[`accounts_import_bip32`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_import_bip32) runs an address-discovery scan before adding +the account, so previously-funded addresses are recognised as used. +Use it when restoring a known-used mnemonic; use +[`accounts_create_bip32`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_create_bip32) for fresh accounts. + +## Keypair accounts + +A keypair account holds one secp256k1 key and produces one address. +No derivation tree — no "next address", no `account_index`. Use one +when you have a single secret to manage alongside other accounts in +the same wallet, or when moving an existing standalone key into +managed storage. + +A keypair account is backed by a `SecretKey`-variant +[private key data entry](private-keys.md): + +```python +from kaspa import PrivateKey, PrvKeyDataVariantKind + +# 64-char hex secp256k1 secret +secret_hex = PrivateKey(...).to_string() + +secret_pkd = await wallet.prv_key_data_create( + wallet_secret=wallet_secret, + secret=secret_hex, + kind=PrvKeyDataVariantKind.SecretKey, + name="demo-secret-key", +) + +kp = await wallet.accounts_create_keypair( + wallet_secret=wallet_secret, + prv_key_data_id=secret_pkd, + ecdsa=False, # False = Schnorr (default), True = ECDSA + account_name="keypair-acct", +) +``` + +`ecdsa` selects the signature scheme. Schnorr is the modern default +and what almost every caller wants. `ecdsa=True` produces an ECDSA +address — only choose it when you need compatibility with a tool or +hardware that requires ECDSA addresses on Kaspa. + +### Keypair descriptor shape + +| Field | Value | +| --- | --- | +| `kind` | `"keypair"` | +| `account_id` | stable id | +| `receive_address` | the one address | +| `change_address` | the *same* address — no separate change chain | +| `account_index`, `xpub_keys`, `receive_address_index`, `change_address_index` | `None` | +| `ecdsa` | reflects the constructor flag | + +[`accounts_create_new_address`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_create_new_address) raises on a keypair account. +[`accounts_import_keypair`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_import_keypair) is the same as [`accounts_create_keypair`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_create_keypair) — +the address-discovery scan is a no-op for a single address. + ## Activate Accounts must be activated before they emit balance events or accept -sends. Activation requires a connected wRPC client *and* a synced +sends. [`accounts_activate`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_activate) requires a connected wRPC client and a synced wallet — see [Sync State](sync-state.md). ```python @@ -90,11 +148,14 @@ await wallet.accounts_activate() ## Ensure-default +Use [`accounts_ensure_default`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_ensure_default) and the +[`AccountKind`](../../reference/Classes/AccountKind.md) helper: + ```python from kaspa import AccountKind acct = await wallet.accounts_ensure_default( - wallet_secret=secret, + wallet_secret=wallet_secret, account_kind=AccountKind("bip32"), mnemonic_phrase=None, # generate a fresh mnemonic if creating ) @@ -104,21 +165,9 @@ Returns the default `bip32` account if there is one, otherwise creates one (generating a fresh mnemonic when `mnemonic_phrase` is `None`). Only `bip32` is supported; other kinds raise. -## Import vs. create - -`accounts_import_bip32` is the recovery-flow variant: it runs an -address-discovery scan before adding the account, so previously-funded -addresses are recognised as used. Use it when restoring a known-used -mnemonic; use `accounts_create_bip32` for fresh accounts. - -To scan a mnemonic *before* picking an index, see -[Wallet Recovery](../../guides/wallet-recovery.md). - ## Where to next - [Addresses](addresses.md) — derive new receive / change addresses on an existing account. -- [Keypair Accounts](keypair.md) — single-key accounts. - [Send Transaction](send-transaction.md) — outgoing flows. -- [Transaction History](transaction-history.md) — `Balance`, `Pending`, - and `Maturity` events. +- [Events](events.md) — `Balance`, `Pending`, and `Maturity` events. diff --git a/docs/learn/wallet/addresses.md b/docs/learn/wallet/addresses.md index 5476b33e..ee801637 100644 --- a/docs/learn/wallet/addresses.md +++ b/docs/learn/wallet/addresses.md @@ -1,61 +1,63 @@ # Addresses -A BIP32 account derives addresses lazily. The wallet records two -indices per account — one for receive, one for change — and -`accounts_create_new_address` advances them. Keypair accounts have a -single fixed address and reject this call. +A BIP32 account derives [`Address`](../../reference/Classes/Address.md) instances. The wallet records two +indices per account — one for receive, one for change. +[`accounts_create_new_address`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_create_new_address) advances them. Keypair accounts have a +single fixed address; calling [`accounts_create_new_address`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_create_new_address) on one +raises. + +For the lower-level [`Address`](../../reference/Classes/Address.md) primitive (parsing, encoding, script conversion), +see [Fundamentals → Addresses](../addresses.md). ## Read the current addresses -`AccountDescriptor` already carries the most recent of each: +[`AccountDescriptor`](../../reference/Classes/AccountDescriptor.md) already carries the most recent of each: ```python -acct = await wallet.accounts_get(acct_id) +acct = await wallet.accounts_get(account_id=acct.account_id) print(acct.receive_address) # for the next-to-receive index print(acct.change_address) # for the next-to-spend-from-as-change index print(acct.receive_address_index) # int, BIP32 only print(acct.change_address_index) # int, BIP32 only ``` -`get_addresses()` returns *every* derived address on the account — -the right choice for re-subscribing UTXO notifications across all of -them. +[`get_addresses()`](../../reference/Classes/AccountDescriptor.md) returns every derived address on the account — the +right choice for re-subscribing UTXO notifications across all of them. ## Derive the next address +[`accounts_create_new_address`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_create_new_address) takes a [`NewAddressKind`](../../reference/Enums/NewAddressKind.md) and returns the next derived address: + ```python from kaspa import NewAddressKind next_recv = await wallet.accounts_create_new_address( - acct.account_id, NewAddressKind.Receive, + account_id=acct.account_id, + kind=NewAddressKind.Receive, ) next_change = await wallet.accounts_create_new_address( - acct.account_id, NewAddressKind.Change, + account_id=acct.account_id, + kind=NewAddressKind.Change, ) ``` The index used is the descriptor's `receive_address_index` or `change_address_index` *before* the call; afterwards the descriptor's counter advances by one. Newly derived addresses register -automatically with the account's `UtxoContext`, so funds sent to them +automatically with the account's [`UtxoContext`](../../reference/Classes/UtxoContext.md), so funds sent to them appear in the next sync. ## Receive vs. change -- **Receive** addresses are what you hand out. Generate one any time - you need a new public-facing address — for billing, for separating - customers, for a hot/cold split. +- **Receive** addresses are what you hand out to 3rd parties. - **Change** addresses are where the wallet returns leftover funds. - The `Generator` (used internally by `accounts_send`) picks the - current change address automatically; you usually don't advance the - change index by hand. To sweep a UTXO set and leave leftover on a *fresh* change address, advance the change index first — see [Sweep Funds](sweep.md). ## Address discovery on import -`accounts_import_bip32` walks the receive and change chains for +[`accounts_import_bip32`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_import_bip32) walks the receive and change chains for addresses that have ever held a UTXO and bumps the indices accordingly. That's what lets a restored wallet "remember" addresses it previously handed out. Without it, `next_recv` would silently @@ -64,7 +66,5 @@ re-issue an already-used address. ## Where to next - [Send Transaction](send-transaction.md) — sending to an address. -- [Transaction History](transaction-history.md) — events that fire when a - derived address receives funds. -- [Wallet Recovery](../../guides/wallet-recovery.md) — scanning a mnemonic - to find used account indices before importing. +- [Events](events.md) — events that fire when a derived address + receives funds. diff --git a/docs/learn/wallet/architecture.md b/docs/learn/wallet/architecture.md index 86020a87..78d0e6f7 100644 --- a/docs/learn/wallet/architecture.md +++ b/docs/learn/wallet/architecture.md @@ -1,40 +1,43 @@ # Architecture -The `Wallet` class has quite a bit going on behind the scenes - it's a small system of cooperating -pieces. Knowing how they fit together is what makes the -[sync gate](sync-state.md) and -[transaction-history events](transaction-history.md) make sense. +The `Wallet` class is a system of cooperating components. Knowing some of the underlying mechanics is useful for development. + +This is a very brief, high-level overview. ## The pieces ```mermaid flowchart TD Wallet["Wallet
lifecycle, file storage, accounts"] - UtxoProcessor["UtxoProcessor"] RpcClient["RpcClient"] - UtxoContext["UtxoContext
one per activated account"] + UtxoProcessor["UtxoProcessor"] + Ctx0["UtxoContext
account 0"] + Ctx1["UtxoContext
account 1"] - Wallet -- owns --> UtxoProcessor Wallet -- owns --> RpcClient + Wallet -- owns --> UtxoProcessor RpcClient -- pushes notifications --> UtxoProcessor - UtxoProcessor -- fans out to --> UtxoContext + UtxoProcessor -- routes per-account --> Ctx0 + UtxoProcessor -- routes per-account --> Ctx1 ``` | Component | Job | | --- | --- | -| **[`Wallet`](../../reference/Classes/Wallet.md)** | Lifecycle, on-disk file storage, account list, event multiplexer. The thing your code calls. | +| **[`Wallet`](../../reference/Classes/Wallet.md)** | Lifecycle, on-disk file storage, account list, event multiplexer. The object your code interacts with. | | **[`RpcClient`](../../reference/Classes/RpcClient.md)** | The wRPC connection. Used internally for calls and as the source of node-pushed notifications. | | **[`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md)** | Subscribes to virtual-chain / UTXO notifications, tracks `synced` state, routes UTXO changes to the right `UtxoContext`. | | **[`UtxoContext`](../../reference/Classes/UtxoContext.md)** | One per activated account. Holds tracked addresses, per-state balance (`mature`, `pending`, `outgoing`), and the mature UTXO set the coin selector pulls from. | -The wallet *does not poll* the node for UTXO state. It is **fed** by -the processor from notifications — see [Sync State](sync-state.md) -for what gates that flow. +The wallet does not poll the node for UTXO state. It is fed by the +processor from notifications — see [Sync State](sync-state.md) for +what gates that flow. ## Where to next - [Lifecycle](lifecycle.md) — the state machine and boot sequence. - [Sync State](sync-state.md) — node IBD vs. processor readiness. -- [UTXO Maturity](utxo-maturity.md) — Pending / Mature / Outgoing +- [Send Transaction → UTXO maturity](send-transaction.md#utxo-maturity) — Pending / Mature / Outgoing states and why `accounts_get_utxos` can return `[]`. -- [Transaction History](transaction-history.md) — the event surface. +- [Events](events.md) — the live event surface. +- [Transaction History](transaction-history.md) — stored records and + annotation. diff --git a/docs/learn/wallet/errors.md b/docs/learn/wallet/errors.md new file mode 100644 index 00000000..a80ef3e5 --- /dev/null +++ b/docs/learn/wallet/errors.md @@ -0,0 +1,28 @@ +# Errors + +The wallet's errors live in `kaspa.exceptions`. Most are +self-explanatory; a few are common enough that they're worth knowing +ahead of time. The full list is in +[`kaspa.exceptions`](../../reference/SUMMARY.md). + +## Common errors + +| Error | Triggered by | Fix | +| --- | --- | --- | +| [`WalletAlreadyExistsError`](../../reference/Exceptions/WalletAlreadyExistsError.md) | [`wallet_create`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_create)`(filename=..., overwrite_wallet_storage=False)` when the file exists. | Catch and call [`wallet_open(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_open) instead, or pass `overwrite_wallet_storage=True` (clobbers). See [Lifecycle → create-or-open](lifecycle.md#create-or-open-pattern). | +| [`WalletNotOpenError`](../../reference/Exceptions/WalletNotOpenError.md) | Any `prv_key_data_*`, `accounts_*`, `transactions_*` call before [`wallet_create`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_create) / [`wallet_open`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_open). | Open the wallet first. | +| [`WalletNotConnectedError`](../../reference/Exceptions/WalletNotConnectedError.md) | [`accounts_activate(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_activate) without a connected wRPC client. | `await wallet.`[`connect`](../../reference/Classes/Wallet.md#kaspa.Wallet.connect)`(...)` first. | +| [`WalletNotSyncedError`](../../reference/Exceptions/WalletNotSyncedError.md) | [`accounts_send`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_send), [`accounts_estimate`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_estimate), [`accounts_get_utxos`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_get_utxos), [`accounts_transfer`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_transfer) before the processor finishes its sync handshake. | Wait on [`wallet.is_synced`](../../reference/Classes/Wallet.md#kaspa.Wallet.is_synced) (poll or `SyncState` listener) — see [Sync State](sync-state.md). | +| [`WalletInsufficientFundsError`](../../reference/Exceptions/WalletInsufficientFundsError.md) | [`accounts_send`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_send) / [`accounts_transfer`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_transfer) when the mature balance can't cover `outputs + fees`. | Wait for pending UTXOs to mature (`Maturity` [event](events.md)), reduce amount, or lower priority fee. | +| `"Invalid prv key data kind, supported types are Mnemonic and SecretKey"` | [`prv_key_data_create`](../../reference/Classes/Wallet.md#kaspa.Wallet.prv_key_data_create)`(kind=`[`PrvKeyDataVariantKind`](../../reference/Enums/PrvKeyDataVariantKind.md)`.Bip39Seed)` or `kind=ExtendedPrivateKey`. | Use `Mnemonic` or `SecretKey`. The other two enum variants are not implemented upstream — see [Private Keys](private-keys.md#variants). | +| `UtxoIndexNotEnabled` ([event](events.md), not exception) | Connecting to a kaspad without the UTXO index. The processor refuses to proceed. | Point at a node that has UTXO indexing enabled. | +| `RuntimeError: cannot change network while connected` | [`wallet.set_network_id(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.set_network_id) after [`connect()`](../../reference/Classes/Wallet.md#kaspa.Wallet.connect). | `await wallet.`[`disconnect()`](../../reference/Classes/Wallet.md#kaspa.Wallet.disconnect) → [`set_network_id(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.set_network_id) → `await wallet.`[`connect()`](../../reference/Classes/Wallet.md#kaspa.Wallet.connect). | +| `RuntimeError: cannot derive address on a keypair account` | [`accounts_create_new_address(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_create_new_address) on a keypair account. | Keypair accounts have one fixed address; read it from [`descriptor.receive_address`](../../reference/Classes/AccountDescriptor.md). See [Keypair accounts](accounts.md#keypair-accounts). | + +## Where to next + +- [Lifecycle](lifecycle.md) — preconditions and ordering rules. +- [Sync State](sync-state.md) — what the sync gate is and how to wait + on it. +- [`kaspa.exceptions` reference](../../reference/SUMMARY.md) — the + full set of named exceptions. diff --git a/docs/learn/wallet/events.md b/docs/learn/wallet/events.md new file mode 100644 index 00000000..5cf39eec --- /dev/null +++ b/docs/learn/wallet/events.md @@ -0,0 +1,190 @@ +# Events + +The wallet emits events for every state change the node pushes +through. Register Python callbacks on the wallet; its event +multiplexer forwards relevant events to them, and you react — update +a UI, trigger the next send, log a maturity, handle a reorg. + +For the "what happened in this account?" view that reads stored +transaction records, see [Transaction History](transaction-history.md). + +!!! warning "Callbacks must be synchronous" + The dispatcher invokes callbacks via `callback(*args, event, **kwargs)` — + a direct synchronous call. If you pass an `async def` function, it + returns a coroutine that is **never awaited**: the body silently does + not run. Use a regular `def` and offload async work with + `asyncio.create_task(...)` from inside the callback. + +## Listener API + +The wallet exposes [`add_event_listener`](../../reference/Classes/Wallet.md#kaspa.Wallet.add_event_listener) and [`remove_event_listener`](../../reference/Classes/Wallet.md#kaspa.Wallet.remove_event_listener): + +```python +def add_event_listener(event, callback, *args, **kwargs) -> None +def remove_event_listener(event, callback=None) -> None +``` + +- `event` — a + [`WalletEventType`](../../reference/Enums/WalletEventType.md), its + kebab-case string name (`"balance"`, `"sync-state"`), or `"all"` / + [`WalletEventType.All`](../../reference/Enums/WalletEventType.md) for every event. +- `callback` — invoked as `callback(*args, event, **kwargs)`. Must be + synchronous (see warning above). +- `args` / `kwargs` — forwarded verbatim to every invocation. Handy + for routing context (account id, channel) without closures. +- [`remove_event_listener(event)`](../../reference/Classes/Wallet.md#kaspa.Wallet.remove_event_listener) with no callback clears every + listener for that event. With `"all"` and no callback, clears every + listener globally. + +## A minimal subscriber + +```python +from kaspa import Resolver, Wallet, WalletEventType + +def on_event(event): + print(event["type"], event.get("data")) + +wallet = Wallet(network_id="testnet-10", resolver=Resolver()) +wallet.add_event_listener(WalletEventType.All, on_event) + +await wallet.start() +await wallet.connect() +``` + +Each event is a dict with at least a `type` key (the kebab-case kind +name, e.g. `"balance"`, `"sync-state"`, `"fee-rate"`) and an optional +`data` payload specific to that event. + +## Event taxonomy + +| Group | Events | +| --- | --- | +| Connection | `Connect`, `Disconnect`, `ServerStatus`, `UtxoIndexNotEnabled` | +| Wallet file | `WalletList`, `WalletStart`, `WalletHint`, `WalletOpen`, `WalletCreate`, `WalletReload`, `WalletClose`, `WalletError` | +| Key & account state | `PrvKeyDataCreate`, `AccountCreate`, `AccountActivation`, `AccountDeactivation`, `AccountSelection`, `AccountUpdate` | +| Sync & runtime | `SyncState`, `UtxoProcStart`, `UtxoProcStop`, `UtxoProcError`, `DaaScoreChange`, `Metrics`, `FeeRate` | +| UTXO movement | `Pending`, `Maturity`, `Reorg`, `Stasis`, `Discovery`, `Balance` | +| Catch-all | `All`, `Error` | + +## Event payloads + +Every event is `{"type": , "data": }`. The `data` +shapes below come from `kaspa-wallet-core::events`. Field names that +appear in `camelCase` are camel-cased on the wire; `snake_case` fields +are passed through as-is. + +### UTXO movement + +| Event | `data` fields | +| --- | --- | +| `Balance` | `balance: {mature, pending, outgoing, mature_utxo_count, pending_utxo_count, stasis_utxo_count} \| None`, `id: ` (see [`Balance`](../../reference/Classes/Balance.md)) | +| `Pending` | `record: TransactionRecord` — UTXO seen, not yet mature. | +| `Maturity` | `record: TransactionRecord` — UTXO crossed the maturity threshold; spendable. | +| `Reorg` | `record: TransactionRecord` — pending UTXO unwound by a reorg. | +| `Stasis` | `record: TransactionRecord` — coinbase output unwound during stasis. Safe to ignore. | +| `Discovery` | `record: TransactionRecord` — UTXO discovered during the initial scan of an account. When using the runtime [`Wallet`](../../reference/Classes/Wallet.md), you can usually rely on the [transaction history](transaction-history.md) instead. | + +A `TransactionRecord` carries `id`, `unixtimeMsec`, `value`, +`binding`, `blockDaaScore`, `network`, `data` (the per-kind +transaction body), and optional `note` / `metadata` strings. + +### Sync & runtime + +| Event | `data` fields | +| --- | --- | +| `SyncState` | `syncState: {type, data}` — see [Sync State → payloads](sync-state.md#reading-syncstate-payloads). | +| `UtxoProcStart` | *(no data)* | +| `UtxoProcStop` | *(no data)* | +| `UtxoProcError` | `message: str` | +| `DaaScoreChange` | `currentDaaScore: int` | +| `Metrics` | `networkId: str`, `metrics: MetricsUpdate` | +| `FeeRate` | `priority: {feerate, seconds}`, `normal: {...}`, `low: {...}` | + +### Key & account state + +| Event | `data` fields | +| --- | --- | +| `PrvKeyDataCreate` | `prvKeyDataInfo:`[`PrvKeyDataInfo`](../../reference/Classes/PrvKeyDataInfo.md) | +| `AccountCreate` | `accountDescriptor:`[`AccountDescriptor`](../../reference/Classes/AccountDescriptor.md) | +| `AccountActivation` | `ids: list[`[`AccountId`](../../reference/Classes/AccountId.md)`]` | +| `AccountDeactivation` | `ids: list[`[`AccountId`](../../reference/Classes/AccountId.md)`]` | +| `AccountSelection` | `id:`[`AccountId`](../../reference/Classes/AccountId.md)`\| None` | +| `AccountUpdate` | `accountDescriptor:`[`AccountDescriptor`](../../reference/Classes/AccountDescriptor.md) — fires when a new address is generated, etc. | + +### Wallet file + +| Event | `data` fields | +| --- | --- | +| `WalletList` | `walletDescriptors: list[`[`WalletDescriptor`](../../reference/Classes/WalletDescriptor.md)`]` | +| `WalletStart` | *(no data)* — fires once after [`start()`](../../reference/Classes/Wallet.md#kaspa.Wallet.start). | +| `WalletHint` | `hint: str \| None` — anti-phishing hint stored on the wallet file. | +| `WalletOpen` | `walletDescriptor:`[`WalletDescriptor`](../../reference/Classes/WalletDescriptor.md)`\| None`, `accountDescriptors: list[`[`AccountDescriptor`](../../reference/Classes/AccountDescriptor.md)`] \| None` | +| `WalletCreate` | `walletDescriptor:`[`WalletDescriptor`](../../reference/Classes/WalletDescriptor.md), `storageDescriptor: StorageDescriptor` | +| `WalletReload` | `walletDescriptor:`[`WalletDescriptor`](../../reference/Classes/WalletDescriptor.md)`\| None`, `accountDescriptors: list[`[`AccountDescriptor`](../../reference/Classes/AccountDescriptor.md)`] \| None` | +| `WalletClose` | *(no data)* | +| `WalletError` | `message: str` | + +### Connection + +| Event | `data` fields | +| --- | --- | +| `Connect` | `networkId: str`, `url: str \| None` | +| `Disconnect` | `networkId: str`, `url: str \| None` | +| `ServerStatus` | `networkId: str`, `serverVersion: str`, `isSynced: bool`, `url: str \| None` | +| `UtxoIndexNotEnabled` | `url: str \| None` | + +## When each event fires + +Common subscriptions: + +- **`SyncState`** — progress while the [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) catches up. + Pair with [`wallet.is_synced`](../../reference/Classes/Wallet.md#kaspa.Wallet.is_synced) — see [Sync State](sync-state.md). +- **`Balance`** — fires when a [`UtxoContext`](../../reference/Classes/UtxoContext.md) balance changes. The + right signal for live UI updates. +- **`Pending`** — a new UTXO landed for a tracked address but isn't + yet spendable. +- **`Maturity`** — a previously-pending UTXO crossed the maturity + depth and is now spendable. The strongest gate for "send-then-wait" + flows — don't trigger the next [`accounts_send`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_send) on `Pending` alone. +- **`Reorg`** / **`Stasis`** — a UTXO was unwound or coinbase-locked. + Defensive code for high-value flows. +- **`AccountActivation`** / **`AccountDeactivation`** — react to + [`accounts_activate`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_activate) / [`wallet_close`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_close). + +## Targeted subscriptions + +```python +wallet.add_event_listener("balance", on_balance) +wallet.add_event_listener("maturity", on_maturity) +wallet.add_event_listener(WalletEventType.SyncState, on_sync) +``` + +To pass context to a generic callback: + +```python +wallet.add_event_listener("balance", on_change, account.account_id, label="primary") +# callback receives: on_change(account.account_id, event, label="primary") +``` + +## Cleanup + +Listeners outlive the wallet's open file but not its runtime. Pair a +permanent registration with an explicit removal on shutdown, or use +`"all"` to clear in one call via [`remove_event_listener`](../../reference/Classes/Wallet.md#kaspa.Wallet.remove_event_listener): + +```python +wallet.remove_event_listener(WalletEventType.All) +await wallet.stop() +``` + +## Where to next + +- [Transaction History](transaction-history.md) — stored records, + notes, and metadata. +- [Send Transaction](send-transaction.md) — `Maturity` as the right + wait condition. +- [Architecture](architecture.md) — what's actually generating these + events. +- [Lifecycle](lifecycle.md) — when each event group fires. +- [Wallet SDK → UTXO Processor](../wallet-sdk/utxo-processor.md) — + the lower-level event surface beneath the managed wallet. diff --git a/docs/learn/wallet/keypair.md b/docs/learn/wallet/keypair.md deleted file mode 100644 index 1b7b3e44..00000000 --- a/docs/learn/wallet/keypair.md +++ /dev/null @@ -1,79 +0,0 @@ -# Keypair Accounts - -A keypair account holds one secp256k1 key and produces one address. -It has no derivation tree — no "next address", no `account_index`. -Use one when you have a single secret to manage alongside other -accounts in the same wallet, or when moving an existing standalone -key into managed storage. - -## Create a keypair account - -A keypair account is backed by a `SecretKey`-variant -[private key data entry](private-keys.md): - -```python -from kaspa import PrivateKey, PrvKeyDataVariantKind - -# 64-char hex secp256k1 secret -secret_hex = PrivateKey(...).to_string() - -secret_pkd = await wallet.prv_key_data_create( - wallet_secret=secret, - secret=secret_hex, - kind=PrvKeyDataVariantKind.SecretKey, - name="demo-secret-key", -) - -kp = await wallet.accounts_create_keypair( - wallet_secret=secret, - prv_key_data_id=secret_pkd, - ecdsa=False, # False = Schnorr, True = ECDSA - account_name="keypair-acct", -) -``` - -`ecdsa` is required. `ecdsa=False` (Schnorr) is what most callers -want; `ecdsa=True` produces an ECDSA-style keypair account. - -## What the descriptor looks like - -Keypair `AccountDescriptor`s have: - -| Field | Value | -| --- | --- | -| `kind` | `"keypair"` | -| `account_id` | stable id | -| `receive_address` | the one address | -| `change_address` | the *same* address — no separate change chain | -| `account_index`, `xpub_keys`, `receive_address_index`, `change_address_index`, `ecdsa` | `None` for the indices; `ecdsa` reflects the constructor flag | - -`accounts_create_new_address` raises on a keypair account — there's -no next address to derive. - -## When to use a keypair account - -- You generated a key with the standalone - [`PrivateKey`](../../reference/Classes/PrivateKey.md) API or - imported one from another tool, and want it managed in a wallet - file. -- You want a single-purpose hot wallet — one address, no rotation. -- You're testing, and a single deterministic address is easier to - reason about than an HD chain. - -For user-facing or rotation-sensitive wallets, use a -[BIP32 account](accounts.md) instead. - -## Import vs. create - -`accounts_import_keypair` is the variant for an existing key with -on-chain history. The address-discovery scan is a no-op (only one -address), so it's effectively the same as `accounts_create_keypair` — -pick whichever reads better at the call site. - -## Where to next - -- [Accounts](accounts.md) — BIP32 accounts. -- [Send Transaction](send-transaction.md) — sending from a keypair - account works the same as from a BIP32 account. -- [Wallet SDK → Key Management](../wallet-sdk/key-management.md) — - generating a `PrivateKey` outside the wallet first. diff --git a/docs/learn/wallet/lifecycle.md b/docs/learn/wallet/lifecycle.md index 81edf828..40043833 100644 --- a/docs/learn/wallet/lifecycle.md +++ b/docs/learn/wallet/lifecycle.md @@ -1,9 +1,7 @@ # Lifecycle A `Wallet` moves through five states. Each transition is async and -ordered — skipping or repeating steps will either raise (e.g. opening -without `start()`) or leave the wallet in a broken state (e.g. -operating before sync). +ordered. ```mermaid stateDiagram-v2 @@ -21,29 +19,28 @@ stateDiagram-v2 | Step | Method | Effect | | --- | --- | --- | -| Construct | `Wallet(network_id, encoding, url, resolver)` | Builds the local file store and an internal wRPC client. No I/O. | -| Start | `await wallet.start()` | Boots the `UtxoProcessor`, the wRPC notifier, and the event-dispatch task. | -| Connect | `await wallet.connect(...)` | Connects the wRPC client to a node (via `resolver` or explicit `url`). | -| Open | `await wallet.wallet_create(...)` / `wallet_open(...)` | Decrypts and loads a wallet file; secrets become available in memory. | -| Activate | `await wallet.accounts_activate([ids])` | Begins UTXO tracking and event emission for the chosen accounts. | -| Close | `await wallet.wallet_close()` | Releases the open wallet; activated accounts stop tracking. | -| Disconnect | `await wallet.disconnect()` | Drops the wRPC connection; the wallet remains started. | -| Stop | `await wallet.stop()` | Tears down the runtime and event task. | +| Construct | [`Wallet(network_id, encoding, url, resolver)`](../../reference/Classes/Wallet.md#kaspa.Wallet) | Builds the local file store and an internal wRPC client. No I/O. | +| Start | [`await wallet.start()`](../../reference/Classes/Wallet.md#kaspa.Wallet.start) | Boots the `UtxoProcessor`, the wRPC notifier, and the event-dispatch task. | +| Connect | [`await wallet.connect(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.connect) | Connects the wRPC client to a node (via `resolver` or explicit `url`). | +| Open | [`await wallet.wallet_create(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_create) / [`wallet_open(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_open) | Decrypts and loads a wallet file; secrets become available in memory. | +| Activate | [`await wallet.accounts_activate([ids])`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_activate) | Begins UTXO tracking and event emission for the chosen accounts. | +| Close | [`await wallet.wallet_close()`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_close) | Releases the open wallet; activated accounts stop tracking. | +| Disconnect | [`await wallet.disconnect()`](../../reference/Classes/Wallet.md#kaspa.Wallet.disconnect) | Drops the wRPC connection; the wallet remains started. | +| Stop | [`await wallet.stop()`](../../reference/Classes/Wallet.md#kaspa.Wallet.stop) | Tears down the runtime and event task. | ## Properties | Property | Type | Meaning | | --- | --- | --- | -| `wallet.rpc` | [`RpcClient`](../../reference/Classes/RpcClient.md) | The underlying wRPC client. Use for direct node calls. | -| `wallet.is_open` | `bool` | `True` between `wallet_open` / `wallet_create` and `wallet_close`. | -| `wallet.is_synced` | `bool` | `True` once the `UtxoProcessor` has caught up. See [Sync State](sync-state.md). | -| `wallet.descriptor` | `WalletDescriptor \| None` | Metadata for the open wallet, or `None` when closed. | +| [`wallet.rpc`](../../reference/Classes/Wallet.md#kaspa.Wallet.rpc) | [`RpcClient`](../../reference/Classes/RpcClient.md) | The underlying wRPC client. Use for direct node calls. | +| [`wallet.is_open`](../../reference/Classes/Wallet.md#kaspa.Wallet.is_open) | `bool` | `True` between `wallet_open` / `wallet_create` and `wallet_close`. | +| [`wallet.is_synced`](../../reference/Classes/Wallet.md#kaspa.Wallet.is_synced) | `bool` | `True` once the `UtxoProcessor` has caught up. See [Sync State](sync-state.md). | +| [`wallet.descriptor`](../../reference/Classes/Wallet.md#kaspa.Wallet.descriptor) | `WalletDescriptor \| None` | Metadata for the open wallet, or `None` when closed. | -## 1.) Construct +## Construct Constructing a [`Wallet`](../../reference/Classes/Wallet.md) does no -I/O. It builds the local file store and an internal wRPC client — -that's it. +I/O. It builds the local file store and an internal wRPC client. ```python from kaspa import Resolver, Wallet @@ -64,8 +61,7 @@ wallet = Wallet( | `encoding` | optional | `"borsh"` (default) or `"json"`. Borsh is right for almost everything. | Addresses derived from this wallet are encoded for `network_id`, and -the resolver only returns nodes on that network — pin it before -`accounts_activate`. +the resolver only returns nodes on that network. ### Switching networks @@ -79,71 +75,55 @@ await wallet.connect() Switching network does not invalidate the file store, but BIP32 account *addresses* are network-specific — a key created under -`testnet-10` produces different (testnet) addresses than the same key -under `mainnet`. +`testnet-10` produces different addresses than the same key under +`mainnet`. ### Storage location Wallet files live in the SDK's local store (under `~/.kaspa/` by -default). The folder is created on first write — nothing happens at -construction time. The current `Wallet` constructor does not expose a -per-instance override; the location is fixed for the process. +default). The folder is created on first write. The current `Wallet` +constructor does not expose a per-instance override; the location is +fixed for the process. -## 2.) Start the runtime +## Start and connect -`start()` boots the wallet's runtime — `UtxoProcessor`, wRPC notifier, -and event-dispatch loop. `connect()` then attaches the wRPC client to -a node. After both, the wallet is ready to *open a file*, but not yet -ready to touch UTXO state. +`start()` boots the runtime. `connect()` attaches the wRPC client to a +node. ```python -wallet = Wallet(network_id="testnet-10", resolver=Resolver()) await wallet.start() -await wallet.connect() +await wallet.connect(strategy="fallback", timeout_duration=5_000) ``` -Both are required. `start()` without `connect()` leaves the runtime -running but unable to reach the node; `connect()` without a prior -`start()` leaves the wallet runtime unstarted, so account activation -and event dispatch never function. +`start()` without `connect()` leaves the runtime running but unable to +reach the node. `connect()` without a prior `start()` leaves the +wallet runtime unstarted, so account activation and event dispatch +never function. ### Connect options `connect()` takes the same options as -[`RpcClient.connect`](../rpc/connecting.md#connection-options): - -```python -await wallet.connect( - block_async_connect=True, # await readiness before returning - strategy="retry", # "retry" or "fallback" - url=None, # override the resolver-discovered URL - timeout_duration=10_000, # ms - retry_interval=1_000, # ms -) -``` - -If you constructed with a `Resolver`, omit `url` and let it pick a -public node. Pass `url=` to override for one connection (handy for -pinning to a specific node temporarily). +[`RpcClient.connect`](../rpc/connecting.md#connection-options) — +`block_async_connect`, `strategy`, `url`, `timeout_duration`, +`retry_interval`. Pass `url=` to override the resolver-discovered node +for one connection. ### Sync gate -`connect()` resolves as soon as the WebSocket is up — *not* when the -wallet's UTXO processor has caught up. Until `wallet.is_synced` flips -to `True`, UTXO-dependent calls (`AccountDescriptor.balance`, -`accounts_get_utxos`, `accounts_send`) are unusable. Quick polling -form for scripts: +[`connect()`](../../reference/Classes/Wallet.md#kaspa.Wallet.connect) resolves as soon as the WebSocket is up — not when the +processor has caught up. UTXO-dependent calls +([`AccountDescriptor.balance`](../../reference/Classes/AccountDescriptor.md), [`accounts_get_utxos`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_get_utxos), [`accounts_send`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_send)) +wait on [`wallet.is_synced`](../../reference/Classes/Wallet.md#kaspa.Wallet.is_synced). Quick polling form for scripts: ```python -await wallet.connect(...) while not wallet.is_synced: await asyncio.sleep(0.5) ``` -For the event-driven pattern and the node-vs-processor breakdown of -what "synced" actually means, see [Sync State](sync-state.md). +For the event-driven pattern and the node-vs-processor breakdown, see +[Sync State](sync-state.md). -## 3.) Open a wallet file +## Open a wallet file A wallet file is a single encrypted file on disk. Only one is open at a time per `Wallet` instance. @@ -159,8 +139,8 @@ created = await wallet.wallet_create( ``` - `filename` — on-disk basename; omit for the SDK default. -- `overwrite_wallet_storage=False` — raises `WalletAlreadyExistsError` - if the file exists; pass `True` to clobber. +- `overwrite_wallet_storage=False` — raises [`WalletAlreadyExistsError`](../../reference/Exceptions/WalletAlreadyExistsError.md) + if the file exists; pass `True` to overwrite. - `user_hint` — stored alongside the file as a recoverable password hint. @@ -169,55 +149,44 @@ To open an existing file: ```python opened = await wallet.wallet_open( wallet_secret="example-secret", - account_descriptors=True, # include account list in the response + account_descriptors=True, filename="demo", ) ``` -`account_descriptors=True` returns the account list in the response -so you can pick which to activate without a follow-up -`accounts_enumerate()`. +`account_descriptors=True` returns the account list in the response so +you can pick which to activate without a follow-up +[`accounts_enumerate()`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_enumerate). ### Create-or-open pattern -`wallet_create` raises `WalletAlreadyExistsError` when the file -exists. The canonical idempotent boot: - ```python from kaspa.exceptions import WalletAlreadyExistsError try: await wallet.wallet_create( - wallet_secret=secret, filename="demo", overwrite_wallet_storage=False, + wallet_secret=secret, + filename="demo", + overwrite_wallet_storage=False, ) except WalletAlreadyExistsError: - await wallet.wallet_open(secret, True, "demo") + await wallet.wallet_open( + wallet_secret=secret, + account_descriptors=True, + filename="demo", + ) ``` For listing, exporting, importing, renaming, and re-encrypting wallet files, see [Wallet Files](wallet-files.md). -## Activate accounts - -`accounts_activate` is what makes accounts emit balance events and -accept sends. Activation requires a connected wRPC client *and* a -synced wallet — see [Sync State](sync-state.md). - -```python -await wallet.accounts_activate([acct.account_id]) -# or, activate every account: -await wallet.accounts_activate() -``` - -Account creation, BIP32 derivation, and discovery flows live on -[Accounts](accounts.md). - ## Reload -`wallet_reload(reactivate)` reboots the account runtime from cached -wallet data — no disk I/O. Pass `reactivate=True` to resume -previously active accounts; pass `False` to call `accounts_activate` -yourself. A `WalletReload` event fires either way. +[`wallet_reload(reactivate)`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_reload) reboots the account runtime from cached +wallet data without disk I/O. Pass `reactivate=True` to resume +previously active accounts. A `WalletReload` [event](events.md) fires either way. +Useful after upstream account-state changes; you usually don't need +it. ## Close, disconnect, stop @@ -234,23 +203,22 @@ shutdown. Skipping `stop()` leaks the notification task; skipping ## Ordering rules !!! warning "Preconditions" - - `start()` must precede `connect()`, `wallet_create()`, and `wallet_open()`. - - `wallet_create()` / `wallet_open()` may run before or after - `connect()`, but `accounts_activate()` requires the wRPC client to - be connected *and* the wallet to be synced (see [Sync State](sync-state.md)). - - `set_network_id()` raises if the wRPC client is currently - connected — `disconnect()` first, change the network, then - `connect()` again. - - `wallet_close()` does not stop the runtime; pair it with `stop()` + - [`start()`](../../reference/Classes/Wallet.md#kaspa.Wallet.start) must precede [`connect()`](../../reference/Classes/Wallet.md#kaspa.Wallet.connect), [`wallet_create()`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_create), and [`wallet_open()`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_open). + - [`wallet_create()`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_create) / [`wallet_open()`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_open) may run before or after + [`connect()`](../../reference/Classes/Wallet.md#kaspa.Wallet.connect), but [`accounts_activate()`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_activate) requires the wRPC client to + be connected and the wallet to be synced (see [Sync State](sync-state.md)). + - [`set_network_id()`](../../reference/Classes/Wallet.md#kaspa.Wallet.set_network_id) raises if the wRPC client is currently + connected — [`disconnect()`](../../reference/Classes/Wallet.md#kaspa.Wallet.disconnect) first, change the network, then + [`connect()`](../../reference/Classes/Wallet.md#kaspa.Wallet.connect) again. + - [`wallet_close()`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_close) does not stop the runtime; pair it with [`stop()`](../../reference/Classes/Wallet.md#kaspa.Wallet.stop) on shutdown. ## Where to next -- [Sync State](sync-state.md) — node IBD vs. processor readiness. - [Wallet Files](wallet-files.md) — enumerate, export, import, rename, change secret. - [Private Keys](private-keys.md) — the next step after creating a wallet. - [Accounts](accounts.md) — derive accounts from stored key data. -- [Architecture](architecture.md) — what `start` / `connect` / - `activate` actually wire up. +- [Sync State](sync-state.md) — node IBD vs. processor readiness. +- [Errors](errors.md) — common exceptions and their fixes. diff --git a/docs/learn/wallet/overview.md b/docs/learn/wallet/overview.md index 4ba2e2b1..827a1f5f 100644 --- a/docs/learn/wallet/overview.md +++ b/docs/learn/wallet/overview.md @@ -1,12 +1,8 @@ # Wallet The [`Wallet`](../../reference/Classes/Wallet.md) class is the SDK's -high-level managed wallet. It layers encrypted on-disk storage, -multi-account management, an event bus, and built-in send / transfer / -sweep flows on top of the primitives in -[Wallet SDK](../wallet-sdk/overview.md). - -Features: +high-level managed wallet. It leverages primitives in +[Wallet SDK](../wallet-sdk/overview.md) (and other rusty-kaspa components) to provide the following features: - Persistent encrypted on-disk storage for keys and account metadata - Multi-account management across BIP32 and keypair accounts @@ -15,58 +11,78 @@ Features: - Address derivation and discovery - Transaction history tracking -## A wallet, end to end +## Runnable example + +Generates a fresh testnet mnemonic, opens a new wallet file, derives a +BIP32 account, waits for sync, prints the receive address, and tears +down cleanly. No external funds required to run. ```python import asyncio -from kaspa import PrvKeyDataVariantKind, Resolver, Wallet +from kaspa import ( + Mnemonic, + PrvKeyDataVariantKind, + Resolver, + Wallet, +) async def main(): wallet = Wallet(network_id="testnet-10", resolver=Resolver()) await wallet.start() - await wallet.connect() + await wallet.connect(strategy="fallback", timeout_duration=5_000) + + while not wallet.is_synced: + await asyncio.sleep(0.5) await wallet.wallet_create( wallet_secret="example-secret", filename="demo", title="demo", ) + + mnemonic = Mnemonic.random() prv_key_id = await wallet.prv_key_data_create( wallet_secret="example-secret", - secret="", + secret=mnemonic.phrase, kind=PrvKeyDataVariantKind.Mnemonic, ) - descriptor = await wallet.accounts_create_bip32( + account = await wallet.accounts_create_bip32( wallet_secret="example-secret", prv_key_data_id=prv_key_id, account_index=0, ) - await wallet.accounts_activate([descriptor.account_id]) + await wallet.accounts_activate([account.account_id]) + + print("receive address:", account.receive_address) await wallet.wallet_close() + await wallet.disconnect() await wallet.stop() asyncio.run(main()) ``` -This script creates a wallet file on disk, derives a BIP32 account from a -mnemonic, and activates it. Re-running raises -`WalletAlreadyExistsError` unless you switch to `wallet_open` — see -[Lifecycle](lifecycle.md#open-a-wallet-file). +Re-running raises [`WalletAlreadyExistsError`](../../reference/Exceptions/WalletAlreadyExistsError.md) on [`wallet_create`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_create) — +switch to [`wallet_open`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_open), or pass `overwrite_wallet_storage=True`. See +[Lifecycle](lifecycle.md). + +For a full send / sweep / history flow, see +[`examples/wallet/transactions.py`](https://github.com/kaspanet/kaspa-python-sdk/blob/main/examples/wallet/transactions.py). ## How this section is laid out -- [Architecture](architecture.md) — `Wallet` / `UtxoProcessor` / - `UtxoContext` and how notifications flow. -- [Lifecycle](lifecycle.md) — the full state machine: construct, - start, connect, open, activate, close, stop. -- [Sync State](sync-state.md) — node IBD vs. processor readiness. +- [Architecture](architecture.md) — [`Wallet`](../../reference/Classes/Wallet.md) / [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) / + [`UtxoContext`](../../reference/Classes/UtxoContext.md) and how notifications flow. +- [Lifecycle](lifecycle.md) — the state machine and ordering rules. - [Wallet Files](wallet-files.md) — enumerate, export, import, rename, change secret. -- [Private Keys](private-keys.md), [Accounts](accounts.md), - [Addresses](addresses.md), [Keypair Accounts](keypair.md) — - populating the wallet. +- [Private Keys](private-keys.md) and [Accounts](accounts.md) — + populating the wallet (BIP32 and keypair). +- [Addresses](addresses.md) — derive and inspect addresses. - [Send Transaction](send-transaction.md), [Sweep Funds](sweep.md) — - outgoing flows. -- [Transaction History](transaction-history.md) — events and history - APIs. + outgoing flows, including the fee model and UTXO maturity. +- [Sync State](sync-state.md) — node IBD vs. processor readiness. +- [Events](events.md) — live event subscriptions. +- [Transaction History](transaction-history.md) — stored records and + annotation APIs. +- [Errors](errors.md) — common exceptions and their fixes. diff --git a/docs/learn/wallet/private-keys.md b/docs/learn/wallet/private-keys.md index 6b1a0fb7..728e3b5b 100644 --- a/docs/learn/wallet/private-keys.md +++ b/docs/learn/wallet/private-keys.md @@ -8,16 +8,16 @@ entries; each account references exactly one by ## Variants [`PrvKeyDataVariantKind`](../../reference/Enums/PrvKeyDataVariantKind.md) -selects the format of `secret` passed to `prv_key_data_create`. The +selects the format of `secret` passed to [`prv_key_data_create`](../../reference/Classes/Wallet.md#kaspa.Wallet.prv_key_data_create). The enum exposes four variants, but only two are accepted by the upstream wallet today: | Variant | `secret` format | Typical source | Status | | --- | --- | --- | --- | -| `Mnemonic` | BIP-39 phrase (12 or 24 words) | New wallets, `Mnemonic.random(...)` | Supported | +| `Mnemonic` | BIP-39 phrase (12 or 24 words) | New wallets, [`Mnemonic.random(...)`](../../reference/Classes/Mnemonic.md) | Supported | | `SecretKey` | 64-char hex secp256k1 key | Single-key (keypair) accounts | Supported | -| `Bip39Seed` | Hex-encoded BIP-39 seed | Pre-derived seeds from another tool | **Not supported upstream** — `prv_key_data_create` raises | -| `ExtendedPrivateKey` | xprv string | Migrating an existing HD wallet | **Not supported upstream** — `prv_key_data_create` raises | +| `Bip39Seed` | Hex-encoded BIP-39 seed | Pre-derived seeds from another tool | **Not supported upstream** — [`prv_key_data_create`](../../reference/Classes/Wallet.md#kaspa.Wallet.prv_key_data_create) raises | +| `ExtendedPrivateKey` | xprv string | Migrating an existing HD wallet | **Not supported upstream** — [`prv_key_data_create`](../../reference/Classes/Wallet.md#kaspa.Wallet.prv_key_data_create) raises | The two unsupported variants fall through to a `_` arm in `kaspa-wallet-core`'s `create_prv_key_data` and surface as @@ -28,9 +28,9 @@ Use `Mnemonic` for HD wallets and `SecretKey` for single-key accounts. | Method | Purpose | | --- | --- | -| `prv_key_data_create(...)` | Encrypt and store a new entry; returns its `PrvKeyDataId`. | -| `prv_key_data_enumerate()` | List `PrvKeyDataInfo` for every stored entry. | -| `prv_key_data_get(secret, id)` | Fetch metadata for a single entry. | +| [`prv_key_data_create(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.prv_key_data_create) | Encrypt and store a new entry; returns its [`PrvKeyDataId`](../../reference/Classes/PrvKeyDataId.md). | +| [`prv_key_data_enumerate()`](../../reference/Classes/Wallet.md#kaspa.Wallet.prv_key_data_enumerate) | List [`PrvKeyDataInfo`](../../reference/Classes/PrvKeyDataInfo.md) for every stored entry. | +| [`prv_key_data_get(secret, id)`](../../reference/Classes/Wallet.md#kaspa.Wallet.prv_key_data_get) | Fetch metadata for a single entry. | The wallet must be open. The actual secret never leaves the wallet — only its metadata is returned. @@ -41,7 +41,7 @@ only its metadata is returned. from kaspa import PrvKeyDataVariantKind prv_key_id = await wallet.prv_key_data_create( - wallet_secret="example-secret", + wallet_secret=wallet_secret, secret="", kind=PrvKeyDataVariantKind.Mnemonic, payment_secret=None, # optional second factor @@ -49,25 +49,28 @@ prv_key_id = await wallet.prv_key_data_create( ) ``` +Returns a [`PrvKeyDataId`](../../reference/Classes/PrvKeyDataId.md). + `payment_secret` layers a second password on top of `wallet_secret`. Every operation that decrypts this entry (account creation, signing, export) must supply it. Use `None` for single-password wallets. -## Enumerate & inspect +## Enumerate and inspect ```python for info in await wallet.prv_key_data_enumerate(): print(info.id, info.name, info.is_encrypted) ``` -`PrvKeyDataInfo` exposes: +Returns `list[PrvKeyDataInfo]`. +[`PrvKeyDataInfo`](../../reference/Classes/PrvKeyDataInfo.md) exposes: - `id: PrvKeyDataId` — stable identifier for account creation. - `name: str | None` — the label set at creation time. - `is_encrypted: bool` — `True` if a `payment_secret` is required. -`prv_key_data_get(wallet_secret, id)` returns the same metadata for one -entry, raising if the id is unknown. +[`prv_key_data_get(wallet_secret, id)`](../../reference/Classes/Wallet.md#kaspa.Wallet.prv_key_data_get) returns the same metadata for +one entry, raising if the id is unknown. ## Using a private key data entry @@ -76,7 +79,7 @@ private key data entry to the accounts derived from it: ```python descriptor = await wallet.accounts_create_bip32( - wallet_secret="example-secret", + wallet_secret=wallet_secret, prv_key_data_id=prv_key_id, account_index=0, ) @@ -84,13 +87,9 @@ descriptor = await wallet.accounts_create_bip32( A single private key data entry can back many accounts — common for BIP32 wallets where multiple account indices share one mnemonic. See -[Accounts](accounts.md). +[Accounts](accounts.md) and [`accounts_create_bip32`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_create_bip32). ## Where to next -- [Accounts](accounts.md) — derive BIP32 accounts from a private key - data entry. -- [Keypair Accounts](keypair.md) — single-key accounts from - `SecretKey`-variant private key data entries. -- [Wallet Recovery](../../guides/wallet-recovery.md) — BIP-44 scan for - accounts already used under a mnemonic. +- [Accounts](accounts.md) — derive BIP32 and keypair accounts from a + private key data entry. diff --git a/docs/learn/wallet/send-transaction.md b/docs/learn/wallet/send-transaction.md index 0ad367be..71fe69a4 100644 --- a/docs/learn/wallet/send-transaction.md +++ b/docs/learn/wallet/send-transaction.md @@ -7,24 +7,26 @@ account, and `wallet.is_synced == True` — see ## Surface -| Method | Purpose | -| --- | --- | -| `accounts_estimate(...)` | Dry-run a send; returns a `GeneratorSummary` without submitting. | -| `accounts_send(...)` | Sign and submit a send. Returns the same `GeneratorSummary` after submission. | -| `accounts_transfer(...)` | Internal transfer between two accounts in the same wallet. | -| `accounts_get_utxos(...)` | Snapshot of an account's tracked UTXOs (post-sync). | -| `fee_rate_estimate()` | Current low / normal / priority fee rates from the node. | -| `fee_rate_poller_enable(seconds)` / `_disable()` | Background fee-rate refresh. | +| Method | Returns | Purpose | +| --- | --- | --- | +| [`accounts_estimate(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_estimate) | [`GeneratorSummary`](../../reference/Classes/GeneratorSummary.md) | Dry-run a send without submitting. | +| [`accounts_send(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_send) | [`GeneratorSummary`](../../reference/Classes/GeneratorSummary.md) | Sign and submit a send. | +| [`accounts_transfer(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_transfer) | [`GeneratorSummary`](../../reference/Classes/GeneratorSummary.md) | Internal transfer between two accounts in the same wallet. | +| [`accounts_get_utxos(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_get_utxos) | `list[dict]` | Snapshot of an account's tracked UTXOs (post-sync). | +| [`fee_rate_estimate()`](../../reference/Classes/Wallet.md#kaspa.Wallet.fee_rate_estimate) | `dict` | Current low / normal / priority fee rates from the node. | +| [`fee_rate_poller_enable(seconds)`](../../reference/Classes/Wallet.md#kaspa.Wallet.fee_rate_poller_enable) / [`_disable()`](../../reference/Classes/Wallet.md#kaspa.Wallet.fee_rate_poller_disable) | — | Background fee-rate refresh. | For sweeping (consolidating every UTXO), see [Sweep Funds](sweep.md). ## Send a single output +[`accounts_send`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_send) takes a [`Fees`](../../reference/Classes/Fees.md) object with a [`FeeSource`](../../reference/Enums/FeeSource.md) and a list of [`PaymentOutput`](../../reference/Classes/PaymentOutput.md): + ```python from kaspa import Fees, FeeSource, PaymentOutput result = await wallet.accounts_send( - wallet_secret="example-secret", + wallet_secret=wallet_secret, account_id=account.account_id, priority_fee_sompi=Fees(0, FeeSource.SenderPays), destination=[PaymentOutput("kaspatest:...", 100_000_000)], # 1 KAS @@ -32,6 +34,10 @@ result = await wallet.accounts_send( print(result.final_transaction_id, result.fees, result.final_amount) ``` +[`Fees(0, ...)`](../../reference/Classes/Fees.md) is fine here: with priority `0` the wallet still pays +the network minimum (mass × `fee_rate`). See +[Fee model](#fee-model) below. + ## Multi-output send A single `destination` list of N outputs becomes one transaction with @@ -40,7 +46,7 @@ N + 1 outputs (the +1 is the change return). ```python outputs = [PaymentOutput(addr, 100_000_000) for addr in addresses] result = await wallet.accounts_send( - wallet_secret=secret, + wallet_secret=wallet_secret, account_id=account.account_id, priority_fee_sompi=Fees(0, FeeSource.SenderPays), destination=outputs, @@ -58,14 +64,47 @@ estimate = await wallet.accounts_estimate( print(estimate.fees, estimate.final_amount, estimate.utxos) ``` -`accounts_estimate` and `accounts_send` take the same arguments. -Estimating first is cheap and surfaces fees and UTXO selection before -signing. +[`accounts_estimate`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_estimate) and [`accounts_send`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_send) take the same arguments. +Estimating first surfaces fees and UTXO selection before signing. + +## `GeneratorSummary` fields + +Both [`accounts_estimate`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_estimate) and [`accounts_send`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_send) return a +[`GeneratorSummary`](../../reference/Classes/GeneratorSummary.md): + +| Field | Type | Meaning | +| --- | --- | --- | +| `network_id` | `str` | Network the summary applies to. | +| `network_type` | `str` | Network type identifier. | +| `utxos` | `int` | Total inputs consumed across the chain. | +| `mass` | `int` | Total transaction mass. | +| `fees` | `int` | Aggregate fee paid in sompi (network + priority). | +| `transactions` | `int` | Number of transactions generated. | +| `stages` | `int` | Number of compounding stages. | +| `final_amount` | `int \| None` | Final output amount in sompi (None if not yet generated). | +| `final_transaction_id` | `str \| None` | ID of the final transaction in the chain. | + +For most sends the chain is one transaction; large sweeps produce +multi-transaction chains (see [Sweep Funds](sweep.md)). + +## Fee model + +A submitted transaction pays: + +``` +total_fee = network_fee + priority_fee +network_fee = mass * fee_rate +``` + +Both arguments are total sompi values, never per-gram on the Python +side: -## Fees +| Argument | Type | What it controls | +| --- | --- | --- | +| `priority_fee_sompi` | [`Fees`](../../reference/Classes/Fees.md)`(amount_sompi, source)` | A flat top-up in sompi above the network minimum. `0` is a valid (minimum-priority) value. | +| `fee_rate` | `float \| None` | Network rate in sompi per gram. `None` resolves to the node's suggested rate. | -`priority_fee_sompi` is a `Fees(amount, FeeSource)` (or equivalent -dict): +[`FeeSource`](../../reference/Enums/FeeSource.md) decides who absorbs the fee: - **`FeeSource.SenderPays`** — fee is added on top of the destination amount. Standard sends. @@ -73,17 +112,20 @@ dict): amount. Used to sweep an exact balance with no leftover change (see [Sweep Funds](sweep.md)). -`fee_rate` overrides the resolved sompi-per-gram rate. Leave it `None` -to use the network-suggested rate. See -[Mass & Fees](../transactions/mass-and-fees.md) for the underlying -model. +See [Mass & Fees](../transactions/mass-and-fees.md) for the underlying +mass model. ```python rates = await wallet.fee_rate_estimate() -# {"low": ..., "normal": ..., "priority": ...} +# {"priority": {"feerate": ..., "seconds": ...}, +# "normal": {"feerate": ..., "seconds": ...}, +# "low": {"feerate": ..., "seconds": ...}} ``` -For latency-sensitive flows, run a background poller: +[`fee_rate_estimate`](../../reference/Classes/Wallet.md#kaspa.Wallet.fee_rate_estimate) returns a one-shot dict. + +For latency-sensitive flows, run a background poller via [`fee_rate_poller_enable`](../../reference/Classes/Wallet.md#kaspa.Wallet.fee_rate_poller_enable) / +[`fee_rate_poller_disable`](../../reference/Classes/Wallet.md#kaspa.Wallet.fee_rate_poller_disable): ```python wallet.fee_rate_poller_enable(15) # refresh every 15 seconds @@ -98,34 +140,72 @@ immediately on transaction acceptance — no maturity wait: ```python await wallet.accounts_transfer( - wallet_secret=secret, + wallet_secret=wallet_secret, source_account_id=src.account_id, destination_account_id=dst.account_id, transfer_amount_sompi=500_000_000, ) ``` -Use `accounts_transfer` for in-wallet movement; use `accounts_send` for -external addresses. +Use [`accounts_transfer`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_transfer) for in-wallet movement; use [`accounts_send`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_send) +for external addresses. + +## UTXO maturity + +Every UTXO the +[`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) sees moves +through three states: + +- **Pending** — seen, but confirmation depth is below the maturity + threshold. Counted in [`Balance.pending`](../../reference/Classes/Balance.md). *Not* spendable. +- **Mature** — confirmed deeply enough to spend. Counted in + [`Balance.mature`](../../reference/Classes/Balance.md). Returned by [`accounts_get_utxos`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_get_utxos). Selectable. +- **Outgoing** — locked because the wallet just spent it in a + transaction it generated. Counted in [`Balance.outgoing`](../../reference/Classes/Balance.md) until the + spend matures or is reorged out. + +The default thresholds (mainnet and testnet-10): + +| | DAA score depth | +| --- | --- | +| User transactions → Mature | 100 | +| Coinbase transactions → Mature | 1000 | +| Coinbase stasis (UTXOs hidden) | 500 | + +Override per network with +[`UtxoProcessor.set_user_transaction_maturity_period_daa(network_id, value)`](../../reference/Classes/UtxoProcessor.md) +and the matching coinbase setter; defaults usually want to stay. + +### Why `accounts_get_utxos` can return `[]` + +[`accounts_get_utxos`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_get_utxos) reads the in-memory [`UtxoContext`](../../reference/Classes/UtxoContext.md). It returns +`[]` when: + +1. The wallet isn't synced yet — see [Sync State](sync-state.md). +2. The account hasn't been activated. +3. No notification for a funding tx has reached the processor yet. + +Each UTXO is a `dict` — read amounts as `u["amount"]`, not +`u.amount`. -## Waiting for funds and confirmations +### Waiting for confirmations Sends submit immediately, but spent UTXOs need to mature before the -next `accounts_send` will see them. Two correct waits, both via -[Transaction History](transaction-history.md): +next [`accounts_send`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_send) will see them. Two correct waits, both via +[Events](events.md): - **Pending** — fires when a UTXO lands but isn't yet spendable. Useful for UI. - **Maturity** — fires when a UTXO crosses the maturity depth and is spendable. The right gate for "send → wait → send again" flows. -Polling `accounts_get_utxos` works for one-shot scripts; a `Maturity` +Polling [`accounts_get_utxos`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_get_utxos) works for one-shot scripts; a `Maturity` listener is the production pattern. ## Where to next - [Sweep Funds](sweep.md) — consolidating every UTXO. -- [Transaction History](transaction-history.md) — `Pending`, `Maturity`, - and listener registration. +- [Events](events.md) — `Pending`, `Maturity`, and listener + registration. - [Wallet SDK → Transaction Generator](../wallet-sdk/tx-generator.md) — the lower-level primitive `accounts_send` is built on. diff --git a/docs/learn/wallet/sweep.md b/docs/learn/wallet/sweep.md index 84f280ab..c691f0ce 100644 --- a/docs/learn/wallet/sweep.md +++ b/docs/learn/wallet/sweep.md @@ -3,6 +3,9 @@ A sweep consolidates every UTXO in an account into one address. Two patterns; the difference is whether you want any leftover change. +Both patterns require a synced wallet — [`accounts_get_utxos`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_get_utxos) returns +`[]` until then. See [Sync State](sync-state.md). + ## Pattern 1: sweep to your own change address Omit `destination` entirely. The wallet routes the full sweepable @@ -14,20 +17,22 @@ mature output before a high-volume send flow. from kaspa import Fees, FeeSource await wallet.accounts_send( - wallet_secret=secret, + wallet_secret=wallet_secret, account_id=account.account_id, priority_fee_sompi=Fees(0, FeeSource.SenderPays), destination=None, ) ``` -`SenderPays` works here because the change return absorbs the fee — -there's no external recipient to subtract from. +[`FeeSource.SenderPays`](../../reference/Enums/FeeSource.md) works here because the change return absorbs the fee — +there's no external recipient to subtract from. [`Fees(0, ...)`](../../reference/Classes/Fees.md) keeps +priority at the network minimum; raise it to bid for faster +inclusion. ## Pattern 2: sweep an exact balance to a fresh address To leave the account at zero with the destination receiving *the exact -aggregate balance minus fees*, use `ReceiverPays`: +aggregate balance minus fees*, use [`FeeSource.ReceiverPays`](../../reference/Enums/FeeSource.md): ```python from kaspa import Fees, FeeSource, PaymentOutput @@ -36,39 +41,34 @@ utxos = await wallet.accounts_get_utxos(account_id=account.account_id) total = sum(u["amount"] for u in utxos) await wallet.accounts_send( - wallet_secret=secret, + wallet_secret=wallet_secret, account_id=account.account_id, priority_fee_sompi=Fees(0, FeeSource.ReceiverPays), destination=[PaymentOutput(sweep_address, total)], ) ``` -The destination amount is the *gross* balance; `ReceiverPays` deducts +[`accounts_get_utxos`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_get_utxos) returns `list[dict]`; each UTXO's amount is +`u["amount"]` (sompi). + +The destination amount is the *gross* balance; [`FeeSource.ReceiverPays`](../../reference/Enums/FeeSource.md) deducts the network fee from it before broadcasting. The result: no change output, no dust. -## Which to use - -| You want… | Use | -| --- | --- | -| To consolidate UTXOs and keep them in this account | Pattern 1 (no `destination`) | -| To move every sompi to an external address, leaving zero | Pattern 2 (`ReceiverPays`) | -| To sweep within the same wallet to a different account | [`accounts_transfer`](send-transaction.md#internal-transfers), not a sweep | - ## Big sweeps come back as multiple transactions If the input set is too large for one transaction's mass budget, the -underlying [`Generator`](../wallet-sdk/tx-generator.md) produces a +underlying [`Generator`](../../reference/Classes/Generator.md) (see [Transaction Generator](../wallet-sdk/tx-generator.md)) produces a series of transactions: each consolidates some UTXOs into a single intermediate output, and the final transaction sends the aggregate to -the destination. `accounts_send` returns the final `GeneratorSummary`. -Watch the [`Maturity` event](transaction-history.md) to know when the -chain has caught up — only the final output is what you'd hand off -downstream. +the destination. [`accounts_send`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_send) returns the final +[`GeneratorSummary`](../../reference/Classes/GeneratorSummary.md); +`summary.transactions` reports how many were submitted. Watch the +[`Maturity` event](events.md) to know when the chain has +caught up — only the final output is what you'd hand off downstream. ## Where to next -- [Send Transaction](send-transaction.md) — non-sweep sends and fee - modes. -- [Transaction History](transaction-history.md) — gating "wait until - swept" on `Maturity`. +- [Send Transaction](send-transaction.md) — non-sweep sends and the + fee model. +- [Events](events.md) — gating "wait until swept" on `Maturity`. diff --git a/docs/learn/wallet/sync-state.md b/docs/learn/wallet/sync-state.md index 2e090ded..714ec783 100644 --- a/docs/learn/wallet/sync-state.md +++ b/docs/learn/wallet/sync-state.md @@ -9,63 +9,62 @@ finished registering subscriptions and confirmed the node is in a usable state? -`accounts_*` calls — balances, UTXO snapshots, sends — wait on (2), +`accounts_*` calls (balances, UTXO snapshots, sends) wait on (2), which is itself gated on (1). Treating them as one gives the right -answer most of the time but blurs *what* the wallet is actually -waiting for. This page splits them. +answer most of the time but obscurs *what* the wallet is waiting for. ## Node sync state -A node that's still in IBD doesn't yet have all blocks/UTXOs, so it -can't answer wallet RPC calls authoritatively. Two surfaces report -this: - -- **`ServerStatus`** event — emitted once after `connect()`, right - after the initial `get_server_info` handshake. Payload: - - ```python - { - "type": "server-status", - "data": { - "networkId": "testnet-10", - "serverVersion": "0.x.y", - "isSynced": True, # node-side flag - "url": "wss://node:17110", - }, - } - ``` +A node still in IBD doesn't have all blocks/UTXOs and can't answer +wallet RPC calls authoritatively. Two surfaces report this: -- **`SyncState`** event with an *IBD substate* — while the node is - still catching up, the wallet derives progress from log lines the - node prints and re-publishes them as `SyncState` events. See +- **`ServerStatus`** [event](events.md) — emitted once after [`connect()`](../../reference/Classes/Wallet.md#kaspa.Wallet.connect), right + after the initial `get_server_info` handshake. Payload includes + `isSynced` (the node-side flag), `networkId`, `serverVersion`, and + `url`. +- **`SyncState`** [event](events.md) with an *IBD substate* — see [Reading SyncState payloads](#reading-syncstate-payloads). If the node is missing its UTXO index entirely, the processor -short-circuits with a **`UtxoIndexNotEnabled`** event and refuses to +short-circuits with a **`UtxoIndexNotEnabled`** [event](events.md) and refuses to proceed — the only fix is to point at a node that has it. ## Processor sync state -Once the node reports synced, the wallet's `UtxoProcessor` does its -own setup: registers UTXO/virtual-chain notification listeners, marks -itself ready, and starts forwarding UTXO changes to per-account -[`UtxoContext`](../../reference/Classes/UtxoContext.md)s. - -- **`wallet.is_synced`** — `True` once the processor finishes that - setup. This is the flag every `accounts_*` call effectively waits - on. Polling it (with `await asyncio.sleep(0.5)`) is fine for - scripts. -- **`SyncState`** event with a *terminal substate* — `Synced` when +After [`connect()`](../../reference/Classes/Wallet.md#kaspa.Wallet.connect), the wallet's [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) runs a short handshake +against the node: + +1. One `get_server_info` round trip — validates RPC API version, + network-ID match, and that the node has its UTXO index enabled + (otherwise it emits `UtxoIndexNotEnabled` and stops). Reads the + node's `is_synced` flag and current `virtualDaaScore`. Emits + `ServerStatus`. +2. Registers a single wRPC listener and subscribes to + `VirtualDaaScoreChanged`. No `UtxosChanged` subscription is opened + at this stage — the processor doesn't know about any addresses yet. +3. Tracks the node's IBD progress (the `proof` / `headers` / `blocks` + / `utxo-sync` substates flow through here), then flips ready. + +Per-address `UtxosChanged` subscriptions and the initial UTXO seed +happen later, inside [`accounts_activate`](../../reference/Classes/Wallet.md#kaspa.Wallet.accounts_activate): each account's +[`UtxoContext`](../../reference/Classes/UtxoContext.md) issues a single +`get_utxos_by_addresses` call to populate its mature set and starts a +`UtxosChanged` subscription scoped to its addresses. After that the +context is updated purely from streamed notifications — there's no +periodic re-poll. + +- **[`wallet.is_synced`](../../reference/Classes/Wallet.md#kaspa.Wallet.is_synced)** — `True` once the handshake above completes. + This is the flag every `accounts_*` call effectively waits on. +- **`SyncState`** [event](events.md) with a *terminal substate* — `Synced` when the processor flips ready, `NotSynced` when it falls back (e.g. on reconnect). - **`UtxoProcStart`** / **`UtxoProcStop`** — fire when the processor - itself starts and stops (lifecycle, not sync per se), but useful as - bookends in event logs. + itself starts and stops. Useful as bookends in event logs. -The relationship is one-way: the processor cannot be synced if the -node isn't. So `wallet.is_synced` is the single condition you -actually need to gate work on — the node-level signals are useful for -*reporting progress*, not for unblocking calls. +The processor cannot be synced if the node isn't, so [`wallet.is_synced`](../../reference/Classes/Wallet.md#kaspa.Wallet.is_synced) +is the single condition you actually need to gate work on. The +node-level signals are useful for *reporting progress*, not for +unblocking calls. ## Reading SyncState payloads @@ -97,8 +96,7 @@ The substates fall into three groups: | Terminal | `not-synced` | *(none)* | Processor | | | `synced` | *(none)* | Processor | -The IBD substates make it easy to drive a progress bar without -parsing kaspad logs yourself: +The IBD substates make it easy to drive a progress bar: ```python def on_event(event): @@ -116,7 +114,9 @@ def on_event(event): ## A staged wait -If you want to surface node IBD separately from processor readiness: +To surface node IBD separately from processor readiness: + +Drive `node_synced` and `processor_ready` events with [`add_event_listener`](../../reference/Classes/Wallet.md#kaspa.Wallet.add_event_listener) and the [`WalletEventType`](../../reference/Enums/WalletEventType.md) enum: ```python import asyncio @@ -137,33 +137,24 @@ wallet.add_event_listener(WalletEventType.All, on_event) await wallet.start() await wallet.connect() - -await node_synced.wait() # node finished IBD -await processor_ready.wait() # processor registered + ready +await node_synced.wait() +await processor_ready.wait() assert wallet.is_synced ``` -For most scripts, the simpler form is fine: - -```python -await wallet.start() -await wallet.connect() -while not wallet.is_synced: - await asyncio.sleep(0.5) -``` +For most scripts the polling form in +[Lifecycle → Sync gate](lifecycle.md#sync-gate) is enough. ## Reconnects and `Disconnect` -A `Disconnect` event flips `wallet.is_synced` back to `False`; the +A `Disconnect` [event](events.md) flips [`wallet.is_synced`](../../reference/Classes/Wallet.md#kaspa.Wallet.is_synced) back to `False`; the processor re-runs its handshake on the next `Connect` and re-emits a fresh `ServerStatus` plus `SyncState` chain. Long-running listeners should treat the gate as *re-arming*, not one-shot — gate every -`accounts_*` batch on `is_synced`, not on a once-set flag. +`accounts_*` batch on [`is_synced`](../../reference/Classes/Wallet.md#kaspa.Wallet.is_synced), not on a once-set flag. ## Where to next -- [Lifecycle](lifecycle.md#sync-gate) — the polling form of the sync gate, in context of the boot sequence. -- [Architecture](architecture.md) — where the processor and RPC client - sit in the component graph. -- [Transaction History](transaction-history.md) — the full event - taxonomy these events live in. +- [Architecture](architecture.md) — where the processor and RPC + client sit in the component graph. +- [Events](events.md) — the full event taxonomy these signals live in. diff --git a/docs/learn/wallet/transaction-history.md b/docs/learn/wallet/transaction-history.md index 5b662f6e..bc1fbce5 100644 --- a/docs/learn/wallet/transaction-history.md +++ b/docs/learn/wallet/transaction-history.md @@ -1,137 +1,76 @@ # Transaction History -The wallet emits events for every state change the node pushes -through. Register Python callbacks on the wallet; its event -multiplexer forwards relevant events to them, and you react — update -a UI, trigger the next send, log a maturity, handle a reorg. The -history APIs (`transactions_data_get` and friends) cover the -"what happened in this account?" question. +The wallet stores a record of every transaction that touched each +account, with optional user-supplied note and metadata strings. -## Listener API +For the live event stream (`Balance`, `Pending`, `Maturity`, etc.), +see [Events](events.md). -```python -def add_event_listener(event, callback, *args, **kwargs) -> None -def remove_event_listener(event, callback=None) -> None -``` - -- `event` — a - [`WalletEventType`](../../reference/Enums/WalletEventType.md), its - kebab-case string name (`"balance"`, `"sync-state"`), or `"all"` / - `WalletEventType.All` for every event. -- `callback` — invoked as `callback(*args, event, **kwargs)`. Must be - a regular (synchronous) function; the dispatcher calls it inline and - does not await coroutines, so an `async def` callback's body never - runs. -- `args` / `kwargs` — forwarded verbatim to every invocation. Handy - for routing context (account id, channel) without closures. -- `remove_event_listener(event)` with no callback clears every - listener for that event. With `"all"` and no callback, clears every - listener globally. +## Fetching a window -## A minimal subscriber +[`transactions_data_get`](../../reference/Classes/Wallet.md#kaspa.Wallet.transactions_data_get) returns a paged window of records: ```python -from kaspa import Resolver, Wallet, WalletEventType - -def on_event(event): - print(event["type"], event.get("data")) - -wallet = Wallet(network_id="testnet-10", resolver=Resolver()) -wallet.add_event_listener(WalletEventType.All, on_event) - -await wallet.start() -await wallet.connect() -# ... events stream in for the rest of the session ... -``` - -Each event is a dict with at least a `type` key (the kebab-case kind -name, e.g. `"balance"`, `"sync-state"`, `"fee-rate"`) and an optional -`data` payload specific to that event. - -## Event taxonomy - -| Group | Events | -| --- | --- | -| Connection | `Connect`, `Disconnect`, `ServerStatus`, `UtxoIndexNotEnabled` | -| Wallet file | `WalletList`, `WalletStart`, `WalletHint`, `WalletOpen`, `WalletCreate`, `WalletReload`, `WalletClose`, `WalletError` | -| Key & account state | `PrvKeyDataCreate`, `AccountCreate`, `AccountActivation`, `AccountDeactivation`, `AccountSelection`, `AccountUpdate` | -| Sync & runtime | `SyncState`, `UtxoProcStart`, `UtxoProcStop`, `UtxoProcError`, `DaaScoreChange`, `Metrics`, `FeeRate` | -| UTXO movement | `Pending`, `Maturity`, `Reorg`, `Stasis`, `Discovery`, `Balance` | -| Catch-all | `All`, `Error` | - -The most common subscriptions: - -- **`SyncState`** — progress while the `UtxoProcessor` catches up. - Pair with `wallet.is_synced` — see [Sync State](sync-state.md) for - the substate payload shape. -- **`Balance`** — fires when a `UtxoContext` balance changes. The - right signal for live UI updates. -- **`Pending`** — a new UTXO landed for a tracked address but isn't - yet spendable. -- **`Maturity`** — a previously-pending UTXO crossed the maturity - depth and is now spendable. The strongest gate for "send-then-wait" - flows — don't trigger the next `accounts_send` on `Pending` alone. -- **`Reorg`** / **`Stasis`** — a UTXO was unwound or coinbase-locked. - Defensive code for high-value flows. -- **`AccountActivation`** / **`AccountDeactivation`** — react to - `accounts_activate` / `wallet_close`. - -## Targeted subscriptions - -```python -wallet.add_event_listener("balance", on_balance) -wallet.add_event_listener("maturity", on_maturity) -wallet.add_event_listener(WalletEventType.SyncState, on_sync) +data = await wallet.transactions_data_get( + account_id=account.account_id, + network_id="testnet-10", + start=0, + end=20, +) ``` -To pass context to a generic callback: +- `account_id` — the account whose history you want. +- `network_id` — history is stored per network; pass the same + identifier the wallet is connected to. +- `start` / `end` — half-open paging window into the stored history + (newest first). +- `filter` *(optional)* — list of + [`TransactionKind`](../../reference/Enums/TransactionKind.md) values + (or kebab strings) to keep; everything else is dropped. -```python -wallet.add_event_listener("balance", on_change, account.account_id, label="primary") -# callback receives: on_change(account.account_id, event, label="primary") -``` +Returns a dict containing the matching `TransactionRecord`s. Each +record carries `id`, `unixtimeMsec`, `value`, `binding`, +`blockDaaScore`, `network`, `data` (the per-kind transaction body), +and the optional `note` / `metadata` strings. -## History queries +## Annotating records -For the "what happened" view rather than the live stream: +Notes and metadata are free-form strings stored alongside the record +in the wallet file, not on chain. Update them with +[`transactions_replace_note`](../../reference/Classes/Wallet.md#kaspa.Wallet.transactions_replace_note) and +[`transactions_replace_metadata`](../../reference/Classes/Wallet.md#kaspa.Wallet.transactions_replace_metadata): ```python -data = await wallet.transactions_data_get( +await wallet.transactions_replace_note( account_id=account.account_id, network_id="testnet-10", - start=0, - end=20, -) -# Annotate / re-annotate: -await wallet.transactions_replace_note( - account.account_id, "testnet-10", tx_id, "rent", + transaction_id=tx_id, + note="rent", ) await wallet.transactions_replace_metadata( - account.account_id, "testnet-10", tx_id, '{"tag": "ops"}', + account_id=account.account_id, + network_id="testnet-10", + transaction_id=tx_id, + metadata='{"tag": "ops"}', ) ``` -Note and metadata are free-form strings stored in the wallet file — -not on chain. If you want structured metadata, encode it yourself -(JSON, etc.). - -## Cleanup +Pass `None` (or omit) to clear the existing value. If you want +structured metadata, encode it yourself (JSON, msgpack, etc.) — the +wallet stores opaque strings. -Listeners outlive the wallet's open file but not its runtime. Pair a -permanent registration with an explicit removal on shutdown, or use -`"all"` to clear in one call: +## Stored records vs. live events -```python -wallet.remove_event_listener(WalletEventType.All) -await wallet.stop() -``` +The history APIs read what's already on disk; they don't reach out to +the node. New records land on disk as the +[`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) processes +notifications, so for "react when something happens" flows use the +[event](events.md) surface and only call [`transactions_data_get`](../../reference/Classes/Wallet.md#kaspa.Wallet.transactions_data_get) when +you need the persisted view (UI history pane, audit, export). ## Where to next -- [Send Transaction](send-transaction.md) — `Maturity` as the right - wait condition. -- [Architecture](architecture.md) — what's actually generating these - events. -- [Lifecycle](lifecycle.md) — when each event group fires. -- [Wallet SDK → UTXO Processor](../wallet-sdk/utxo-processor.md) — - the lower-level event surface beneath the managed wallet. +- [Events](events.md) — live `Balance` / `Pending` / `Maturity` etc. +- [Send Transaction](send-transaction.md) — outgoing flows that + populate this history. +- [Wallet Files](wallet-files.md) — where the records are persisted. diff --git a/docs/learn/wallet/utxo-maturity.md b/docs/learn/wallet/utxo-maturity.md deleted file mode 100644 index fa68fb07..00000000 --- a/docs/learn/wallet/utxo-maturity.md +++ /dev/null @@ -1,37 +0,0 @@ -# UTXO Maturity - -Every UTXO the [`UtxoProcessor`](../../reference/Classes/UtxoProcessor.md) -sees moves through three states: - -- **Pending** — seen, but confirmation depth is below the maturity - threshold. Counted in `Balance.pending`. *Not* spendable. -- **Mature** — confirmed deeply enough to spend. Counted in - `Balance.mature`. Returned by `accounts_get_utxos`. Selectable. -- **Outgoing** — locked because the wallet just spent it in a - transaction it generated. Counted in `Balance.outgoing` until the - spend matures or is reorged out. - -[Send Transaction](send-transaction.md) waits on `Maturity` for this -reason: a `Pending` UTXO is real, but the next `accounts_send` won't -see it as spendable. - -## Why `accounts_get_utxos` can return `[]` - -`accounts_get_utxos` reads the in-memory `UtxoContext`. It returns -`[]` when: - -1. The wallet isn't synced yet — see [Sync State](sync-state.md). -2. The account hasn't been activated. -3. No notification for a funding tx has reached the processor yet. - -None of these mean "the address has no funds" — they mean "the wallet -hasn't been told yet." Listen for `Maturity` instead of polling. - -## Where to next - -- [Sync State](sync-state.md) — the gate that controls when UTXOs - start flowing. -- [Send Transaction](send-transaction.md) — `Maturity` as the gate for - send-then-wait flows. -- [Transaction History](transaction-history.md) — `Pending`, - `Maturity`, `Reorg`, and `Stasis` events. diff --git a/docs/learn/wallet/wallet-files.md b/docs/learn/wallet/wallet-files.md index ff91dc2a..538d7f40 100644 --- a/docs/learn/wallet/wallet-files.md +++ b/docs/learn/wallet/wallet-files.md @@ -9,16 +9,16 @@ password. ## Surface -| Method | Purpose | -| --- | --- | -| `wallet_enumerate()` | List every wallet file in the store. | -| `wallet_export(...)` | Dump the encrypted payload as hex. | -| `wallet_import(...)` | Materialise a previously exported payload as a new file. | -| `wallet_change_secret(...)` | Re-encrypt the open file with a new password. | -| `wallet_rename(...)` | Update the title (and/or filename — see warning below). | - -All methods require `start()`; none require a wRPC connection. -`wallet_change_secret` and `wallet_rename` operate on the currently +| Method | Returns | Purpose | +| --- | --- | --- | +| [`wallet_enumerate()`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_enumerate) | `list[`[`WalletDescriptor`](../../reference/Classes/WalletDescriptor.md)`]` | List every wallet file in the store. | +| [`wallet_export(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_export) | `str` (hex) | Dump the encrypted payload as hex. | +| [`wallet_import(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_import) | `dict` | Materialise a previously exported payload as a new file. | +| [`wallet_change_secret(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_change_secret) | — | Re-encrypt the open file with a new password. | +| [`wallet_rename(...)`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_rename) | — | Update the title (and/or filename — see warning below). | + +All methods require [`start()`](../../reference/Classes/Wallet.md#kaspa.Wallet.start); none require a wRPC connection. +[`wallet_change_secret`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_change_secret) and [`wallet_rename`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_rename) operate on the currently open file. ## Enumerate @@ -29,29 +29,36 @@ for d in descriptors: print(d.filename, d.title) ``` -Returns a `list[WalletDescriptor]`. Available before any wallet is -opened — useful for a wallet picker UI. +[`WalletDescriptor`](../../reference/Classes/WalletDescriptor.md) is a typed object — use attribute access. Available +before any wallet is opened, useful for a wallet picker UI. -## Export & import +## Export and import ```python hex_payload = await wallet.wallet_export( wallet_secret="example-secret", include_transactions=True, ) -# ...transfer hex_payload to another machine, then: +# ... transfer hex_payload to another machine, then: imported = await wallet.wallet_import( wallet_secret="example-secret", wallet_data=hex_payload, ) new_filename = imported["walletDescriptor"]["filename"] -await wallet.wallet_open("example-secret", True, new_filename) +await wallet.wallet_open( + wallet_secret="example-secret", + account_descriptors=True, + filename=new_filename, +) ``` +[`wallet_import`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_import) is the only file-API method that returns a dict — +read the new filename via `imported["walletDescriptor"]["filename"]`. + The exported payload is borsh-serialized and remains encrypted with `wallet_secret`; private key material never leaves memory in the -clear. `wallet_import` writes a new file in the store and returns its -descriptor — you still need to `wallet_open` it. +clear. [`wallet_import`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_import) writes a new file in the store and returns its +descriptor — you still need to [`wallet_open`](../../reference/Classes/Wallet.md#kaspa.Wallet.wallet_open) it. ## Change secret diff --git a/docs/llms.txt b/docs/llms.txt index 3afb641e..349c8951 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -21,8 +21,7 @@ Docs are versioned (base URL requires version path like `/latest/` or `/dev/`). ## Documentation - [API Reference](https://kaspanet.github.io/kaspa-python-sdk/latest/reference/): Full API documentation -- [Examples](https://kaspanet.github.io/kaspa-python-sdk/latest/getting-started/examples/): Code examples -- [Guides](https://kaspanet.github.io/kaspa-python-sdk/latest/guides/): Feature guides for RPC, transactions, addresses, etc. +- [Examples](https://kaspanet.github.io/kaspa-python-sdk/latest/examples/): Runnable example scripts on GitHub - [Source](https://github.com/kaspanet/kaspa-python-sdk): GitHub repository ## Optional diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 07f9217f..ea6043e0 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,5 +1,5 @@ -/* Style navigation section headers to stand out more */ -.md-nav--primary .md-nav__item--section > .md-nav__link { +/* Top-level section headers (e.g. tabs like LEARN) — uppercase teal */ +.md-nav--primary > .md-nav__list > .md-nav__item--section > .md-nav__link { font-weight: 700; color: var(--md-primary-fg-color); text-transform: uppercase; @@ -9,15 +9,39 @@ opacity: 0.8; } -/* Add a subtle top border to separate sections */ -.md-nav--primary .md-nav__item--section { +/* Top-level section dividers */ +.md-nav--primary > .md-nav__list > .md-nav__item--section { border-top: 1px solid var(--md-default-fg-color--lightest); padding-top: 0.6em; margin-top: 0.6em; } -/* Remove border from the first section */ -.md-nav--primary .md-nav__item--section:first-child { +.md-nav--primary > .md-nav__list > .md-nav__item--section:first-child { + border-top: none; + margin-top: 0; + padding-top: 0; +} + +/* Nested section headers (e.g. RPC, Wallet, Transactions inside Learn) — + bold + slightly muted, not uppercase, so hierarchy reads clearly */ +.md-nav--primary .md-nav .md-nav__item--section > .md-nav__link { + font-weight: 700; + color: var(--md-default-fg-color); + font-size: 0.75rem; + text-transform: none; + letter-spacing: 0; + margin-top: 0.8em; + opacity: 0.85; +} + +/* Lighter divider above nested sections */ +.md-nav--primary .md-nav .md-nav__item--section { + border-top: 1px dashed var(--md-default-fg-color--lightest); + padding-top: 0.4em; + margin-top: 0.4em; +} + +.md-nav--primary .md-nav .md-nav__item--section:first-child { border-top: none; margin-top: 0; padding-top: 0; diff --git a/mkdocs.yml b/mkdocs.yml index 6a0667ff..1780d046 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -111,15 +111,17 @@ extra: copyright: Copyright © 2022-2026 Kaspa developers nav: - - Home: index.md - Getting Started: + - index.md - Installation: getting-started/installation.md - - Examples: getting-started/examples.md - Security: getting-started/security.md - Learn: - Overview: learn/index.md + - Fundamentals: + - Networks: learn/networks.md + - Addresses: learn/addresses.md - RPC: - - RPC Intro: learn/rpc/overview.md + - Overview: learn/rpc/overview.md - Connecting: learn/rpc/connecting.md - Resolver: learn/rpc/resolver.md - Calls: learn/rpc/calls.md @@ -128,41 +130,34 @@ nav: - Overview: learn/wallet/overview.md - Architecture: learn/wallet/architecture.md - Lifecycle: learn/wallet/lifecycle.md - - Sync State: learn/wallet/sync-state.md - Wallet Files: learn/wallet/wallet-files.md - Private Keys: learn/wallet/private-keys.md - Accounts: learn/wallet/accounts.md - Addresses: learn/wallet/addresses.md - - Keypair Accounts: learn/wallet/keypair.md - - UTXO Maturity: learn/wallet/utxo-maturity.md - Send Transaction: learn/wallet/send-transaction.md - Sweep Funds: learn/wallet/sweep.md + - Sync State: learn/wallet/sync-state.md + - Events: learn/wallet/events.md - Transaction History: learn/wallet/transaction-history.md + - Errors: learn/wallet/errors.md - Wallet SDK: - Overview: learn/wallet-sdk/overview.md - Key Management: learn/wallet-sdk/key-management.md - - Transaction Generator: learn/wallet-sdk/tx-generator.md - Derivation: learn/wallet-sdk/derivation.md - - UTXO Context: learn/wallet-sdk/utxo-context.md - UTXO Processor: learn/wallet-sdk/utxo-processor.md - - Networks: learn/networks.md - - Addresses: learn/addresses.md + - UTXO Context: learn/wallet-sdk/utxo-context.md + - Transaction Generator: learn/wallet-sdk/tx-generator.md - Transactions: - Overview: learn/transactions/overview.md + - Metadata Fields: learn/transactions/metadata.md - Inputs: learn/transactions/inputs.md - Outputs: learn/transactions/outputs.md - Mass & Fees: learn/transactions/mass-and-fees.md - Signing: learn/transactions/signing.md + - Scripts: learn/transactions/scripts.md - Submission: learn/transactions/submission.md - - Metadata Fields: learn/transactions/metadata.md - Serialization: learn/transactions/serialization.md - - Kaspa Concepts: learn/concepts.md - - Guides: - - Generate or restore a mnemonic: guides/mnemonics.md - - Sign and verify a message: guides/message-signing.md - - Recover a wallet: guides/wallet-recovery.md - - Custom derivation paths: guides/custom-derivation.md - - Multi-signature transactions: guides/multisig.md + - Examples: examples.md - API Reference: reference/ - Contributing: - Overview: contributing/index.md diff --git a/python/kaspa/__init__.pyi b/python/kaspa/__init__.pyi index d9de118a..203bd04d 100644 --- a/python/kaspa/__init__.pyi +++ b/python/kaspa/__init__.pyi @@ -5666,166 +5666,4 @@ class SubmitTransactionResponse(TypedDict): class SubmitTransactionReplacementResponse(TypedDict): """Response from submit_transaction_replacement.""" transactionId: str - replacedTransaction: RpcTransaction - - -# ============================================================================= -# Notification Bodies (the inner notification struct delivered as event["data"]) -# ============================================================================= - -class RpcBlockAddedNotification(TypedDict): - """Body for the `block-added` notification. - - Delivered as `event["data"]` to listeners registered with - `add_event_listener("block-added", ...)`. - """ - block: RpcBlock - - -class RpcVirtualChainChangedNotification(TypedDict): - """Body for the `virtual-chain-changed` notification. - - `acceptedTransactionIds` is only populated when the subscription was - opened with `include_accepted_transaction_ids=True`. - """ - removedChainBlockHashes: list[str] - addedChainBlockHashes: list[str] - acceptedTransactionIds: list[RpcAcceptedTransactionIds] - - -class RpcFinalityConflictNotification(TypedDict): - """Body for the `finality-conflict` notification.""" - violatingBlockHash: str - - -class RpcFinalityConflictResolvedNotification(TypedDict): - """Body for the `finality-conflict-resolved` notification.""" - finalityBlockHash: str - - -class RpcSinkBlueScoreChangedNotification(TypedDict): - """Body for the `sink-blue-score-changed` notification.""" - sinkBlueScore: int - - -class RpcVirtualDaaScoreChangedNotification(TypedDict): - """Body for the `virtual-daa-score-changed` notification.""" - virtualDaaScore: int - - -class RpcPruningPointUtxoSetOverrideNotification(TypedDict, total=False): - """Body for the `pruning-point-utxo-set-override` notification. - - The body has no fields; the notification fires as a signal only. - """ - pass - - -class RpcNewBlockTemplateNotification(TypedDict, total=False): - """Body for the `new-block-template` notification. - - The body has no fields; the notification fires as a signal only. - """ - pass - - -# ============================================================================= -# Event Payloads (the dict an `add_event_listener` callback receives) -# ============================================================================= - -class BlockAddedEvent(TypedDict): - """Payload delivered for the `block-added` event.""" - type: typing.Literal["block-added"] - data: RpcBlockAddedNotification - - -class VirtualChainChangedEvent(TypedDict): - """Payload delivered for the `virtual-chain-changed` event.""" - type: typing.Literal["virtual-chain-changed"] - data: RpcVirtualChainChangedNotification - - -class FinalityConflictEvent(TypedDict): - """Payload delivered for the `finality-conflict` event.""" - type: typing.Literal["finality-conflict"] - data: RpcFinalityConflictNotification - - -class FinalityConflictResolvedEvent(TypedDict): - """Payload delivered for the `finality-conflict-resolved` event.""" - type: typing.Literal["finality-conflict-resolved"] - data: RpcFinalityConflictResolvedNotification - - -class SinkBlueScoreChangedEvent(TypedDict): - """Payload delivered for the `sink-blue-score-changed` event.""" - type: typing.Literal["sink-blue-score-changed"] - data: RpcSinkBlueScoreChangedNotification - - -class VirtualDaaScoreChangedEvent(TypedDict): - """Payload delivered for the `virtual-daa-score-changed` event.""" - type: typing.Literal["virtual-daa-score-changed"] - data: RpcVirtualDaaScoreChangedNotification - - -class PruningPointUtxoSetOverrideEvent(TypedDict): - """Payload delivered for the `pruning-point-utxo-set-override` event.""" - type: typing.Literal["pruning-point-utxo-set-override"] - data: RpcPruningPointUtxoSetOverrideNotification - - -class NewBlockTemplateEvent(TypedDict): - """Payload delivered for the `new-block-template` event.""" - type: typing.Literal["new-block-template"] - data: RpcNewBlockTemplateNotification - - -class UtxosChangedEvent(TypedDict): - """Payload delivered for the `utxos-changed` event. - - Note: unlike most notifications, the body is flattened onto the - event dict rather than nested under a `data` key. - """ - type: typing.Literal["utxos-changed"] - added: list[RpcUtxosByAddressesEntry] - removed: list[RpcUtxosByAddressesEntry] - - -class ConnectEvent(TypedDict): - """Payload delivered for the `connect` RPC control event. - - Fires when the underlying WebSocket connects (including reconnects). - `add_event_listener("connect", ...)` alone is enough — no - `subscribe_*` is required. - """ - type: typing.Literal["connect"] - rpc: str | None - - -class DisconnectEvent(TypedDict): - """Payload delivered for the `disconnect` RPC control event. - - Fires when the underlying WebSocket drops. - `add_event_listener("disconnect", ...)` alone is enough — no - `subscribe_*` is required. - """ - type: typing.Literal["disconnect"] - rpc: str | None - - -NotificationEventPayload = typing.Union[ - BlockAddedEvent, - VirtualChainChangedEvent, - FinalityConflictEvent, - FinalityConflictResolvedEvent, - SinkBlueScoreChangedEvent, - VirtualDaaScoreChangedEvent, - PruningPointUtxoSetOverrideEvent, - NewBlockTemplateEvent, - UtxosChangedEvent, - ConnectEvent, - DisconnectEvent, -] -"""Discriminated union of every dict shape that an event listener can -receive. Narrow on the `type` field to access event-specific keys.""" \ No newline at end of file + replacedTransaction: RpcTransaction \ No newline at end of file From a76e1f0d1c50f64fb74e98a0a529903c430a818e Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 2 May 2026 08:42:01 -0400 Subject: [PATCH 5/7] update security page --- docs/getting-started/security.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/getting-started/security.md b/docs/getting-started/security.md index 164e7ac9..9872f512 100644 --- a/docs/getting-started/security.md +++ b/docs/getting-started/security.md @@ -66,12 +66,3 @@ Code samples pass literal hex strings and short passwords inline for readability. **That is not how to handle real secrets.** Replace inline strings with values sourced from a secret manager, environment variable, hardware wallet, or interactive prompt. - -## When something leaks - -1. Move every UTXO out of the affected wallet *immediately* — to a freshly - derived wallet from a *new* mnemonic. -2. Stop using the leaked mnemonic. Don't "rotate the passphrase" or "skip - account 0" — derive from new entropy. -3. Audit any service that accepted that wallet's signed messages or extended - public key. From 302ccd5fb453b0d0bd9a21ba617568edd363c650 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 2 May 2026 09:02:26 -0400 Subject: [PATCH 6/7] add missing typed dicts --- docs/CHANGELOG.md | 1 + docs/learn/rpc/subscriptions.md | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 153dfff8..b3570636 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -20,6 +20,7 @@ - Wallet-specific exception classes populated into the `kaspa.exceptions` submodule, covering the rusty-kaspa wallet error variants (e.g. `WalletInsufficientFundsError`, `WalletAccountNotFoundError`, `WalletNotSyncedError`, etc.). - Examples under `examples/wallet/` demonstrating wallet usage - Pytest options `--network-id` and `--rpc-url` for targeting integration tests at a specific network / node. +- TypedDicts for `RpcClient` subscription event payloads — `BlockAddedEvent`, `VirtualChainChangedEvent`, `FinalityConflictEvent`, `FinalityConflictResolvedEvent`, `UtxosChangedEvent`, `SinkBlueScoreChangedEvent`, `VirtualDaaScoreChangedEvent`, `PruningPointUtxoSetOverrideEvent`, `NewBlockTemplateEvent`, `ConnectEvent`, `DisconnectEvent` — and their notification body TypedDicts (`RpcBlockAddedNotification`, etc.) for typing event-listener callbacks. ### Changed - `py_error_map!` macro extended to register wallet exception variants into the `kaspa.exceptions` submodule. diff --git a/docs/learn/rpc/subscriptions.md b/docs/learn/rpc/subscriptions.md index aff8ba46..53afeb5a 100644 --- a/docs/learn/rpc/subscriptions.md +++ b/docs/learn/rpc/subscriptions.md @@ -87,9 +87,13 @@ flattened so callbacks can read `event["added"]` directly. The [`RpcUtxosByAddressesEntry`](../../reference/TypedDicts/RpcUtxosByAddressesEntry.md) items. +The `"type"` value is the PascalCase variant name (`"UtxosChanged"`, +`"BlockAdded"`, …) — the kebab-case form (`"utxos-changed"`, +`"block-added"`, …) is only used when registering listeners. + ```python { - "type": "utxos-changed", + "type": "UtxosChanged", "added": [ { "address": "kaspa:qz...", @@ -118,7 +122,7 @@ For example, a `virtual-daa-score-changed` callback receives: ```python { - "type": "virtual-daa-score-changed", + "type": "VirtualDaaScoreChanged", "data": { "virtualDaaScore": 123456789, }, From 410fce584c9d545e94261fb58e4e825b304b3270 Mon Sep 17 00:00:00 2001 From: smartgoo Date: Sat, 2 May 2026 09:12:33 -0400 Subject: [PATCH 7/7] mkdocs build strict fix --- kaspa_rpc.pyi | 127 ++++++++++++++++++++++++++++++++++++- python/kaspa/__init__.pyi | 129 +++++++++++++++++++++++++++++++++++++- 2 files changed, 253 insertions(+), 3 deletions(-) diff --git a/kaspa_rpc.pyi b/kaspa_rpc.pyi index 506bdff6..7819c0ea 100644 --- a/kaspa_rpc.pyi +++ b/kaspa_rpc.pyi @@ -7,7 +7,7 @@ Long term, attempts should be made to auto generate. """ from enum import Enum -from typing import TypedDict +from typing import Literal, TypedDict # ============================================================================= @@ -962,3 +962,128 @@ class SubmitTransactionReplacementResponse(TypedDict): transactionId: str replacedTransaction: RpcTransaction + +# ============================================================================= +# Notification bodies (the value of the `data` field on subscription events) +# ============================================================================= + +class RpcBlockAddedNotification(TypedDict): + """Body of a `BlockAdded` event (the value of `event["data"]`).""" + block: RpcBlock + + +class RpcVirtualChainChangedNotification(TypedDict): + """Body of a `VirtualChainChanged` event.""" + removedChainBlockHashes: list[str] + addedChainBlockHashes: list[str] + acceptedTransactionIds: list[RpcAcceptedTransactionIds] + + +class RpcFinalityConflictNotification(TypedDict): + """Body of a `FinalityConflict` event.""" + violatingBlockHash: str + + +class RpcFinalityConflictResolvedNotification(TypedDict): + """Body of a `FinalityConflictResolved` event.""" + finalityBlockHash: str + + +class RpcSinkBlueScoreChangedNotification(TypedDict): + """Body of a `SinkBlueScoreChanged` event.""" + sinkBlueScore: int + + +class RpcVirtualDaaScoreChangedNotification(TypedDict): + """Body of a `VirtualDaaScoreChanged` event.""" + virtualDaaScore: int + + +class RpcPruningPointUtxoSetOverrideNotification(TypedDict): + """Body of a `PruningPointUtxoSetOverride` event. The body is empty.""" + pass + + +class RpcNewBlockTemplateNotification(TypedDict): + """Body of a `NewBlockTemplate` event. The body is empty.""" + pass + + +# ============================================================================= +# Subscription event wrappers (the dict passed to `add_event_listener` callbacks) +# +# Note: the `type` field is the PascalCase variant name as emitted by the +# bindings (e.g. `"BlockAdded"`), even though the kebab-case form (`"block-added"`) +# is what `add_event_listener` accepts when registering listeners. +# ============================================================================= + +class BlockAddedEvent(TypedDict): + """Callback payload for the `block-added` subscription.""" + type: Literal["BlockAdded"] + data: RpcBlockAddedNotification + + +class VirtualChainChangedEvent(TypedDict): + """Callback payload for the `virtual-chain-changed` subscription.""" + type: Literal["VirtualChainChanged"] + data: RpcVirtualChainChangedNotification + + +class FinalityConflictEvent(TypedDict): + """Callback payload for the `finality-conflict` subscription.""" + type: Literal["FinalityConflict"] + data: RpcFinalityConflictNotification + + +class FinalityConflictResolvedEvent(TypedDict): + """Callback payload for the `finality-conflict-resolved` subscription.""" + type: Literal["FinalityConflictResolved"] + data: RpcFinalityConflictResolvedNotification + + +class UtxosChangedEvent(TypedDict): + """Callback payload for the `utxos-changed` subscription. + + Unlike the other notification events, the body is flattened onto the + event itself rather than nested under a `data` key. + """ + type: Literal["UtxosChanged"] + added: list[RpcUtxosByAddressesEntry] + removed: list[RpcUtxosByAddressesEntry] + + +class SinkBlueScoreChangedEvent(TypedDict): + """Callback payload for the `sink-blue-score-changed` subscription.""" + type: Literal["SinkBlueScoreChanged"] + data: RpcSinkBlueScoreChangedNotification + + +class VirtualDaaScoreChangedEvent(TypedDict): + """Callback payload for the `virtual-daa-score-changed` subscription.""" + type: Literal["VirtualDaaScoreChanged"] + data: RpcVirtualDaaScoreChangedNotification + + +class PruningPointUtxoSetOverrideEvent(TypedDict): + """Callback payload for the `pruning-point-utxo-set-override` subscription.""" + type: Literal["PruningPointUtxoSetOverride"] + data: RpcPruningPointUtxoSetOverrideNotification + + +class NewBlockTemplateEvent(TypedDict): + """Callback payload for the `new-block-template` subscription.""" + type: Literal["NewBlockTemplate"] + data: RpcNewBlockTemplateNotification + + +class ConnectEvent(TypedDict): + """Emitted by `RpcClient` whenever the underlying WebSocket connects.""" + type: Literal["connect"] + rpc: str + + +class DisconnectEvent(TypedDict): + """Emitted by `RpcClient` whenever the underlying WebSocket disconnects.""" + type: Literal["disconnect"] + rpc: str + diff --git a/python/kaspa/__init__.pyi b/python/kaspa/__init__.pyi index 203bd04d..abc4076f 100644 --- a/python/kaspa/__init__.pyi +++ b/python/kaspa/__init__.pyi @@ -4713,7 +4713,7 @@ Long term, attempts should be made to auto generate. """ from enum import Enum -from typing import TypedDict +from typing import Literal, TypedDict # ============================================================================= @@ -5666,4 +5666,129 @@ class SubmitTransactionResponse(TypedDict): class SubmitTransactionReplacementResponse(TypedDict): """Response from submit_transaction_replacement.""" transactionId: str - replacedTransaction: RpcTransaction \ No newline at end of file + replacedTransaction: RpcTransaction + + +# ============================================================================= +# Notification bodies (the value of the `data` field on subscription events) +# ============================================================================= + +class RpcBlockAddedNotification(TypedDict): + """Body of a `BlockAdded` event (the value of `event["data"]`).""" + block: RpcBlock + + +class RpcVirtualChainChangedNotification(TypedDict): + """Body of a `VirtualChainChanged` event.""" + removedChainBlockHashes: list[str] + addedChainBlockHashes: list[str] + acceptedTransactionIds: list[RpcAcceptedTransactionIds] + + +class RpcFinalityConflictNotification(TypedDict): + """Body of a `FinalityConflict` event.""" + violatingBlockHash: str + + +class RpcFinalityConflictResolvedNotification(TypedDict): + """Body of a `FinalityConflictResolved` event.""" + finalityBlockHash: str + + +class RpcSinkBlueScoreChangedNotification(TypedDict): + """Body of a `SinkBlueScoreChanged` event.""" + sinkBlueScore: int + + +class RpcVirtualDaaScoreChangedNotification(TypedDict): + """Body of a `VirtualDaaScoreChanged` event.""" + virtualDaaScore: int + + +class RpcPruningPointUtxoSetOverrideNotification(TypedDict): + """Body of a `PruningPointUtxoSetOverride` event. The body is empty.""" + pass + + +class RpcNewBlockTemplateNotification(TypedDict): + """Body of a `NewBlockTemplate` event. The body is empty.""" + pass + + +# ============================================================================= +# Subscription event wrappers (the dict passed to `add_event_listener` callbacks) +# +# Note: the `type` field is the PascalCase variant name as emitted by the +# bindings (e.g. `"BlockAdded"`), even though the kebab-case form (`"block-added"`) +# is what `add_event_listener` accepts when registering listeners. +# ============================================================================= + +class BlockAddedEvent(TypedDict): + """Callback payload for the `block-added` subscription.""" + type: Literal["BlockAdded"] + data: RpcBlockAddedNotification + + +class VirtualChainChangedEvent(TypedDict): + """Callback payload for the `virtual-chain-changed` subscription.""" + type: Literal["VirtualChainChanged"] + data: RpcVirtualChainChangedNotification + + +class FinalityConflictEvent(TypedDict): + """Callback payload for the `finality-conflict` subscription.""" + type: Literal["FinalityConflict"] + data: RpcFinalityConflictNotification + + +class FinalityConflictResolvedEvent(TypedDict): + """Callback payload for the `finality-conflict-resolved` subscription.""" + type: Literal["FinalityConflictResolved"] + data: RpcFinalityConflictResolvedNotification + + +class UtxosChangedEvent(TypedDict): + """Callback payload for the `utxos-changed` subscription. + + Unlike the other notification events, the body is flattened onto the + event itself rather than nested under a `data` key. + """ + type: Literal["UtxosChanged"] + added: list[RpcUtxosByAddressesEntry] + removed: list[RpcUtxosByAddressesEntry] + + +class SinkBlueScoreChangedEvent(TypedDict): + """Callback payload for the `sink-blue-score-changed` subscription.""" + type: Literal["SinkBlueScoreChanged"] + data: RpcSinkBlueScoreChangedNotification + + +class VirtualDaaScoreChangedEvent(TypedDict): + """Callback payload for the `virtual-daa-score-changed` subscription.""" + type: Literal["VirtualDaaScoreChanged"] + data: RpcVirtualDaaScoreChangedNotification + + +class PruningPointUtxoSetOverrideEvent(TypedDict): + """Callback payload for the `pruning-point-utxo-set-override` subscription.""" + type: Literal["PruningPointUtxoSetOverride"] + data: RpcPruningPointUtxoSetOverrideNotification + + +class NewBlockTemplateEvent(TypedDict): + """Callback payload for the `new-block-template` subscription.""" + type: Literal["NewBlockTemplate"] + data: RpcNewBlockTemplateNotification + + +class ConnectEvent(TypedDict): + """Emitted by `RpcClient` whenever the underlying WebSocket connects.""" + type: Literal["connect"] + rpc: str + + +class DisconnectEvent(TypedDict): + """Emitted by `RpcClient` whenever the underlying WebSocket disconnects.""" + type: Literal["disconnect"] + rpc: str \ No newline at end of file