diff --git a/docs/api/_category_.json b/docs/api/_category_.json index be96b3c..4e4ade4 100644 --- a/docs/api/_category_.json +++ b/docs/api/_category_.json @@ -1,4 +1,4 @@ { "label": "API", - "position": 6 + "position": 5 } diff --git a/docs/api/conventions.md b/docs/api/conventions.md index 8ac6398..3a24d5e 100644 --- a/docs/api/conventions.md +++ b/docs/api/conventions.md @@ -10,18 +10,31 @@ Behavior shared by all indexer API endpoints. ## Pagination -List endpoints accept two query parameters: +List endpoints support two pagination styles: simple offset-based paging and opaque keyset cursors. Both validate `items` the same way. + +### `items` validation | Parameter | Type | Default | Description | | --------- | ---- | ------- | ----------- | -| `offset` | integer | `0` | Number of items to skip | | `items` | integer | `10` | Page size, capped at `100` | +`items` must be at least 1 (`items=0` returns HTTP 400) and at most 100; non-numeric values are rejected as well. This applies to both pagination styles. + +### Offset pagination + +For shallow listing ("the first few results"), pass `offset`: + +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `offset` | integer | `0` | Number of items to skip | + ```bash curl "https://api-server.mintlayer.org/api/v2/transaction?offset=0&items=20" ``` -Invalid values (non-numeric, or `items` above 100) return a client error. The transaction list endpoint additionally accepts `offset_mode`: +Deep offsets re-read every preceding row on each request, so they get slower the deeper you page. For long walks, use keyset pagination instead. + +The transaction list endpoint additionally accepts `offset_mode`: | `offset_mode` | Behavior | | ------------- | -------- | @@ -30,6 +43,31 @@ Invalid values (non-numeric, or `items` above 100) return a client error. The tr Use `absolute` when you need stable pagination (e.g. syncing all transactions); use the default for "recent activity" views. +### Keyset pagination (cursors) + +Deeper listings — [pools](endpoints/pool.md#get-pool), [transactions](endpoints/transaction.md#get-transaction), [address balance holders](endpoints/statistics.md#get-statisticscoinholders), and the [order book](endpoints/order.md#get-orderpairpairbook) — accept an opaque `cursor` query parameter instead of relying on deep offsets: + +```bash +curl "https://api-server.mintlayer.org/api/v2/pool?cursor=&items=100" +``` + +- The cursor is an **opaque string** (base64-encoded JSON, at most 1 KiB and 16 keys). Treat it as a black box: never construct, decode, modify, or store one beyond the life of the walk — pass back exactly what the server returned. An empty value (`cursor=`) starts the listing from the beginning. +- When a `cursor` parameter is supplied, the response is an envelope instead of a plain array (the [holders](endpoints/statistics.md#get-statisticscoinholders) and [order book](endpoints/order.md#get-orderpairpairbook) endpoints always return the envelope): + +```json +{ + "items": ["..."], + "next_cursor": "eyJ0YWciOiJwb29scyIsImtleXMiOlsiMTAyMzAwIl0sImlkIjoibXBvb2wx..." +} +``` + +- Pass `next_cursor` back as `cursor` to fetch the following page. A `null` `next_cursor` means the listing is exhausted. (For the order book, `null` can also mean the walk was cut short by the server-side scan cap — see [Orders](endpoints/order.md#get-orderpairpairbook).) +- Ordering is stable, newest first. Ties are broken by descending byte-order ID, so equal-ranked entries keep a fixed relative order across pages. Transactions additionally order by ascending transaction index within a block, so consecutive pages reconstruct each block's transaction order. +- If both `cursor` and `offset` are given, the cursor takes precedence. +- Pages are guaranteed stable only once the scanner has fully caught up with the chain tip. While it is catching up (e.g. after a reorg), a concurrent walk may skip or repeat an entry. + +Offset pagination remains the simple alternative for shallow listing; cursors are the right tool whenever a full listing needs to be walked. + ## Amounts All amounts are returned as an object with two representations: diff --git a/docs/api/endpoints/order.md b/docs/api/endpoints/order.md index d6b0a0f..4037bf0 100644 --- a/docs/api/endpoints/order.md +++ b/docs/api/endpoints/order.md @@ -51,6 +51,47 @@ curl "https://api-server.mintlayer.org/api/v2/order/pair/ML_mmltk18e0xfgmw3sn4s8 The pair direction is normalized (matching orders for the pair are returned regardless of give/ask orientation). +## GET /order/pair/\{pair\}/book + +Returns the aggregated order book for a trading pair: open orders are grouped by price, and each price level's `amount` is the summed available balance across the orders at that price (an order filled partially contributes its remaining balance, which is carried as it fills). + +The pair is `{BASE}_{QUOTE}` — exactly two non-empty, `_`-separated sides, each the coin ticker (`ML` on mainnet, case-insensitive) or a token ID. Both sides must be the native coin or a registered token; anything else is a client error (`400 {"error": "Invalid order trading pair"}`). The direction is normalized: requesting the reversed pair works and shows the mirrored side of the same orders. + +Query parameters: + +| Parameter | Description | +| --------- | ----------- | +| `side` | Required. `ask` lists the orders asking for the base currency (first in the pair) while giving the quote; `bid` lists the reverse (asking for quote, giving base) | +| `items` | Page size, capped at `100`. See [Conventions](../conventions.md#pagination) | +| `cursor` | [Keyset pagination](../conventions.md#keyset-pagination-cursors) cursor from `next_cursor`. Cursors are side-specific: a cursor from the ask book is rejected (HTTP 400) on the bid walk, and vice versa | + +Ask levels are ordered by ascending price, bid levels by descending price (best price first). Each level's `price` is quote-per-base in two forms: `atoms` is the exact rational in atoms as a `numer/denom` string (quote atoms per base atom), and `decimal` is a display string floored from the exact value. + +```bash +curl "https://api-server.mintlayer.org/api/v2/order/pair/ML_mmltk18e0xfgmw3sn4s8vqu0ypjsnlv2fhnm6ye4zcazwqpr9dujrrdf6qjyhh0e/book?side=ask&items=2" +``` + +```json +{ + "items": [ + { + "price": { "decimal": "1", "atoms": "1/1" }, + "amount": { "atoms": "10000000000000", "decimal": "100" } + }, + { + "price": { "decimal": "3", "atoms": "3/1" }, + "amount": { "atoms": "98250000000000", "decimal": "982.5" } + } + ], + "next_cursor": null +} +``` + +- `amount` is in the base currency of the pair; both amounts follow the [atoms/decimal convention](../conventions.md#amounts). +- A request scans at most 10,000 open orders. If the scan hit that cap, the response carries an additional `"truncated": true` and **no `next_cursor`**: the levels in hand may be an incomplete view of the book, so re-issue the request (e.g. with different parameters) rather than continuing from a cursor. +- Otherwise `next_cursor` follows the usual [keyset pagination](../conventions.md#keyset-pagination-cursors) contract, and `null` means the end of the book. +- The book is computed per request from a live snapshot of open orders. While the scanner is still catching up, a concurrent walk may skip or repeat a level — re-fetch from the start (`cursor=`) when you need a consistent view. + ## Go SDK ```go diff --git a/docs/api/endpoints/pool.md b/docs/api/endpoints/pool.md index bb743c1..e6ae51b 100644 --- a/docs/api/endpoints/pool.md +++ b/docs/api/endpoints/pool.md @@ -30,6 +30,7 @@ curl "https://api-server.mintlayer.org/api/v2/pool?items=1" - `margin_ratio_per_thousand` is the pool's commission in per-mille (e.g. `100%` means 10%... represented as per-thousand of the reward). - `staker_balance` is the pool founder's pledge; `delegations_balance` is the total delegated by other users. +- The listing is ordered by creation height, newest first. Besides `offset`/`items`, it accepts [keyset pagination](../conventions.md#keyset-pagination-cursors) via `cursor`: with a `cursor` parameter the response is the `{items, next_cursor}` envelope instead of a plain array, and `cursor` takes precedence over `offset`. Pass `next_cursor` back as `cursor` to walk the full list without deep offsets; a `null` `next_cursor` ends the walk. ## GET /pool/\{id\} diff --git a/docs/api/endpoints/statistics.md b/docs/api/endpoints/statistics.md index c73c02d..516396f 100644 --- a/docs/api/endpoints/statistics.md +++ b/docs/api/endpoints/statistics.md @@ -1,6 +1,6 @@ --- title: "Statistics" -description: "Supply statistics endpoints of the Mintlayer indexer API: coin and per-token." +description: "Supply statistics endpoints of the Mintlayer indexer API: coin and per-token supply counters and address balance holders." sidebar_position: 7 --- @@ -25,20 +25,72 @@ curl https://api-server.mintlayer.org/api/v2/statistics/coin All amounts follow the [atoms/decimal convention](../conventions.md#amounts). +The counters are maintained by the scanner while it indexes the chain (and rolled back on reorgs), so they are always up to date with the indexed tip: + +- `preminted` — the total amount that has entered circulation: for the coin, transfer outputs and pool-creation pledges; for a token, transfer outputs since issuance. +- `staked` — the amount currently staked in pools (the founder's pledge plus delegated balances). +- `circulating_supply` — the amount currently in circulation: `preminted` minus everything `burned`. Burns are subtracted during indexing, so this counter already reflects everything counted in `burned`; the total amount ever minted is therefore `circulating_supply + burned`. +- `burned` — the total amount destroyed: outputs sent to burn destinations, plus the fees of token account commands (minting, freezing, authority changes, ...), which are burned rather than paid to anyone. + +Consumers should treat an absent statistic as zero: the endpoint lists the statistics that have been written, and a counter with no recorded activity is omitted from the underlying listing rather than returned as `0`. The endpoints above render such absent counters as zero, as in the token example below. + ## GET /statistics/token/\{id\} Returns the same statistics for a specific token, identified by its bech32 token ID (`mmltk1...`): +```bash +curl https://api-server.mintlayer.org/api/v2/statistics/token/mmltk18e0xfgmw3sn4s8vqu0ypjsnlv2fhnm6ye4zcazwqpr9dujrrdf6qjyhh0e +``` + ```json { "burned": { "atoms": "0", "decimal": "0" }, "circulating_supply": { "atoms": "0", "decimal": "0" }, "preminted": { "atoms": "0", "decimal": "0" }, - "staked": { "atoms": "0", "decimal": "0" }, - "total_supply": { "atoms": "0", "decimal": "0" } + "staked": { "atoms": "0", "decimal": "0" } +} +``` + +For tokens, `circulating_supply` increases with transfer outputs and mint commands and decreases with everything burned. An unknown token returns HTTP 404 with `{"error": "Token not found"}`; a malformed token ID returns HTTP 400. + +## GET /statistics/coin/holders + +Lists the top addresses by coin balance, largest first: + +```bash +curl "https://api-server.mintlayer.org/api/v2/statistics/coin/holders?items=2" +``` + +```json +{ + "items": [ + { + "address": "mtc1q8n9u3g3aw4h40gsagxn7yw0jatdfe9xsuftnvur", + "amount": { "atoms": "18923740491080455517", "decimal": "189237404.91080455517" } + }, + { + "address": "mtc1q9sl5xwkfypf7gutvnwfscvzs9w9gjqc0vs90e34", + "amount": { "atoms": "98250000000000", "decimal": "982.5" } + } + ], + "next_cursor": null } ``` +- Entries are ordered by balance, descending; ties are broken by address in descending byte order, so equal balances keep a stable order. Addresses with a zero balance are not listed. +- `amount` follows the [atoms/decimal convention](../conventions.md#amounts). +- The response is always the `{items, next_cursor}` envelope. Walk the full listing with [keyset pagination](../conventions.md#keyset-pagination-cursors): pass `next_cursor` back as `cursor`; `null` means the listing is exhausted. + +## GET /statistics/token/\{id\}/holders + +The same listing for the holders of a specific token, identified by its bech32 token ID (`mmltk1...`): + +```bash +curl "https://api-server.mintlayer.org/api/v2/statistics/token/mmltk18e0xfgmw3sn4s8vqu0ypjsnlv2fhnm6ye4zcazwqpr9dujrrdf6qjyhh0e/holders?items=1" +``` + +Same response shape and ordering as the coin holders listing. An unknown token returns HTTP 404 with `{"error": "Token not found"}`. + ## Go SDK ```go diff --git a/docs/api/endpoints/transaction.md b/docs/api/endpoints/transaction.md index c30c1c2..a1c5757 100644 --- a/docs/api/endpoints/transaction.md +++ b/docs/api/endpoints/transaction.md @@ -8,7 +8,7 @@ sidebar_position: 3 ## GET /transaction -Lists transactions, newest first. Supports [pagination](../conventions.md#pagination) with `offset` and `items`. +Lists transactions, newest first. Supports [pagination](../conventions.md#pagination) with `offset` and `items`, or with an opaque keyset `cursor` for walking the full history. ```bash curl "https://api-server.mintlayer.org/api/v2/transaction?offset=0&items=1" @@ -54,6 +54,13 @@ Query parameters: | --------- | ----------- | | `offset`, `items` | Standard [pagination](../conventions.md#pagination) | | `offset_mode` | `legacy` (default) or `absolute` for stable pagination over the global index. See [Conventions](../conventions.md#pagination) | +| `cursor` | [Keyset pagination](../conventions.md#keyset-pagination-cursors) cursor from `next_cursor`; an empty value starts from the beginning. Returns pages in the same, stable newest-first order (transactions within a block in ascending transaction index, so pages reconstruct block order) | + +Notes on `cursor`: + +- With a `cursor` parameter the response is the `{items, next_cursor}` envelope instead of a plain array; a `null` `next_cursor` ends the walk. +- `cursor` takes precedence over `offset`. Combining `cursor` with `offset_mode` is a client error. +- For shallow listing, plain `offset`/`items` remains the simple alternative. ## GET /transaction/\{id\} diff --git a/docs/api/index.md b/docs/api/index.md index eace05a..c8ef1cf 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -49,7 +49,7 @@ curl https://api-server.mintlayer.org/ - **No authentication** for reads. The API is read-only by default and suitable for public frontends (CORS is open). - **Writes are gated.** Submitting transactions via `POST /transaction` is disabled unless the server is started with `--enable-post-routes` (the production API does not expose it). -- **Pagination** on list endpoints via `offset` and `items` query parameters (default 10 items, max 100). See [Conventions](conventions.md). +- **Pagination** on list endpoints via `offset` and `items` query parameters (default 10 items, max 100), and via opaque keyset `cursor`s for walking deep listings (pools, transactions, holders, order book). See [Conventions](conventions.md). - **Amounts** are returned as `{atoms, decimal}` pairs. See [Conventions](conventions.md). ## Documentation map @@ -63,9 +63,9 @@ curl https://api-server.mintlayer.org/ | [Addresses](endpoints/address.md) | Balances, UTXOs, delegations, token authority | | [Pools](endpoints/pool.md) | Staking pools, block stats, delegations | | [Delegations](endpoints/delegation.md) | Delegation lookup | -| [Statistics](endpoints/statistics.md) | Coin and token supply statistics | +| [Statistics](endpoints/statistics.md) | Coin and token supply statistics, address balance holders | | [Tokens](endpoints/token.md) | Fungible tokens, ticker lookup | | [NFTs](endpoints/nft.md) | NFT issuance data | -| [Orders](endpoints/order.md) | On-chain order book | +| [Orders](endpoints/order.md) | On-chain order book, price-level aggregation | | [Fee rate](endpoints/feerate.md) | Mempool fee estimates | | [Transaction submission](endpoints/transaction-submission.md) | POST /transaction (gated) | diff --git a/docs/build/index.md b/docs/build/index.md index c1cfe21..430c3a7 100644 --- a/docs/build/index.md +++ b/docs/build/index.md @@ -21,6 +21,7 @@ For ports, RPC authentication, WebSocket subscriptions, regtest, and the WASM wr - **[Go SDK](sdks/go/index.md)**, full coverage of the node, indexer, and wallet RPCs plus the embedded WASM cryptography runtime (no CGO). Recommended for services, exchanges, and backends. - **[Rust SDK](sdks/rust/index.md)**, async sub-clients for the node, indexer, and wallet RPCs plus native cryptography and transaction building backed by mintlayer-core (opt-in `crypto` feature). +- **[Python SDK](sdks/python/index.md)**, the Go SDK ported to Python: synchronous RPC clients plus the WASM cryptography runtime bundled via wasmtime. ## Accept addresses from users diff --git a/docs/build/sdks/go/_category_.json b/docs/build/sdks/go/_category_.json new file mode 100644 index 0000000..fa63931 --- /dev/null +++ b/docs/build/sdks/go/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Go SDK", + "position": 2, + "link": { + "type": "doc", + "id": "build/sdks/go/index" + } +} diff --git a/docs/build/sdks/javascript/_category_.json b/docs/build/sdks/javascript/_category_.json index 3f5267d..9deef83 100644 --- a/docs/build/sdks/javascript/_category_.json +++ b/docs/build/sdks/javascript/_category_.json @@ -1,6 +1,6 @@ { "label": "JavaScript SDK", - "position": 2, + "position": 1, "link": { "type": "doc", "id": "build/sdks/javascript/index" diff --git a/docs/build/sdks/python/_category_.json b/docs/build/sdks/python/_category_.json new file mode 100644 index 0000000..3b0b998 --- /dev/null +++ b/docs/build/sdks/python/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Python SDK", + "position": 4, + "link": { + "type": "doc", + "id": "build/sdks/python/index" + } +} diff --git a/docs/build/sdks/python/getting-started.md b/docs/build/sdks/python/getting-started.md new file mode 100644 index 0000000..b7b7866 --- /dev/null +++ b/docs/build/sdks/python/getting-started.md @@ -0,0 +1,100 @@ +--- +title: "Getting Started" +description: "Set up the Mintlayer Python SDK: installation, first call, networks and ports, amounts, error handling, and testing." +sidebar_position: 2 +--- + +# Getting Started + +This page covers everything needed to get a project running with the Python SDK: installation, the first call, networks, units, error handling, and testing. The sub-client references ([Node](node.md), [Indexer](indexer.md), [Wallet](wallet.md), [WASM](wasm.md)) document the full API surface. + +## Requirements + +- **Python 3.10+**; the package name on PyPI is `mintlayer` +- Dependencies: `requests` and `wasmtime` (the WASM cryptography binary is bundled with the package and sha256-pinned at import) +- Whichever services your code talks to: a synced [node-daemon](../../../node/index.md), an [api-web-server](../../../getting-started/install/install-from-docker.md#running-the-api-server-stack) indexer, and/or a wallet-rpc-daemon. For development setups see [Developer Setup](../../development.md). + +## Installation + +```bash +pip install mintlayer +``` + +## First call + +Create the top-level client with the URLs of the services you need, and query the chain tip: + +```python +import mintlayer + +client = mintlayer.Client( + mintlayer.Config( + node_url="http://127.0.0.1:3030", + indexer_url="http://127.0.0.1:3000", + wallet_url="http://127.0.0.1:3034", + ) +) + +tip = client.indexer.get_tip() +print(f"chain tip: height={tip.block_height} id={tip.block_id}") + +client.close() # or use `with mintlayer.Client(cfg) as client:` +``` + +Notes: + +- `Config` only constructs the sub-clients whose URL field is non-empty (`node_url`, `indexer_url`, `wallet_url`, plus shared `username`, `password`, `timeout`); use the top-level client when you need several, or import sub-modules directly when you need one. +- All remote methods are synchronous and thread-safe; clients support the context-manager protocol. +- The WASM runtime instantiates lazily on first access; call `client.init_wasm()` to make the ~400 ms cost explicit (it is a no-op afterwards), and `client.wasm.close()` to release it. + +## Networks and endpoints + +| Service | Mainnet | Testnet | Protocol | +| ------- | ------- | ------- | -------- | +| node-daemon RPC | `3030` | `13030` | JSON-RPC 2.0 (HTTP and WebSocket) | +| api-web-server (indexer) | `3000` | `13000` | REST under `/api/v2` | +| wallet-rpc-daemon RPC | `3034` | `13034` | JSON-RPC 2.0 | + +The node and wallet clients accept optional `username`/`password` (HTTP basic auth) and `timeout` (seconds, default 30); the indexer accepts `timeout` and an optional `requests.Session`, and is unauthenticated. Loopback `http://` is fine; for remote daemons prefer TLS or an authenticated tunnel. + +Cryptography functions that derive addresses or encode transactions take the network explicitly: `mintlayer.MAINNET`, `TESTNET`, `REGTEST`, `SIGNET` (or the `Network` enum members). + +## Amounts + +All coin and token amounts are atom counts: **1 ML = 100,000,000,000 atoms** (11 decimal places). + +```python +from mintlayer.wasm import Amount + +one = Amount.from_atoms("100000000000") # 1 ML +print(one.atoms) # "100000000000" +``` + +The indexer and wallet clients use their own `Amount` dataclass with `atoms` and `decimal` fields populated by the server; requests typically set `atoms` only. + +## Error handling + +- `mintlayer.node.RPCError`: JSON-RPC error from the node daemon (`code`, `message`); transport failures raise `JSONRPCError` +- `mintlayer.wallet.RPCError`: JSON-RPC error from the wallet daemon +- `mintlayer.indexer.HTTPError`: non-2xx response (`status_code`, `body`); transport/decode failures raise `IndexerError` +- `mintlayer.wasm.WasmError`: WASM operation failures, with a message prefixed by `mintlayer:` + +Error bodies are trimmed and stripped of control characters, and basic-auth credentials are redacted from the clients' `repr`. + +## Testing + +- The SDK repository carries an extensive pytest suite; development tooling is pytest, ruff, and mypy (via `uv`). +- For your own code, test against **testnet**: get TML from the [faucet](https://faucet.mintlayer.org) (testnet addresses start with `tmt1`) and point the clients at the testnet ports above. +- For fully local iteration, use `REGTEST` with a [regtest node](../../../build/development.md#running-a-node-for-development) and generate blocks on demand. +- The repository's runnable examples double as integration checks: + + ```bash + uv run python examples/send_coins.py --to mtc1qrecipient... --amount 100000000000 --indexer http://127.0.0.1:3000 + uv run python examples/issue_token.py --wallet /path/to/wallet.dat --ticker MYTOKEN --decimals 2 --supply 1000000 --wallet-rpc http://127.0.0.1:3034 + ``` + +## Next steps + +- [Node client](node.md), [Indexer client](indexer.md), [Wallet client](wallet.md), [WASM client](wasm.md): the full API references +- [Building transactions](transactions.md): the full-custody flow without the wallet daemon +- [Guides](../../../guides/python/index.md): tokens, NFTs, staking, orders, and atomic swaps in Python diff --git a/docs/build/sdks/python/index.md b/docs/build/sdks/python/index.md new file mode 100644 index 0000000..595570e --- /dev/null +++ b/docs/build/sdks/python/index.md @@ -0,0 +1,84 @@ +--- +title: "Python SDK" +description: "The Mintlayer Python SDK: one client for the node, indexer and wallet RPCs, plus WASM cryptography and transaction building via wasmtime." +sidebar_position: 1 +--- + +# Python SDK + +The Python SDK (`mintlayer`) is ported from the Go SDK: it wraps the node RPC, the indexer REST API and the wallet RPC as typed synchronous clients, and embeds the WASM cryptography runtime ([wasmtime](https://wasmtime.dev/), bundled with the package) for key management and transaction building. + +```bash +pip install mintlayer +``` + +- Requires Python 3.10+; every remote method is synchronous and thread-safe +- Dependencies: `requests` and `wasmtime` only; the WASM binary is sha256-pinned at import +- Source: [github.com/mintlayer/python-sdk](https://github.com/mintlayer/python-sdk) + +## Architecture + +A top-level `Client` wires the sub-clients; `Config` constructs only the sub-clients whose URL field is non-empty. Use the top-level client when you need several, or import sub-modules (`mintlayer.node`, `mintlayer.indexer`, `mintlayer.wallet`, `mintlayer.wasm`) directly when you need one. + +| Module | Purpose | Default port | +| ------ | ------- | ------------ | +| `mintlayer.node` | JSON-RPC 2.0 client for the node daemon | 3030 (mainnet) | +| `mintlayer.indexer` | REST client for the indexer (api-web-server) | 3000 (mainnet) | +| `mintlayer.wallet` | JSON-RPC 2.0 client for the wallet daemon | 3034 (mainnet) | +| `mintlayer.wasm` | Cryptography and transaction building via WASM | none | + +The WASM runtime instantiates in ~400 ms; the first access triggers lazy construction, and `init_wasm()` makes the cost explicit (it is a no-op afterwards). Call `client.close()` when done, or use the client as a context manager. + +The sub-clients map 1:1 to the services documented in [Developer Setup](../../development.md), and the indexer client mirrors the [API endpoints](../../../api/endpoints/chain.md) endpoint-for-endpoint. + +## Amounts + +All coin and token amounts use the `Amount` type, a decimal string of **atoms**: **1 ML = 100,000,000,000 atoms** (11 decimal places). + +- `mintlayer.wasm.Amount`: built with `Amount(atoms="...")` or `Amount.from_atoms("...")` +- The indexer and wallet clients use their own `Amount` dataclass with `atoms` and `decimal` fields populated by the server + +## Quick start + +```python +import mintlayer + +client = mintlayer.Client( + mintlayer.Config( + node_url="http://127.0.0.1:3030", + indexer_url="http://127.0.0.1:3000", + wallet_url="http://127.0.0.1:3034", + ) +) + +# Query the chain tip from the indexer. +tip = client.indexer.get_tip() +print(f"chain tip: height={tip.block_height} id={tip.block_id}") + +# Optionally initialise the embedded WASM cryptography runtime (~400 ms). +client.init_wasm() + +priv_key = client.wasm.make_private_key() +pub_key = client.wasm.public_key_from_private_key(priv_key) +addr = client.wasm.pubkey_to_pubkeyhash_address(pub_key, mintlayer.MAINNET) +print("address:", addr) + +client.close() # or use `with mintlayer.Client(cfg) as client:` +``` + +Convenience re-exports (`mintlayer.Amount`, `mintlayer.Network`, `mintlayer.MAINNET`, ...) mirror the Go SDK's aliases, so callers that only import the top-level package do not need to also import `mintlayer.wasm`. + +## Guides + +| Guide | Contents | +| ----- | -------- | +| [Getting Started](getting-started.md) | Installation, first call, networks and ports, amounts, errors, testing | +| [Node client](node.md) | Chainstate, mempool, P2P, block submission, trust policy | +| [Indexer client](indexer.md) | Chain, blocks, transactions, addresses, pools, tokens, orders, statistics | +| [Wallet client](wallet.md) | Wallet lifecycle, accounts, balances, transactions, staking, tokens, orders | +| [WASM client](wasm.md) | Keys, addresses, inputs, outputs, signing, fees, intents | +| [Building transactions](transactions.md) | Full-custody flow: UTXOs, fees, signing, submission | +| [Staking](staking.md) | Pool creation, delegation, withdrawal | +| [Tokens and NFTs](tokens.md) | Fungible token and NFT lifecycle, and manual encoders | + +Runnable examples live in the repository under [examples/](https://github.com/mintlayer/python-sdk/tree/main/examples) (`send_coins.py`, `issue_token.py`). diff --git a/docs/build/sdks/python/indexer.md b/docs/build/sdks/python/indexer.md new file mode 100644 index 0000000..5d6aa0c --- /dev/null +++ b/docs/build/sdks/python/indexer.md @@ -0,0 +1,575 @@ +--- +title: "Indexer client" +description: "Indexer client reference for the Mintlayer Python SDK: chain, blocks, transactions, addresses, pools, tokens, orders, statistics." +sidebar_position: 4 +--- + +# Indexer client + +The `mintlayer.indexer` module is a REST client for the Mintlayer indexer +(`api-web-server`). All paths are relative to the `/api/v2` base appended to +the configured URL. + +```python +from mintlayer.indexer import Client + +c = Client( + "http://127.0.0.1:3000", + timeout=15.0, # optional, seconds (default 30.0) + session=None, # optional requests.Session (client owns it otherwise) +) +``` + +**Default port:** 3000 (mainnet), 13000 (testnet). + +Non-2xx HTTP responses raise `HTTPError`; transport/decode failures raise +`IndexerError`: + +```python +from mintlayer.indexer import HTTPError, IndexerError + +try: + info = c.get_address_info("mtc1q...") +except HTTPError as e: + print(e.status_code, e.body) # e.g. 404 "address not found" +except IndexerError as e: + ... # connection failure or malformed JSON body +``` + +The client is safe for concurrent use from multiple threads and supports the +context-manager protocol (`with Client(...) as c: ...`). + +--- + +## Pagination + +List endpoints accept a `PageOpts` dataclass: + +```python +@dataclass(frozen=True) +class PageOpts: + offset: int = 0 # default: 0 + items: int = 0 # default: 10 (server-side default) +``` + +**Zero-omission rule:** zero values are omitted from the query string entirely, +so the server defaults apply. Only positive values are sent: + +```python +from mintlayer.indexer import PageOpts + +c.list_transactions(PageOpts(offset=20, items=50)) +c.list_orders() # server defaults +``` + +--- + +## Chain + +### `get_tip` + +```python +def get_tip(self) -> ChainTip: ... +``` + +Returns the highest confirmed block. + +```python +@dataclass(frozen=True) +class ChainTip: + block_height: int + block_id: str +``` + +### `get_genesis` + +```python +def get_genesis(self) -> GenesisInfo: ... +``` + +Returns genesis block information (`block_id`, `genesis_message`, +`timestamp`, `utxos`). + +### `get_block_id_at_height` + +```python +def get_block_id_at_height(self, height: int) -> str | None: ... +``` + +Returns the block ID at a given height, or `None` if the indexer responds +with JSON null. Raises a 404 `HTTPError` if no block exists at that height +(for example, when querying a height beyond the current tip). + +--- + +## Blocks + +### `get_block` + +```python +def get_block(self, block_id: str) -> Block: ... +``` + +Returns the full block: `height`, `header` (`BlockHeader`) and `body` +(`BlockBody` with `reward` and `transactions`). + +### `get_block_header` + +```python +def get_block_header(self, block_id: str) -> BlockHeader: ... +``` + +Returns only the block header (`previous_block_id`, `timestamp`, `merkle_root`, +`witness_merkle_root`, `consensus_data`). Cheaper than `get_block` when you do +not need transaction data. + +### `get_block_reward` + +```python +def get_block_reward(self, block_id: str) -> list: ... +``` + +Returns the reward outputs of a block as raw decoded JSON values. + +### `get_block_transaction_ids` + +```python +def get_block_transaction_ids(self, block_id: str) -> list[str]: ... +``` + +Returns the transaction IDs included in a block. Use this to page through block +contents without fetching full transaction data. + +--- + +## Transactions + +### `list_transactions` + +```python +def list_transactions(self, opts: PageOpts | None = None) -> list[Transaction]: ... +``` + +Returns a paginated list of confirmed transactions across the entire chain. + +### `get_transaction` + +```python +def get_transaction(self, tx_id: str) -> Transaction: ... +``` + +Returns a transaction by ID. The `block_id`, `timestamp`, and `confirmations` +fields are empty strings for unconfirmed transactions. + +```python +@dataclass(frozen=True) +class Transaction: + id: str + inputs: Any # raw decoded JSON + outputs: Any # raw decoded JSON + block_id: str + timestamp: str + confirmations: str +``` + +### `get_transaction_merkle_path` + +```python +def get_transaction_merkle_path(self, tx_id: str) -> MerklePath: ... +``` + +Returns the Merkle inclusion proof for a transaction (`block_id`, +`transaction_index`, `merkle_root`, `path`). Raises a 404 `HTTPError` if the +transaction is not yet in a block. + +### `get_transaction_output` + +```python +def get_transaction_output(self, tx_id: str, output_index: int) -> Any: ... +``` + +Returns a single output from a transaction as raw decoded JSON. The shape is +determined by the `"type"` field. Common types: `"Transfer"`, +`"LockThenTransfer"`, `"Burn"`, `"CreateStakePool"`, `"CreateDelegationId"`, +`"DelegateStaking"`, `"IssueFungibleToken"`, `"IssueNft"`, `"DataDeposit"`, +`"Htlc"`, `"CreateOrder"`. + +### `submit_transaction` + +```python +def submit_transaction(self, signed_tx_hex: str) -> str: ... +``` + +Submits a hex-encoded signed transaction to the network. Returns the +transaction ID on success. The hex string is POSTed verbatim as +`text/plain` — the one non-GET route in the client. + +**Requires** the indexer to be started with `--enable-post-routes`. + +--- + +## Addresses + +### `get_address_info` + +```python +def get_address_info(self, address: str) -> AddressInfo: ... +``` + +Returns balance and transaction history for a bech32m address. Raises a 404 +`HTTPError` if the address has no on-chain history. + +```python +@dataclass(frozen=True) +class AddressInfo: + coin_balance: Amount + locked_coin_balance: Amount + transaction_history: list[str] + tokens: list[TokenBalance] # TokenBalance(token_id, amount) +``` + +### `get_spendable_utxos` + +```python +def get_spendable_utxos(self, address: str) -> list[UTXO]: ... +``` + +Returns confirmed, unspent UTXOs that can be spent immediately. + +### `get_all_utxos` + +```python +def get_all_utxos(self, address: str) -> list[UTXO]: ... +``` + +Returns all UTXOs including those that are locked or otherwise unspendable. + +```python +@dataclass(frozen=True) +class UTXO: + outpoint: UTXOOutpoint # UTXOOutpoint(source_id: str, index: int) + output: Any # raw JSON; note the payload key on the wire is "utxo" +``` + +### `get_delegations` + +```python +def get_delegations(self, address: str) -> list[DelegationInfo]: ... +``` + +Returns all staking delegations owned by an address. + +```python +@dataclass(frozen=True) +class DelegationInfo: + delegation_id: str + pool_id: str + next_nonce: int + spend_destination: str + balance: Amount +``` + +### `get_token_authority` + +```python +def get_token_authority(self, address: str) -> list[str]: ... +``` + +Returns the IDs (bech32m) of fungible tokens for which the address holds +authority (can mint, freeze, etc.). + +--- + +## Pools and delegations + +### `list_pools` + +```python +def list_pools(self, opts: PoolListOpts | None = None) -> list[Pool]: ... +``` + +Returns staking pools with optional pagination. The `sort` field accepts: + +- `"by_height"` (server default): newest pools first +- `"by_pledge"`: largest staker balance first + +```python +@dataclass(frozen=True) +class PoolListOpts: + offset: int = 0 + items: int = 0 + sort: str = "" # omitted from the query when empty (zero-omission rule) +``` + +```python +c.list_pools(PoolListOpts(sort="by_pledge", items=20)) +``` + +### `get_pool` + +```python +def get_pool(self, pool_id: str) -> Pool: ... +``` + +Returns a single staking pool by its bech32m pool ID. + +```python +@dataclass(frozen=True) +class Pool: + pool_id: str + decommission_destination: str + staker_balance: Amount + margin_ratio_per_thousand: float + cost_per_block: Amount + vrf_public_key: str + delegations_balance: Amount +``` + +### `get_pool_block_stats` + +```python +def get_pool_block_stats(self, pool_id: str, from_time: datetime, to_time: datetime) -> int: ... +``` + +Returns the number of blocks produced by a pool in the half-open interval +`[from_time, to_time)`. Datetimes are converted to Unix-seconds query +parameters. + +```python +from datetime import datetime, timedelta + +count = c.get_pool_block_stats( + "mpool1...", + datetime.now() - timedelta(hours=24), + datetime.now(), +) +``` + +### `get_delegation` + +```python +def get_delegation(self, delegation_id: str) -> Delegation: ... +``` + +Returns a single delegation by its bech32m delegation ID. + +```python +@dataclass(frozen=True) +class Delegation: + delegation_id: str + pool_id: str + next_nonce: int + spend_destination: str + balance: Amount + creation_block_height: int +``` + +### `get_pool_delegations` + +```python +def get_pool_delegations(self, pool_id: str) -> list[PoolDelegation]: ... +``` + +Returns all delegations in a pool. Each entry includes the +`creation_block_height` in addition to the standard delegation fields (but no +`pool_id`, since it is implied by the query). + +--- + +## Tokens and NFTs + +### `list_tokens` + +```python +def list_tokens(self, opts: PageOpts | None = None) -> list[str]: ... +``` + +Returns a paginated list of fungible token IDs (bech32m). + +### `get_token` + +```python +def get_token(self, token_id: str) -> TokenInfo: ... +``` + +Returns full information about a fungible token. + +```python +@dataclass(frozen=True) +class TokenInfo: + authority: str + is_locked: bool + circulating_supply: Amount + token_ticker: str + metadata_uri: str + number_of_decimals: int + total_supply: Any # raw JSON + frozen: bool + is_token_unfreezable: bool | None # non-None only when frozen + is_token_freezable: bool | None # non-None only when not frozen + next_nonce: int +``` + +### `get_token_transactions` + +```python +def get_token_transactions(self, token_id: str, opts: PageOpts | None = None) -> list[TokenTx]: ... +``` + +Returns the transaction history for a token (issuance, mints, transfers, +burns) as `TokenTx(tx_global_index, tx_id)` entries. + +### `find_tokens_by_ticker` + +```python +def find_tokens_by_ticker(self, ticker: str, opts: PageOpts | None = None) -> list[str]: ... +``` + +Returns token IDs whose ticker matches the given string. Tickers are not +unique, so this may return multiple results. + +### `get_nft` + +```python +def get_nft(self, token_id: str) -> NFTInfo: ... +``` + +Returns information about an NFT: `owner`, `token_id`, and `metadata` +(`NFTMetadata` with `creator`, `name`, `description`, `ticker`, `icon_uri`, +`additional_metadata_uri`, `media_uri`, `media_hash` — the URI/creator fields +are `None` when unset). + +--- + +## Orders + +### `list_orders` + +```python +def list_orders(self, opts: PageOpts | None = None) -> list[Order]: ... +``` + +Returns active orders. + +### `get_order` + +```python +def get_order(self, order_id: str) -> Order: ... +``` + +Returns a single order by its bech32m order ID. + +```python +@dataclass(frozen=True) +class Order: + order_id: str + conclude_destination: str + give_currency: Any # raw JSON, "type" of "Coin" or "Token" + initially_given: Amount + give_balance: Amount + ask_currency: Any # raw JSON, "type" of "Coin" or "Token" + initially_asked: Amount + ask_balance: Amount + nonce: int +``` + +### `list_orders_by_pair` + +```python +def list_orders_by_pair( + self, ask_currency: str, give_currency: str, opts: PageOpts | None = None +) -> list[Order]: ... +``` + +Returns orders filtered by a trading pair. Pass `"ML"` (the coin ticker) or a +bech32m token ID for each currency; the request path is +`/order/pair/{ask}_{give}`. + +--- + +## Statistics + +### `get_coin_statistics` + +```python +def get_coin_statistics(self) -> CoinStats: ... +``` + +Returns supply statistics for the native ML coin. + +```python +@dataclass(frozen=True) +class CoinStats: + circulating_supply: Amount + preminted: Amount + burned: Amount + staked: Amount +``` + +### `get_token_statistics` + +```python +def get_token_statistics(self, token_id: str) -> CoinStats: ... +``` + +Returns the same statistics for a fungible token. + +### `get_fee_rate` + +```python +def get_fee_rate(self, in_top_x_mb: int = 0) -> str: ... +``` + +Returns the current fee rate in atoms per kilobyte (a decimal string) needed to +place a transaction in the top `in_top_x_mb` megabytes of the mempool priority +queue. + +**Default-parameter quirk:** when `in_top_x_mb` is `0` (the default) the query +parameter is omitted entirely and the server default (5 MB) applies. + +```python +rate = int(c.get_fee_rate(1)) # atoms per KB, top 1 MB of the mempool +``` + +--- + +## Lenient numeric parsing + +The indexer documents several fields as integers but the server sometimes +serialises them as strings — and `margin_ratio_per_thousand` even arrives as a +string with a trailing `%` (e.g. `"10.0%"`). The client parses these +transparently (`mintlayer.indexer.number`): + +- `parse_uint64` — accepts a bare JSON number or a decimal string + (`block_height`, `next_nonce`, `number_of_decimals`, …), returns `int`. +- `parse_per_thousand` — accepts a bare number, a decimal string, or a string + with a trailing `%`; returns `float` (used for `Pool.margin_ratio_per_thousand`). + +Malformed values raise `IndexerError`; malformed payloads in `from_json` are +wrapped as `IndexerError` too rather than leaking `KeyError`/`TypeError`. + +--- + +## Amounts + +The `Amount` type carries both raw atoms and a human-readable decimal: + +```python +@dataclass(frozen=True) +class Amount: + atoms: str + decimal: str +``` + +All values populated by the server include both fields. When constructing +amounts to send to the server, you only need to set `atoms` +(`Amount(atoms="100000000000")`). + +--- + +## Related + +- [node.md](node.md) — node daemon JSON-RPC client +- [transactions.md](transactions.md) — building and signing transactions to submit here +- [staking.md](staking.md) — pools and delegations +- [tokens.md](tokens.md) — token and NFT lifecycle diff --git a/docs/build/sdks/python/node.md b/docs/build/sdks/python/node.md new file mode 100644 index 0000000..689b60c --- /dev/null +++ b/docs/build/sdks/python/node.md @@ -0,0 +1,548 @@ +--- +title: "Node client" +description: "Node client reference for the Mintlayer Python SDK: chainstate, mempool, P2P, block submission, and trust policy." +sidebar_position: 3 +--- + +# Node client + +The `mintlayer.node` module is a JSON-RPC 2.0 client for the Mintlayer node +daemon. Every method is synchronous and thread-safe. + +```python +from mintlayer.node import Client + +c = Client( + "http://127.0.0.1:3030", + username="user", # optional; Basic Auth applied only when set + password="pass", # optional + timeout=10.0, # optional, seconds (default 30.0) + session=None, # optional requests.Session (client owns it otherwise) +) +``` + +**Default ports:** 3030 (mainnet), 13030 (testnet). + +> **Transport security:** Basic Auth credentials travel in cleartext over +> plain `http://`. Bind the daemon to localhost or front it with an HTTPS +> reverse proxy / SSH tunnel when connecting over a network. + + +There is no per-call cancellation: configure `timeout` on the client (Go's +per-call `context` has no direct `requests` equivalent). Errors from the daemon +raise `RPCError`; transport and decode failures raise `JSONRPCError`: + +```python +from mintlayer.node import RPCError, JSONRPCError + +try: + height = c.best_block_height() +except RPCError as e: + print(e.code, e.message) +except JSONRPCError as e: + ... # HTTP failure or malformed response body +``` + +The HTTP status code is never inspected — a JSON-RPC `error` object in the body +is the error contract. JSON `null` results map to `None` for every +"not-found"-style method (`block_id_at_height`, `stake_pool_balance`, …). + +--- + +## Chain state + +### `chainstate_info` + +```python +def chainstate_info(self) -> ChainstateInfo: ... +``` + +Returns a summary of the current chain state. + +```python +@dataclass(frozen=True) +class ChainstateInfo: + best_block_height: int + best_block_id: str + best_block_timestamp: Timestamp # Timestamp(timestamp: int), Unix seconds + median_time: Timestamp + is_initial_block_download: bool +``` + +### `best_block_id` + +```python +def best_block_id(self) -> str: ... +``` + +Returns the hex block ID of the current tip. + +### `best_block_height` + +```python +def best_block_height(self) -> int: ... +``` + +Returns the height of the current tip. + +### `block_id_at_height` + +```python +def block_id_at_height(self, height: int) -> str | None: ... +``` + +Returns the block ID at a given height, or `None` if no block exists at that +height. + +### `block_height_in_main_chain` + +```python +def block_height_in_main_chain(self, block_id: str) -> int | None: ... +``` + +Returns the mainchain height for a block ID, or `None` if the block is not on +the main chain. + +### `get_block` + +```python +def get_block(self, block_id: str) -> str | None: ... +``` + +Returns the hex-encoded raw block (`None` if unknown; the genesis block is not +retrievable). + +### `get_block_json` + +```python +def get_block_json(self, block_id: str) -> Any: ... +``` + +Returns the block as decoded JSON. Useful for inspection without custom +deserialization. + +### `get_mainchain_blocks` + +```python +def get_mainchain_blocks(self, from_height: int, max_count: int) -> list[str]: ... +``` + +Returns up to `max_count` mainchain block IDs starting at `from_height`. + +### `get_utxo` + +```python +def get_utxo(self, outpoint: Outpoint) -> Any: ... +``` + +Returns the output at a given outpoint as decoded JSON (`None` if +spent/unknown). + +```python +@dataclass(frozen=True) +class Outpoint: + source_id: OutpointSourceID + index: int + + +@dataclass +class OutpointSourceID: + type: str # "Transaction" or "BlockReward" + content: Any # {"tx_id": ""} or {"block_id": ""} +``` + +Build the `content` payloads with the helpers: + +```python +from mintlayer.node import Outpoint, OutpointSourceID, tx_source_content, block_source_content + +op = Outpoint( + source_id=OutpointSourceID(type="Transaction", content=tx_source_content(tx_id)), + index=0, +) +utxo = c.get_utxo(op) +``` + +### `submit_block` + +```python +def submit_block(self, block_hex: str) -> None: ... +``` + +Submits a hex-encoded block. Used by block producers. + +--- + +## Pool and delegation queries + +### `stake_pool_balance` + +```python +def stake_pool_balance(self, pool_address: str) -> Amount | None: ... +``` + +Returns the total balance of a pool (staker pledge plus all delegations). +Returns `None` if the pool is not found. + +### `staker_balance` + +```python +def staker_balance(self, pool_address: str) -> Amount | None: ... +``` + +Returns the staker's own balance, excluding delegations. Returns `None` if the +pool is not found. + +### `pool_decommission_destination` + +```python +def pool_decommission_destination(self, pool_address: str) -> str | None: ... +``` + +Returns the address that receives funds when the pool is decommissioned. + +### `delegation_share` + +```python +def delegation_share(self, pool_address: str, delegation_address: str) -> Amount | None: ... +``` + +Returns the amount owned by a specific delegation in a pool. + +--- + +## Token and order info + +Amounts are decimal atom strings — **1 ML = 100,000,000,000 atoms** (11 +decimal places). `Amount` is a frozen dataclass with a single `atoms: str` +field. + +### `token_info` + +```python +def token_info(self, token_id: str) -> TokenInfo | None: ... +``` + +Returns on-chain token metadata (`None` if unknown). + +```python +@dataclass +class TokenInfo: + type: str # "FungibleToken" or "NonFungibleToken" + content: Any # raw decoded JSON +``` + +### `tokens_info` + +```python +def tokens_info(self, token_ids: list[str]) -> list[TokenInfo]: ... +``` + +Batch version of `token_info`. More efficient than calling `token_info` in a +loop. + +### `order_info` + +```python +def order_info(self, order_id: str) -> OrderInfo | None: ... +``` + +Returns the current state of an order. + +```python +@dataclass +class OrderInfo: + conclude_key: str + initially_asked: Any + initially_given: Any + ask_balance: Amount + give_balance: Amount + nonce: int | None # None for active orders (daemon sends null) + is_frozen: bool +``` + +Quirk: `nonce` is `None` for active orders — the daemon sends JSON `null` for +the field. This fixes a known Go SDK incompatibility, where the `null` broke +`uint64` decoding. + +### `orders_info_by_currencies` + +```python +def orders_info_by_currencies( + self, ask: Currency | None, give: Currency | None +) -> dict[str, OrderInfo]: ... +``` + +Returns all orders matching the given currency pair, as a dict from order ID to +`OrderInfo`. Pass `None` for either currency to match any (both keys are always +sent; `None` serialises as JSON `null`). + +```python +@dataclass(frozen=True) +class Currency: + type: str # "Coin" or "Token" + content: str | None # bech32 token ID when type is "Token" +``` + +Construct with the helpers: + +```python +from mintlayer.node import Currency + +orders = c.orders_info_by_currencies(Currency.coin(), Currency.token("ttml1...")) +any_coin = c.orders_info_by_currencies(None, Currency.coin()) +``` + +--- + +## Mempool + +### `contains_tx` + +```python +def contains_tx(self, tx_id: str) -> bool: ... +``` + +Returns `True` if the mempool contains the transaction. + +### `contains_orphan_tx` + +```python +def contains_orphan_tx(self, tx_id: str) -> bool: ... +``` + +Returns `True` if the orphan pool contains the transaction. + +### `get_transaction` + +```python +def get_transaction(self, tx_id: str) -> MempoolTx | None: ... +``` + +Returns a mempool transaction (`None` if not present). + +```python +@dataclass(frozen=True) +class MempoolTx: + id: str + status: str + transaction: str +``` + +### `mempool_submit_transaction` + +```python +def mempool_submit_transaction(self, tx_hex: str, trust_policy: TrustPolicy | str) -> None: ... +``` + +Submits a transaction to the local mempool only, without broadcasting to peers. +Use `TrustPolicy.UNTRUSTED` for transactions you constructed yourself; use +`TrustPolicy.TRUSTED` to skip some fee checks. A plain string is accepted too: + +```python +from mintlayer.node import TrustPolicy + +c.mempool_submit_transaction(signed_hex, TrustPolicy.UNTRUSTED) +``` + +```python +class TrustPolicy(str, enum.Enum): + TRUSTED = "Trusted" + UNTRUSTED = "Untrusted" +``` + +### `get_fee_rate` + +```python +def get_fee_rate(self, in_top_x_mb: int) -> FeeRate | None: ... +``` + +Returns the fee rate needed to land in the top `in_top_x_mb` megabytes of the +mempool. + +```python +@dataclass(frozen=True) +class FeeRate: + amount_per_kb: Amount # atoms per kilobyte +``` + +### `get_fee_rate_points` + +```python +def get_fee_rate_points(self) -> list[FeeRatePoint]: ... +``` + +Returns the mempool fee-rate curve as a list of (size, rate) pairs. + +Wire quirk: the daemon sends each point as a two-element array +`[size, {"amount_per_kb": {...}}]`, decoded into: + +```python +@dataclass(frozen=True) +class FeeRatePoint: + size: int + rate: FeeRate +``` + +### `memory_usage` + +```python +def memory_usage(self) -> int: ... +``` + +Returns the current mempool memory usage in bytes. + +--- + +## P2P + +### `get_peer_count` + +```python +def get_peer_count(self) -> int: ... +``` + +Returns the number of currently connected peers. + +### `get_connected_peers` + +```python +def get_connected_peers(self) -> list[PeerInfo]: ... +``` + +Returns details about all connected peers. + +```python +@dataclass(frozen=True) +class PeerInfo: + peer_id: int + address: str + peer_role: str + ban_score: int + user_agent: str + software_version: str + ping_wait: int | None = None + ping_last: int | None = None + ping_min: int | None = None + last_tip_block_time: int | None = None +``` + +### `get_bind_addresses` + +```python +def get_bind_addresses(self) -> list[str]: ... +``` + +Returns the addresses the node is listening on for P2P connections. + +### `add_reserved_node` + +```python +def add_reserved_node(self, addr: str) -> None: ... +``` + +Adds a persistent peer that the node always attempts to reconnect to. + +### `remove_reserved_node` + +```python +def remove_reserved_node(self, addr: str) -> None: ... +``` + +Removes a persistent peer. + +### `connect` + +```python +def connect(self, addr: str) -> None: ... +``` + +Makes a one-time connection attempt to a peer address. + +### `disconnect` + +```python +def disconnect(self, peer_id: int) -> None: ... +``` + +Closes the connection to a peer by ID. + +### `list_banned` + +```python +def list_banned(self) -> list[BannedPeer]: ... +``` + +Returns the list of banned peers. + +Wire quirk: each entry is a two-element array +`["
", {"time": [secs, nanos]}]`, decoded into: + +```python +@dataclass(frozen=True) +class BannedPeer: + address: str + ban_time: tuple[int, int] # (seconds, nanoseconds) +``` + +### `ban` + +```python +def ban(self, address: str, duration: timedelta) -> None: ... +``` + +Bans a peer for the specified duration. Durations are `datetime.timedelta` +values, split into the daemon's `[seconds, nanoseconds]` wire form: + +```python +from datetime import timedelta + +c.ban("192.0.2.1", timedelta(hours=24)) +``` + +### `unban` + +```python +def unban(self, address: str) -> None: ... +``` + +Removes a peer from the ban list. + +### `p2p_submit_transaction` + +```python +def p2p_submit_transaction(self, tx_hex: str, trust_policy: TrustPolicy | str) -> None: ... +``` + +Submits a transaction to the mempool and broadcasts it to peers. This is the +normal path for publishing a transaction to the network (the alternative — the +indexer's `submit_transaction` — requires `--enable-post-routes`, see +[indexer.md](indexer.md)). + +--- + +## Node management + +### `node_version` + +```python +def node_version(self) -> str: ... +``` + +Returns the node software version string. + +### `node_shutdown` + +```python +def node_shutdown(self) -> None: ... +``` + +Initiates a graceful node shutdown. + +--- + +## Related + +- [indexer.md](indexer.md) — read-only chain queries and `submit_transaction` +- [transactions.md](transactions.md) — building and signing transactions +- [wallet.md](wallet.md) — the wallet daemon client diff --git a/docs/build/sdks/python/staking.md b/docs/build/sdks/python/staking.md new file mode 100644 index 0000000..a99c300 --- /dev/null +++ b/docs/build/sdks/python/staking.md @@ -0,0 +1,323 @@ +--- +title: "Staking" +description: "Staking reference for the Mintlayer Python SDK: pools, delegations, withdrawals, and manual encoders." +sidebar_position: 8 +--- + +# Staking and delegations + +Mintlayer uses Proof of Stake. ML holders can earn staking rewards by either: + +- Running a **staking pool** directly (requires a node, VRF key, and pledge) +- **Delegating** to an existing pool (no node required) + +The wallet daemon manages both flows. The indexer provides read-only access to +pool and delegation state. The `mintlayer.wasm` module provides low-level +primitives for manual transaction flows. + +--- + +## Staking pools (wallet daemon) + +### Creating a pool + +```python +from mintlayer.wallet import Amount, Client, CreatePoolParams + +wc = Client("http://127.0.0.1:3034") + +result = wc.create_stake_pool( + CreatePoolParams( + account=0, + amount=Amount(atoms="40000000000000"), # minimum pledge + cost_per_block=Amount(atoms="100000000"), # flat fee per block + margin_ratio_per_thousand="100", # 10% staker cut + decommission_address=decommission_addr, + # staker_address and vrf_public_key default to wallet-managed keys + # when None (sent as JSON null) + ) +) +print(f"tx id: {result.tx_id}") +``` + +`margin_ratio_per_thousand` is the staker's cut of block rewards expressed in +thousandths, as a **string**: `"100"` = 10%, `"50"` = 5%, `"1000"` = 100%. + +`cost_per_block` is a flat atom amount deducted from rewards before the margin +split. Delegators receive the remainder proportionally to their stake. + +The result is a `SendResult(tx_id, fees, broadcasted)` — the pool ID can be +predicted before broadcasting with `get_pool_id` (see below). + +### Starting and stopping block production + +```python +wc.start_staking(0) # account 0 + +status = wc.get_staking_status(0) +print(status) # StakingStatus.ACTIVE ("Staking") or StakingStatus.INACTIVE ("NotStaking") + +wc.stop_staking(0) +``` + +`stop_staking` stops block production but does not decommission the pool. +Delegations and staked funds remain untouched. + +### Listing owned pools + +```python +pools = wc.list_owned_pools(0) +for p in pools: + print(f"pool {p.pool_id} pledge={p.pledge.atoms} balance={p.balance.atoms}") +``` + +### Pool balance + +```python +balance = wc.get_pool_balance(0, "mpool1...") # Amount +``` + +Quirk: matching the daemon route, the `account` argument is accepted for API +consistency but **not sent on the wire** — the route only uses `pool_id`. + +### Decommissioning a pool + +```python +from mintlayer.wallet import DecommissionParams + +result = wc.decommission_stake_pool( + DecommissionParams( + account=0, + pool_id="mpool1...", + output_address=return_addr, + ) +) +``` + +After decommissioning, the pledge is returned to `output_address` after the +maturity period. Delegators must withdraw their funds separately. + +### Reading pool state from the indexer + +```python +from datetime import datetime, timedelta + +from mintlayer.indexer import Client, PoolListOpts + +idx = Client("http://127.0.0.1:3000") + +# All pools sorted by pledge size +pools = idx.list_pools(PoolListOpts(sort="by_pledge")) + +# Single pool +pool = idx.get_pool("mpool1...") +print(f"staker balance: {pool.staker_balance.decimal}") +print(f"delegations: {pool.delegations_balance.decimal}") + +# Blocks produced in the last 24 hours +count = idx.get_pool_block_stats( + "mpool1...", + datetime.now() - timedelta(hours=24), + datetime.now(), +) + +# All delegations in the pool +delegations = idx.get_pool_delegations("mpool1...") +``` + +--- + +## Delegations (wallet daemon) + +### Creating a delegation + +A delegation ID is tied to a pool and an owner address. You create the +delegation record first, then fund it separately. + +```python +from mintlayer.wallet import Amount, CreateDelegationParams, DelegateParams + +# Step 1: create the delegation +create_result = wc.create_delegation( + CreateDelegationParams( + account=0, + address=owner_addr, # address that can withdraw funds + pool_id="mpool1...", + ) +) +print(f"delegation id: {create_result.delegation_id}") + +# Step 2: fund the delegation (wait for the creation tx to confirm first) +delegate_result = wc.delegate_staking( + DelegateParams( + account=0, + amount=Amount(atoms="10000000000000"), # 100 ML + delegation_id=create_result.delegation_id, + ) +) +``` + +You can send multiple `delegate_staking` transactions to the same delegation to +increase your stake. + +### Withdrawing from a delegation + +```python +from mintlayer.wallet import WithdrawParams + +result = wc.withdraw_from_delegation( + WithdrawParams( + account=0, + address=recipient_addr, + amount=Amount(atoms="5000000000000"), # 50 ML + delegation_id="mdelg1...", + ) +) +``` + +Withdrawn funds arrive at `address` after the lock period (determined by +consensus rules). + +### Listing delegations + +```python +delegations = wc.list_delegations(0) +for d in delegations: + print(f"delegation {d.delegation_id} pool={d.pool_id} balance={d.balance.atoms}") +``` + +### Reading delegation state from the indexer + +```python +# All delegations owned by an address +delegation_infos = idx.get_delegations("mtc1qowner...") + +# Single delegation by ID +delegation = idx.get_delegation("mdelg1...") +print(f"pool: {delegation.pool_id}") +print(f"balance: {delegation.balance.decimal}") +print(f"nonce: {delegation.next_nonce}") +``` + +--- + +## Building delegation transactions manually + +The `mintlayer.wasm` module provides the low-level primitives when you need to +build delegation transactions without the wallet daemon. + +### Create a delegation + +```python +from mintlayer.wasm import Client, Network + +c = Client() +try: + # Predict the delegation ID before broadcasting + delegation_id_str = c.get_delegation_id(encoded_inputs, Network.MAINNET) + + # Encode the CreateDelegationId output + create_deleg_output = c.encode_output_create_delegation( + "mpool1...", + owner_address, + Network.MAINNET, + ) +finally: + c.close() +``` + +### Fund a delegation + +```python +delegate_output = c.encode_output_delegate_staking( + Amount.from_atoms("10000000000000"), + "mdelg1...", + Network.MAINNET, +) +``` + +### Withdraw from a delegation + +```python +# The nonce must match the current next_nonce from the indexer +delegation = idx.get_delegation("mdelg1...") + +withdraw_input = c.encode_input_for_withdraw_from_delegation( + "mdelg1...", + Amount.from_atoms("5000000000000"), + delegation.next_nonce, + Network.MAINNET, +) + +# The output receives the withdrawn coins after the lock period +withdraw_output = c.encode_output_lock_then_transfer( + Amount.from_atoms("5000000000000"), + recipient_address, + lock, # from encode_lock_for_block_count or similar + Network.MAINNET, +) +``` + +Assemble, sign, and submit the resulting inputs/outputs exactly as in +[transactions.md](transactions.md). + +--- + +## Manual pool creation + +For full-custody pool creation, encode the pool parameters and wrap them in a +`CreateStakePool` output. `encode_stake_pool_data` takes the pool value +(pledge), the staker/VRF/decommission keys, the margin ratio, and the per-block +cost: + +```python +from mintlayer.wasm import Amount, Network + +pool_id = c.get_pool_id(encoded_inputs, Network.MAINNET) + +pool_data = c.encode_stake_pool_data( + value=Amount.from_atoms("40000000000000"), + staker=staker_addr, + vrf_public_key=vrf_key, + decommission_key=decommission_addr, + margin_ratio_per_thousand=100, # int here (wallet daemon takes a string) + cost_per_block=Amount.from_atoms("100000000"), + network=Network.MAINNET, +) + +pool_output = c.encode_output_create_stake_pool(pool_id, pool_data, Network.MAINNET) +``` + +Two helpers are useful when planning a pool: + +```python +# Effective balance used in the slot lottery (diminishing returns for big pools) +eff = c.effective_pool_balance(Network.MAINNET, pledge_amount, pool_balance) + +# Blocks a pool output must mature after decommission before funds are spendable +maturity = c.staking_pool_spend_maturity_block_count(tip.block_height, Network.MAINNET) +``` + +--- + +## Checking rewards + +The indexer does not expose a rewards endpoint directly. To calculate staking +rewards: + +1. Get pool block stats for a time range (`get_pool_block_stats`) +2. Get the pool's cost per block and margin ratio (`get_pool`) +3. Get your delegation's share of the total pool balance (`get_delegation`) + +The staker receives `cost_per_block + margin_ratio_per_thousand/1000 * +(block_reward - cost_per_block)`. Delegators split the remainder +proportionally to their stake. + +--- + +## Related + +- [wallet.md](wallet.md) — wallet client reference +- [indexer.md](indexer.md) — pool/delegation read queries +- [wasm.md](wasm.md) — WASM encoding reference +- [transactions.md](transactions.md) — assembling and signing manual transactions diff --git a/docs/build/sdks/python/tokens.md b/docs/build/sdks/python/tokens.md new file mode 100644 index 0000000..2fee117 --- /dev/null +++ b/docs/build/sdks/python/tokens.md @@ -0,0 +1,383 @@ +--- +title: "Tokens and NFTs" +description: "Tokens reference for the Mintlayer Python SDK: fungible token and NFT lifecycle, and manual encoders." +sidebar_position: 9 +--- + +# Tokens and NFTs + +Mintlayer supports on-chain fungible tokens and NFTs. The wallet daemon manages +the full lifecycle. The `mintlayer.wasm` module provides low-level encoding for +manual transaction flows. + +--- + +## Fungible tokens (wallet daemon) + +### Supply policies + +When issuing a token you choose one of three supply policies +(`TokenSupply` dataclass, wire field `type`): + +| Policy | `TokenSupply.type` | Description | +|--------|--------------------|-------------| +| Lockable | `"Lockable"` | Unlimited minting until `lock_token_supply` is called, after which the supply is frozen permanently | +| Unlimited | `"Unlimited"` | Minting is always allowed with no cap | +| Fixed | `"Fixed"` | A cap is set at issuance; supply cannot exceed it | + +For a `Fixed` cap, set the `content` field to an `Amount`: + +```python +TokenSupply(type="Fixed", content=Amount(atoms="1000000")) # hard cap +``` + +`content` is omitted from the wire for the other two policies. + +### Issuing a token + +```python +from mintlayer.wallet import ( + Amount, + Client, + IssueTokenParams, + TokenMetadata, + TokenSupply, +) + +wc = Client("http://127.0.0.1:3034") + +authority_addr = wc.new_address(0) + +result = wc.issue_token( + IssueTokenParams( + account=0, + destination_address=authority_addr, + metadata=TokenMetadata( + token_ticker="MYTOKEN", + number_of_decimals=2, + metadata_uri="https://example.com/token-metadata.json", + token_supply=TokenSupply(type="Lockable"), + is_freezable=True, + ), + ) +) +print(f"token id: {result.token_id}\ntx id: {result.tx_id}") +``` + +The `destination_address` becomes the **authority address**: the key that +controls future token operations (minting, freezing, authority transfer). Keep +it secure. + +### Minting + +Wait for the issuance transaction to confirm before minting. + +```python +from mintlayer.wallet import MintParams + +mint_result = wc.mint_tokens( + MintParams( + account=0, + token_id=result.token_id, + address=recipient_addr, + amount=Amount(atoms="100000"), # in smallest token units + ) +) +print(f"minted tx id: {mint_result.tx_id}") +``` + +### Unminting + +Returns tokens to an unminted state (removes them from circulation without +burning). + +```python +from mintlayer.wallet import UnmintParams + +unmint_result = wc.unmint_tokens( + UnmintParams( + account=0, + token_id="ttml1...", + amount=Amount(atoms="50000"), + ) +) +``` + +### Locking supply + +After locking, the supply policy becomes `Fixed` at the current circulating +supply. This operation is irreversible. + +```python +from mintlayer.wallet import LockSupplyParams + +lock_result = wc.lock_token_supply( + LockSupplyParams( + account_index=0, + token_id="ttml1...", + ) +) +``` + +Quirk: the field is `account_index`, not `account` — the daemon route expects +the wire key `account_index` (every other token method uses `account`). + +### Freezing and unfreezing + +Freezing prevents all transfers. If `is_unfreezable` is `True`, the authority +can unfreeze later. + +```python +from mintlayer.wallet import FreezeParams, UnfreezeParams + +freeze_result = wc.freeze_token( + FreezeParams( + account=0, + token_id="ttml1...", + is_unfreezable=True, + ) +) + +unfreeze_result = wc.unfreeze_token( + UnfreezeParams( + account=0, + token_id="ttml1...", + ) +) +``` + +### Transferring authority + +```python +from mintlayer.wallet import ChangeAuthorityParams + +change_result = wc.change_token_authority( + ChangeAuthorityParams( + account=0, + token_id="ttml1...", + address=new_authority_addr, + ) +) +``` + +### Sending tokens + +```python +from mintlayer.wallet import TokenSendParams + +send_result = wc.send_token( + TokenSendParams( + account=0, + token_id="ttml1...", + address=recipient_addr, + amount=Amount(atoms="10000"), + ) +) +``` + +`send_token` is an alias of `token_send` on the transactions mixin — both call +the daemon's `token_send` route. + +--- + +## NFTs (wallet daemon) + +NFTs are non-fungible tokens. Each has unique on-chain metadata. + +### Issuing an NFT + +```python +from mintlayer.wallet import IssueNFTParams, NFTMetadata + +owner_addr = wc.new_address(0) + +result = wc.issue_nft( + IssueNFTParams( + account=0, + destination_address=owner_addr, + metadata=NFTMetadata( + name="My NFT", + description="A unique digital collectible", + ticker="MYNFT", + media_hash="sha256hexhash...", + media_uri="https://example.com/media.png", + icon_uri="https://example.com/icon.png", + # creator and additional_metadata_uri default to None + ), + ) +) +print(f"nft id: {result.token_id}") +``` + +NFTs cannot be minted after issuance: each issuance transaction creates exactly +one NFT. + +--- + +## Reading token state from the indexer + +```python +from mintlayer.indexer import Client, PageOpts + +idx = Client("http://127.0.0.1:3000") + +# Find by ticker +ids = idx.find_tokens_by_ticker("MYTOKEN", PageOpts(items=10)) + +# Full token info +token = idx.get_token("ttml1...") +print(f"ticker: {token.token_ticker}") +print(f"supply: {token.circulating_supply.decimal}") +print(f"locked: {token.is_locked}") +print(f"frozen: {token.frozen}") + +# Transaction history +txs = idx.get_token_transactions("ttml1...", PageOpts(items=20)) + +# NFT +nft = idx.get_nft("nftid1...") +print(f"owner: {nft.owner}") +print(f"name: {nft.metadata.name}") + +# Tokens where address is authority +token_ids = idx.get_token_authority("mtc1qauthority...") +``` + +--- + +## Building token transactions manually + +Use the `mintlayer.wasm` module for full control over token transactions. + +### Issue a token + +```python +from mintlayer.wasm import Amount, Client, FreezableToken, Network, TotalSupply + +c = Client() +try: + tip = idx.get_tip() + + issuance_fee = c.fungible_token_issuance_fee(tip.block_height, Network.MAINNET) + + # Predict the token ID + token_id_str = c.get_token_id(encoded_inputs, tip.block_height, Network.MAINNET) + + issue_output = c.encode_output_issue_fungible_token( + authority_addr, + "MYTOKEN", + "https://example.com/metadata.json", + 2, # decimals + TotalSupply.LOCKABLE, + None, # supply_amount: only required for TotalSupply.FIXED + FreezableToken.YES, + tip.block_height, + Network.MAINNET, + ) +finally: + c.close() +``` + +For a capped supply, pass the cap as the `supply_amount`: + +```python +issue_output = c.encode_output_issue_fungible_token( + authority_addr, + "MYTOKEN", + "https://example.com/metadata.json", + 2, + TotalSupply.FIXED, + Amount.from_atoms("1000000"), # required exactly for TotalSupply.FIXED + FreezableToken.NO, + tip.block_height, + Network.MAINNET, +) +``` + +Include the issuance fee as a separate output or subtract it from an input +UTXO. + +### Issue an NFT + +```python +issue_nft_output = c.encode_output_issue_nft( + token_id=token_id_str, # derived with get_token_id (same scheme as FTs) + authority=owner_addr, + name="My NFT", + ticker="MYNFT", + description="A unique digital collectible", + media_hash=bytes.fromhex("..."), # 32-byte sha256 of the media + creator=bytes.fromhex("..."), # or None + media_uri="https://example.com/media.png", + icon_uri=None, + additional_metadata_uri=None, + current_block_height=tip.block_height, + network=Network.MAINNET, +) +``` + +### Mint tokens + +```python +# Get the current nonce from the indexer +token_info = idx.get_token(token_id_str) + +mint_input = c.encode_input_for_mint_tokens( + token_id_str, + Amount.from_atoms("100000"), + token_info.next_nonce, + Network.MAINNET, +) + +mint_output = c.encode_output_token_transfer( + Amount.from_atoms("100000"), + recipient_addr, + token_id_str, + Network.MAINNET, +) +``` + +### Freeze a token + +```python +from mintlayer.wasm import TokenUnfreezable + +freeze_input = c.encode_input_for_freeze_token( + token_id_str, + TokenUnfreezable.YES, # can be unfrozen later + token_info.next_nonce, + Network.MAINNET, +) +``` + +Assemble, sign, and submit manual token transactions exactly as in +[transactions.md](transactions.md). + +--- + +## Token fees + +Protocol fees apply to many token operations. Query them from the WASM client +before building transactions: + +```python +tip = idx.get_tip() + +issuance_fee = c.fungible_token_issuance_fee(tip.block_height, Network.MAINNET) +nft_fee = c.nft_issuance_fee(tip.block_height, Network.MAINNET) +mint_fee = c.token_supply_change_fee(tip.block_height, Network.MAINNET) +freeze_fee = c.token_freeze_fee(tip.block_height, Network.MAINNET) +authority_fee = c.token_change_authority_fee(tip.block_height, Network.MAINNET) +``` + +These fees must be included as coin inputs in the transaction (or deducted +from change). The `data_deposit_fee` applies to `DataDeposit` outputs. + +--- + +## Related + +- [wallet.md](wallet.md) — wallet client reference +- [indexer.md](indexer.md) — token read queries +- [wasm.md](wasm.md) — WASM encoding reference +- [transactions.md](transactions.md) — assembling and signing manual transactions diff --git a/docs/build/sdks/python/transactions.md b/docs/build/sdks/python/transactions.md new file mode 100644 index 0000000..f301d4c --- /dev/null +++ b/docs/build/sdks/python/transactions.md @@ -0,0 +1,282 @@ +--- +title: "Building transactions" +description: "Step-by-step transaction building with the Mintlayer Python SDK: UTXOs, fees, signing, and submission without the wallet daemon." +sidebar_position: 7 +--- + +# Building transactions manually + +The wallet daemon handles transaction building automatically for most use +cases. Use the `mintlayer.wasm` module directly when you need: + +- Full custody (no wallet daemon) +- Custom output types or complex spending conditions +- Integration testing or tooling + +The [examples/send_coins.py](https://github.com/mintlayer/python-sdk/tree/main/examples/send_coins.py) program demonstrates +this flow end to end. + +--- + +## Overview + +Building a transaction manually requires these steps: + +1. Derive the spending key and address from a mnemonic +2. Fetch spendable UTXOs from the indexer +3. Encode each input as binary +4. Encode each output as binary +5. Build the unsigned transaction +6. Sign each input to produce witness bytes +7. Assemble the signed transaction +8. Submit to the network + +--- + +## Step 1: Key derivation + +```python +from mintlayer.wasm import Client, Network + +c = Client() +try: + mnemonic = "word1 word2 ... word12" + + account_key = c.make_default_account_privkey(mnemonic, Network.MAINNET) + + # key index 0 = first receiving address + spend_key = c.make_receiving_address(account_key, 0) + pub_key = c.public_key_from_private_key(spend_key) + from_addr = c.pubkey_to_pubkeyhash_address(pub_key, Network.MAINNET) +finally: + c.close() +``` + +--- + +## Step 2: Fetch spendable UTXOs + +```python +from mintlayer.indexer import Client as IndexerClient + +idx = IndexerClient("http://127.0.0.1:3000") + +utxos = idx.get_spendable_utxos(from_addr) +if not utxos: + raise RuntimeError("no spendable UTXOs") +``` + +Each `UTXO` carries `outpoint` (`source_id` hex string + `index`) and `output` +(raw JSON). + +--- + +## Step 3: Encode inputs + +Each input requires: + +1. Hex-decode the source transaction ID +2. Encode the outpoint source ID (`encode_outpoint_source_id`) +3. Encode the input (`encode_input_for_utxo`) + +**Concatenation semantics:** inputs (and outputs, and witnesses) are plain +binary blobs — the concatenation of every per-item encoding **is** the +transaction field. There is no count prefix or separator; just append with +`+=`. + +```python +from mintlayer.wasm import SOURCE_TRANSACTION + +encoded_inputs = b"" +for u in utxos: + tx_id_bytes = bytes.fromhex(u.outpoint.source_id) + + src_id = c.encode_outpoint_source_id(tx_id_bytes, SOURCE_TRANSACTION) + inp = c.encode_input_for_utxo(src_id, u.outpoint.index) + encoded_inputs += inp +``` + +--- + +## Step 4: Encode outputs + +```python +from mintlayer.wasm import Amount + +output = c.encode_output_transfer( + Amount(atoms="100000000000"), # 1 ML in atoms + "mtc1qrecipient...", + Network.MAINNET, +) +``` + +For multiple outputs, concatenate them: + +```python +change_output = c.encode_output_transfer( + Amount(atoms=str(change_atoms)), + from_addr, # send change back to sender + Network.MAINNET, +) + +all_outputs = output + change_output +``` + +--- + +## Step 5: Build the unsigned transaction + +```python +tx = c.encode_transaction(encoded_inputs, output, 0) # flags = 0 + +tx_id = c.get_transaction_id(tx, True) +print(f"unsigned tx id: {tx_id}") +``` + +--- + +## Step 6: Prepare UTXO bytes for signing + +The sighash computation requires access to the UTXO being spent. Build a +per-input blob where each entry is prefixed with either: + +- `0x01` followed by the re-encoded output bytes (recommended for coin + transfers) +- `0x00` alone (acceptable for some output types) + +```python +def encode_utxo_entry(c: Client, utxo_json: dict, network: Network) -> bytes: + try: + if utxo_json.get("type") == "Transfer": + value = utxo_json["value"] + if value.get("type") == "Coin": + encoded = c.encode_output_transfer( + Amount(atoms=value["amount"]["atoms"]), + value["destination"], + network, + ) + return b"\x01" + encoded + except (KeyError, TypeError, ValueError): + pass + return b"\x00" + + +all_utxo_bytes = b"" +for u in utxos: + all_utxo_bytes += encode_utxo_entry(c, u.output, Network.MAINNET) +``` + +The order of these entries must match the input order exactly — witness `i` is +validated against entry `i`. + +--- + +## Step 7: Sign each input + +Call `encode_witness` once per input. Concatenate results. + +```python +from mintlayer.wasm import SignatureHashType, TxAdditionalInfo + +witness_bytes = b"" +for i in range(len(utxos)): + w = c.encode_witness( + SignatureHashType.SIGHASH_ALL, + spend_key, + from_addr, + tx, + all_utxo_bytes, + i, # input index + TxAdditionalInfo(), # empty for standard transfers + 0, # block height (0 when no timelock constraint) + Network.MAINNET, + ) + witness_bytes += w +``` + +--- + +## Step 8: Assemble and submit + +```python +signed_tx = c.encode_signed_transaction(tx, witness_bytes) +signed_hex = signed_tx.hex() + +# Submit via the indexer (requires --enable-post-routes) +submitted_tx_id = idx.submit_transaction(signed_hex) +print(f"submitted: {submitted_tx_id}") + +# Alternative: broadcast via the node daemon +# node_client.p2p_submit_transaction(signed_hex, TrustPolicy.UNTRUSTED) +``` + +See [indexer.md](indexer.md) for the submit route requirements and +[node.md](node.md) for the P2P alternative. + +--- + +## Fee estimation + +Compute the fee before constructing outputs so you can deduct it from the +change: + +```python +# Collect destination addresses (one per input, in input order) +dest_addresses = [from_addr] * len(utxos) + +estimated_size = c.estimate_transaction_size( + encoded_inputs, dest_addresses, all_outputs, Network.MAINNET +) + +# get_fee_rate returns atoms per KB for the top 1 MB of the mempool +fee_rate = int(idx.get_fee_rate(1)) + +fee = estimated_size * fee_rate // 1000 + +# Subtract fee from the amount going to the recipient or from the change output. +``` + +--- + +## Lock-then-transfer outputs + +To send coins that cannot be spent for a period of time: + +```python +# Unlock after 1000 blocks +lock = c.encode_lock_for_block_count(1000) + +output = c.encode_output_lock_then_transfer( + Amount(atoms="100000000000"), + "mtc1qrecipient...", + lock, + Network.MAINNET, +) +``` + +--- + +## Token transfers + +Sending fungible tokens uses the same flow, with a different output encoder: + +```python +token_output = c.encode_output_token_transfer( + Amount(atoms="1000"), # token amount in smallest units + "mtc1qrecipient...", + "ttml1tokenid...", + Network.MAINNET, +) +``` + +Note that a token transfer transaction must also include a coin output (or +coin inputs) to cover the network fee. + +--- + +## Related + +- [wasm.md](wasm.md) — full WASM client reference +- [staking.md](staking.md) — manual delegation/pool transactions +- [tokens.md](tokens.md) — manual token issuance/minting +- [wallet.md](wallet.md) — let the wallet daemon do all of this for you diff --git a/docs/build/sdks/python/wallet.md b/docs/build/sdks/python/wallet.md new file mode 100644 index 0000000..b7b3af9 --- /dev/null +++ b/docs/build/sdks/python/wallet.md @@ -0,0 +1,633 @@ +--- +title: "Wallet client" +description: "Wallet client reference for the Mintlayer Python SDK: lifecycle, accounts, balances, transactions, staking, tokens, orders." +sidebar_position: 5 +--- + +# Wallet client + +The `mintlayer.wallet` module is a JSON-RPC 2.0 client for the Mintlayer wallet +daemon (`wallet-rpc-daemon`). The daemon manages key storage, address +derivation, signing, and broadcasting. + +```python +from mintlayer.wallet import Client + +c = Client( + "http://127.0.0.1:3034", + username="user", # optional; Basic Auth applied only when set + password="pass", # optional + timeout=30.0, # optional, seconds (default 30.0) + session=None, # optional requests.Session (client owns it otherwise) +) +``` + +**Default ports:** 3034 (mainnet), 13034 (testnet). + +Errors returned by the daemon raise `RPCError`; transport/decode failures raise +`JSONRPCError`: + +```python +from mintlayer.wallet import RPCError, JSONRPCError + +try: + balance = c.get_balance(0) +except RPCError as e: + print(e.code, e.message) +except JSONRPCError as e: + ... +``` + +The client is safe for concurrent use from multiple threads and supports the +context-manager protocol (`with Client(...) as c: ...`). + +> **Security:** the daemon has no transport encryption. If it listens beyond +> localhost, put it behind an HTTPS reverse proxy or an SSH tunnel; only talk +> to `https://…` endpoints (or plain `http://127.0.0.1`/localhost) — never to a +> bare `http://` host over a network. Mnemonic and passphrase values are +> **redacted from `repr()`**: `CreateWalletParams`, `RecoverWalletParams` and +> the returned `MnemonicResult` print `` instead of the secret, so +> logging a params object never leaks the seed phrase. + +--- + +## Wallet lifecycle + +Typical flow: **create or recover → open → sync → use**. + +```python +from mintlayer.wallet import Client, CreateWalletParams + +c = Client("http://127.0.0.1:3034") + +# 1. Create a wallet file (a fresh 24-word BIP-39 phrase is generated +# when params.mnemonic is None). +result = c.create_wallet( + CreateWalletParams( + path="/path/to/wallet.dat", + store_seed_phrase=False, # don't persist the phrase to disk + ) +) +if result.mnemonic: + print(result.mnemonic.content.mnemonic) # store it securely NOW + +# 2. (next session) Open the wallet. Empty password → sent as JSON null. +c.open_wallet("/path/to/wallet.dat", password="") + +# 3. Sync to the chain tip. +c.sync_wallet() + +# 4. ... accounts, addresses, sends ... + +c.close_wallet() +``` + +### `create_wallet` + +```python +def create_wallet(self, params: CreateWalletParams) -> CreateWalletResult: ... +``` + +Creates a new wallet file. If `mnemonic` is `None`, the daemon generates a +fresh 24-word BIP-39 phrase. + +```python +@dataclass(frozen=True) +class CreateWalletParams: + path: str + store_seed_phrase: bool + mnemonic: str | None = None + passphrase: str | None = None + hardware_wallet: str | None = None + + +@dataclass(frozen=True) +class CreateWalletResult: + mnemonic: MnemonicResult | None = None +``` + +When `store_seed_phrase` is `False`, the mnemonic is returned in +`CreateWalletResult.mnemonic` and not persisted to disk. Store it securely. +Optional pointer fields are always sent, serialising as JSON `null` when unset +(wire-fidelity with the Go structs). + +### `recover_wallet` + +```python +def recover_wallet(self, params: RecoverWalletParams) -> None: ... +``` + +Recreates a wallet from an existing mnemonic (`RecoverWalletParams` has the +same fields as `CreateWalletParams` but `mnemonic` is required). The daemon +will rescan the chain to recover the balance. + +### `open_wallet` + +```python +def open_wallet(self, path: str, password: str = "") -> None: ... +``` + +Opens an existing wallet file. An empty `password` is sent as JSON `null` +(unencrypted wallets). + +### `close_wallet` + +```python +def close_wallet(self) -> None: ... +``` + +Closes the currently open wallet. + +### `get_wallet_info` + +```python +def get_wallet_info(self) -> WalletInfo: ... +``` + +Returns wallet metadata (`wallet_id`, `account_names`, `extra_info` — a +`WalletExtraInfo` carrying the software/hardware wallet type in its `type` +field). + +### `sync_wallet` + +```python +def sync_wallet(self) -> None: ... +``` + +Syncs the wallet to the current chain tip. + +### `rescan_wallet` + +```python +def rescan_wallet(self) -> None: ... +``` + +Rescans the entire chain from genesis. Use this after importing a wallet or +when balances appear incorrect. + +### `best_block` + +```python +def best_block(self) -> BestBlock: ... +``` + +Returns the block (`height`, `id`) the wallet is currently synced to. + +--- + +## Accounts + +### `create_account` + +```python +def create_account(self, name: str) -> AccountInfo: ... +``` + +Creates a new BIP-44 account within the wallet. Returns the account index +(`AccountInfo(account, name)`). + +### `rename_account` + +```python +def rename_account(self, account: int, name: str = "") -> None: ... +``` + +Renames an existing account. An empty `name` is sent as JSON `null`, which +removes the name. + +--- + +## Balances and addresses + +### `get_balance` + +```python +def get_balance(self, account: int) -> Balance: ... +``` + +Returns the confirmed coin and token balances for an account (the request pins +`utxo_states` to `["Confirmed"]`). + +```python +@dataclass(frozen=True) +class Balance: + coins: Amount + tokens: dict[str, Amount] # keyed by token ID +``` + +### `new_address` + +```python +def new_address(self, account: int) -> str: ... +``` + +Derives a fresh receiving address and marks it as used. + +### `show_receive_addresses` + +```python +def show_receive_addresses(self, account: int) -> list[AddressWithUsage]: ... +``` + +Lists all receiving addresses that have been derived for an account, with +their usage status and coin balance +(`AddressWithUsage(address, used, coins)`). Change addresses are excluded. + +### `reveal_public_key` + +```python +def reveal_public_key(self, account: int, address: str) -> str: ... +``` + +Returns the hex-encoded public key for an address controlled by the wallet. + +--- + +## Key encryption + +### `encrypt_private_keys` + +```python +def encrypt_private_keys(self, password: str) -> None: ... +``` + +Encrypts the wallet's private keys with a password. + +### `unlock_private_keys` + +```python +def unlock_private_keys(self, password: str) -> None: ... +``` + +Unlocks an encrypted wallet for signing. The keys remain unlocked until +`lock_private_keys` is called or the daemon restarts. + +### `lock_private_keys` + +```python +def lock_private_keys(self) -> None: ... +``` + +Locks the private keys without closing the wallet. + +--- + +## Transactions + +### `address_send` + +```python +def address_send(self, params: SendParams) -> SendResult: ... +``` + +Sends coins to an address. The wallet selects UTXOs, computes fees, and +broadcasts. + +```python +@dataclass(frozen=True) +class SendParams: + account: int + address: str + amount: Amount + selected_utxos: list[Outpoint] | None = None + options: TxOptions = field(default_factory=TxOptions) + + +@dataclass(frozen=True) +class SendResult: + tx_id: str + fees: FeesBreakdown + broadcasted: bool +``` + +**Wire rules** (all params dataclasses serialise via `to_json()`): + +- `Amount(atoms="...", decimal="...")` omits empty fields; at least one must be + set when sending. +- `selected_utxos` is `omitempty`: `None` **or an empty list** is omitted from + the wire. +- `options` always serialises both keys (`null` when unset). +- Optional `str | None` fields (e.g. `htlc_secret`, `output_address`) always + serialise, as JSON `null` when unset. + +```python +from mintlayer.wallet import Amount, SendParams, TxOptions + +result = c.address_send( + SendParams( + account=0, + address="mtc1q...", + amount=Amount(atoms="100000000000"), # 1 ML + options=TxOptions(in_top_x_mb=1), # high fee priority + ) +) +print(result.tx_id, result.fees.coins.atoms, result.broadcasted) +``` + +Set `options.broadcast_to_mempool=False` to build and sign without +broadcasting; the transaction is still returned via the wallet history. + +### `token_send` + +```python +def token_send(self, params: TokenSendParams) -> SendResult: ... +``` + +Sends tokens from the account to an address +(`TokenSendParams(account, token_id, address, amount, options)`). + +### `sweep_spendable` + +```python +def sweep_spendable(self, params: SweepParams) -> SendResult: ... +``` + +Sweeps all spendable funds to a destination. Set `all=True` to sweep the +entire account; set `from_addresses` to sweep specific addresses only +(`SweepParams(account, destination_address, from_addresses, all, options)`). + +### `spend_utxo` + +```python +def spend_utxo(self, params: UTXOSpendParams) -> SendResult: ... +``` + +Spends a specific UTXO, optionally providing an HTLC secret +(`UTXOSpendParams(account, utxo: Outpoint, output_address, htlc_secret, +options)`). + +### `compose_transaction` + +```python +def compose_transaction(self, params: ComposeParams) -> ComposedTx: ... +``` + +Composes an unsigned transaction from explicit inputs and outputs. Returns a +hex-encoded `PartiallySignedTransaction` (`ComposedTx(hex, fees)`). Use this +for advanced flows where you construct outputs manually. + +```python +@dataclass(frozen=True) +class ComposeParams: + inputs: list[Outpoint] = field(default_factory=list) + outputs: list[Any] = field(default_factory=list) # raw output objects + htlc_secrets: Any = None + only_transaction: bool = False +``` + +### `sign_raw_transaction` + +```python +def sign_raw_transaction(self, account: int, raw_tx: str) -> SignedTx: ... +``` + +Signs a hex-encoded transaction using keys from the given account +(`SignedTx(hex, current_signatures)`). Used in cold-wallet flows where +composition and signing happen separately. + +```python +composed = c.compose_transaction(ComposeParams(...)) +signed = c.sign_raw_transaction(0, composed.hex) +submit = c.submit_transaction(signed.hex) +``` + +### `inspect_transaction` + +```python +def inspect_transaction(self, tx_hex: str) -> TxInspection: ... +``` + +Inspects a hex-encoded transaction without broadcasting. Returns input count, +signature count, and fees (`TxInspection(stats: TxStats, fees: +FeesBreakdown | None)`). + +### `submit_transaction` + +```python +def submit_transaction(self, tx_hex: str, do_not_store: bool = False) -> SubmitResult: ... +``` + +Broadcasts a signed transaction. Set `do_not_store=True` to broadcast without +saving the transaction in the wallet history. + +Quirk: the daemon route hardcodes the trust policy to `"Trusted"` — the client +always sends `{"trust_policy": "Trusted"}`. + +### `list_transactions_by_address` + +```python +def list_transactions_by_address( + self, account: int, address: str | None, limit: int +) -> list[WalletTx]: ... +``` + +Lists confirmed transactions for an account. Pass `address=None` to list +across all addresses (sent as JSON `null`). The most recent transactions are +returned first (`WalletTx(id, height, timestamp)`). + +### `list_pending_transactions` + +```python +def list_pending_transactions(self, account: int) -> list[str]: ... +``` + +Lists transaction IDs that are in the mempool but not yet confirmed. + +### `get_transaction` + +```python +def get_transaction(self, account: int, tx_id: str) -> Any: ... +``` + +Returns a transaction as raw decoded JSON. + +### `abandon_transaction` + +```python +def abandon_transaction(self, account: int, tx_id: str) -> None: ... +``` + +Removes an unconfirmed transaction from the wallet. The transaction will no +longer be rebroadcast. The UTXOs it spent are returned to the available +balance. + +### `deposit_data` + +```python +def deposit_data(self, account: int, data_hex: str) -> SendResult: ... +``` + +Embeds arbitrary hex-encoded data in a transaction output (`DataDeposit` +output type). + +--- + +## Transaction options + +Most transaction methods embed a `TxOptions` in their params: + +```python +@dataclass(frozen=True) +class TxOptions: + in_top_x_mb: int | None = None + broadcast_to_mempool: bool | None = None +``` + +`in_top_x_mb` controls fee priority. Setting it to `1` targets the top 1 MB of +the mempool (highest priority). The default (`None`) lets the daemon choose. + +Setting `broadcast_to_mempool=False` builds and signs the transaction without +broadcasting it. The transaction hex is still returned in the result. + +Unlike the optional params fields, `TxOptions` **always** serialises both keys +on the wire (`null` when unset) — this matches the Go struct exactly. + +--- + +## Staking + +See [staking.md](staking.md) for a complete guide. Quick reference: + +| Method | Description | +|--------|-------------| +| `create_stake_pool` | Create and fund a new staking pool | +| `decommission_stake_pool` | Wind down a pool and recover the pledge | +| `list_owned_pools` | List pools owned by an account | +| `get_pool_balance` | Get pool balance | +| `start_staking` | Start block production | +| `stop_staking` | Stop block production | +| `get_staking_status` | Check whether staking is active | +| `create_delegation` | Create a delegation to a pool | +| `delegate_staking` | Send coins into a delegation | +| `withdraw_from_delegation` | Withdraw from a delegation | +| `list_delegations` | List delegations owned by an account | + +--- + +## Tokens and NFTs + +See [tokens.md](tokens.md) for a complete guide. Quick reference: + +| Method | Description | +|--------|-------------| +| `issue_token` | Issue a new fungible token | +| `issue_nft` | Issue a new NFT | +| `mint_tokens` | Mint additional supply | +| `unmint_tokens` | Remove supply (return to unminted state) | +| `lock_token_supply` | Permanently lock supply | +| `freeze_token` | Freeze all transfers | +| `unfreeze_token` | Unfreeze (if allowed) | +| `change_token_authority` | Transfer the authority key | +| `send_token` | Send tokens to an address (alias of `token_send`) | + +--- + +## DEX orders + +### `create_order` + +```python +def create_order(self, params: CreateOrderParams) -> OrderCreated: ... +``` + +Creates a new DEX order (`OrderCreated(order_id, tx_id, broadcasted)`). + +```python +@dataclass(frozen=True) +class CreateOrderParams: + account: int + ask: OutputValue + give: OutputValue + conclude_address: str + options: TxOptions = field(default_factory=TxOptions) +``` + +The two sides of the order use `OutputValue`, which has a fully custom wire +encoding. Construct with the helper constructors: + +```python +from mintlayer.wallet import CreateOrderParams, OutputValue, coin_filter, token_filter + +params = CreateOrderParams( + account=0, + ask=OutputValue.coins(atoms="500000000000"), # asking 5 ML + give=OutputValue.tokens("ttml1...", atoms="1000"), # giving 1000 tokens + conclude_address="mtc1q...", +) +created = c.create_order(params) +``` + +### `conclude_order` + +```python +def conclude_order(self, params: ConcludeOrderParams) -> SendResult: ... +``` + +Concludes an order owned by the account +(`ConcludeOrderParams(account, order_id, output_address=None, options)`). + +### `fill_order` + +```python +def fill_order(self, params: FillOrderParams) -> SendResult: ... +``` + +Fills (partially or fully) an existing order +(`FillOrderParams(account, order_id, fill_amount_in_ask_currency, +output_address=None, options)`). + +### `freeze_order` + +```python +def freeze_order(self, params: FreezeOrderParams) -> SendResult: ... +``` + +Freezes an order (orders V1 fork only) +(`FreezeOrderParams(account, order_id, options)`). + +### `list_own_orders` + +```python +def list_own_orders(self, account: int) -> list[OwnOrder]: ... +``` + +Lists the account's own orders (`OwnOrder(order_id, initially_asked, +initially_given, existing_order_data, is_marked_as_frozen_in_wallet, +is_marked_as_concluded_in_wallet)`). + +### `list_all_active_orders` + +```python +def list_all_active_orders(self, params: ListOrdersParams) -> list[ActiveOrder]: ... +``` + +Lists all active orders, optionally filtered by currency pair — `None` filters +match any (`ActiveOrder(order_id, initially_asked, initially_given, +ask_balance, give_balance, is_own)`). + +```python +from mintlayer.wallet import ListOrdersParams, coin_filter, token_filter + +# module-level helpers (same as CurrencyFilter.coin_filter() / .token_filter(id)) +orders = c.list_all_active_orders( + ListOrdersParams( + account=0, + ask_currency=coin_filter(), + give_currency=token_filter("ttml1..."), + ) +) +``` + +`CurrencyFilter` (wire: `{"type":"Coin"}` with no content key, or +`{"type":"Token","content":""}`) and `OutputValue` raise `ValueError` +before any HTTP request is sent when constructed invalidly (missing token id / +missing amount). + +--- + +## Related + +- [transactions.md](transactions.md) — manual transaction building (no daemon) +- [staking.md](staking.md) — staking pools and delegations +- [tokens.md](tokens.md) — token and NFT lifecycle +- [node.md](node.md) — node daemon client diff --git a/docs/build/sdks/python/wasm.md b/docs/build/sdks/python/wasm.md new file mode 100644 index 0000000..790b150 --- /dev/null +++ b/docs/build/sdks/python/wasm.md @@ -0,0 +1,996 @@ +--- +title: "WASM client" +description: "WASM client reference for the Mintlayer Python SDK: keys, addresses, inputs, outputs, signing, fees, intents." +sidebar_position: 6 +--- + +# WASM client + +The `mintlayer.wasm` module exposes cryptographic primitives and binary +transaction encoding via an embedded WebAssembly module (instantiated with +wasmtime, sha256-pinned at import). + +```python +from mintlayer.wasm import Client + +c = Client() # instantiation compiles the WASM binary: ~400 ms +... +c.close() # or use `with Client() as c:` +``` + +Call it once per process and reuse it. The `Client` is **safe for concurrent +use from multiple threads** — every public method serialises access to the +single WASM instance with a lock. + +All errors raise `WasmError` with a message prefixed `mintlayer: `: + +```python +from mintlayer.wasm import WasmError + +try: + addr = c.pubkey_to_pubkeyhash_address(pubkey, Network.MAINNET) +except WasmError as e: + print(e) # e.g. "mintlayer: invalid public key" +``` + +> **Key material in memory:** result buffers carrying private keys, derived +> keys and signatures are zeroed before being released, but input buffers and +> WASM-side intermediate copies of secrets persist in WASM linear memory until +> the allocator reuses them (inherited from the wasm-bindgen design; the host +> cannot reach them). Treat the process memory of a long-lived `Client` as +> sensitive. + +--- + +## Amounts + +```python +from mintlayer.wasm import Amount + +one = Amount.from_atoms("100000000000") # 1 ML +zero = Amount.zero() +print(one.atoms) # "100000000000" +print(str(one)) # "100000000000" +``` + +All amounts are decimal strings of atoms. **1 ML = 100,000,000,000 atoms** +(11 decimal places). + +--- + +## Networks and enums + +Enums are `enum.IntEnum` subclasses passed to WASM as their integer +discriminant: + +```python +from mintlayer.wasm import ( + Network, # MAINNET=0, TESTNET=1, REGTEST=2, SIGNET=3 + SignatureHashType, # SIGHASH_ALL=0, SIGHASH_NONE=1, SIGHASH_SINGLE=2, SIGHASH_ANYONECANPAY=3 + SourceId, # SOURCE_TRANSACTION=0, SOURCE_BLOCK_REWARD=1 + TotalSupply, # LOCKABLE=0, UNLIMITED=1, FIXED=2 + FreezableToken, # NO=0, YES=1 + TokenUnfreezable, # NO=0, YES=1 +) +``` + +Module-level aliases (`MAINNET`, `TESTNET`, `REGTEST`, `SIGNET`, +`SIGHASH_ALL`, …, `SOURCE_TRANSACTION`, `SOURCE_BLOCK_REWARD`) match the Go +constant names; `mintlayer.Mainnet` in Go is `mintlayer.MAINNET` (or +`Network.MAINNET`) in Python. + +Pass the appropriate constant to any function that generates addresses or +encodes transactions. + +--- + +## Key derivation + +### `make_private_key` + +```python +def make_private_key(self) -> bytes: ... +``` + +Generates a random private key. + +### `make_default_account_privkey` + +```python +def make_default_account_privkey(self, mnemonic: str, network: Network) -> bytes: ... +``` + +Derives the default account extended private key from a BIP-39 mnemonic. The +derivation path is `m/44'/coin_type'/0'` where `coin_type` depends on the +network. + +### `public_key_from_private_key` + +```python +def public_key_from_private_key(self, privkey: bytes) -> bytes: ... +``` + +Derives the compressed public key from a private key. + +### `extended_public_key_from_extended_private_key` + +```python +def extended_public_key_from_extended_private_key(self, privkey: bytes) -> bytes: ... +``` + +Derives the extended public key from an extended private key. Useful for +watch-only wallets. + +### `make_receiving_address` + +```python +def make_receiving_address(self, account_privkey: bytes, key_index: int) -> bytes: ... +``` + +Derives the private key for receiving address `key_index` within an account. + +### `make_change_address` + +```python +def make_change_address(self, account_privkey: bytes, key_index: int) -> bytes: ... +``` + +Derives the private key for change address `key_index` within an account. + +### `make_receiving_address_public_key` + +```python +def make_receiving_address_public_key(self, account_pubkey: bytes, key_index: int) -> bytes: ... +``` + +Derives the public key for receiving address `key_index` from an extended +public key. Does not require the private key. + +### `make_change_address_public_key` + +```python +def make_change_address_public_key(self, account_pubkey: bytes, key_index: int) -> bytes: ... +``` + +Derives the public key for change address `key_index` from an extended public +key. + +**Example: derive a receiving address from a mnemonic** + +```python +mnemonic = "abandon abandon abandon ... about" + +account_key = c.make_default_account_privkey(mnemonic, Network.MAINNET) +recv_key = c.make_receiving_address(account_key, 0) +pub_key = c.public_key_from_private_key(recv_key) +addr = c.pubkey_to_pubkeyhash_address(pub_key, Network.MAINNET) +``` + +--- + +## Addresses + +### `encode_destination` + +```python +def encode_destination(self, address: str, network: Network) -> bytes: ... +``` + +Encodes a bech32m address into its binary representation for use in +transaction outputs. + +### `pubkey_to_pubkeyhash_address` + +```python +def pubkey_to_pubkeyhash_address(self, pubkey: bytes, network: Network) -> str: ... +``` + +Derives a P2PKH bech32m address from a compressed public key. + +### `encode_multisig_challenge` + +```python +def encode_multisig_challenge( + self, pubkeys: bytes, min_required_signatures: int, network: Network +) -> bytes: ... +``` + +Encodes a multisig challenge (script) from a concatenation of compressed +public keys. + +### `multisig_challenge_to_address` + +```python +def multisig_challenge_to_address(self, challenge: bytes, network: Network) -> str: ... +``` + +Derives a bech32m address from a multisig challenge. + +--- + +## IDs + +These functions derive chain-assigned IDs from transaction inputs. The ID is +deterministic: the same inputs always produce the same ID. + +### `get_pool_id` + +```python +def get_pool_id(self, inputs: bytes, network: Network) -> str: ... +``` + +Derives the pool ID that will be assigned to a `CreateStakePool` transaction. + +### `get_token_id` + +```python +def get_token_id(self, inputs: bytes, current_block_height: int, network: Network) -> str: ... +``` + +Derives the token ID that will be assigned to an `IssueFungibleToken` (or NFT) +transaction. `current_block_height` selects the ID scheme for the active +network upgrade. + +### `get_delegation_id` + +```python +def get_delegation_id(self, inputs: bytes, network: Network) -> str: ... +``` + +Derives the delegation ID that will be assigned to a `CreateDelegationId` +transaction. + +### `get_order_id` + +```python +def get_order_id(self, inputs: bytes, network: Network) -> str: ... +``` + +Derives the order ID that will be assigned to a `CreateOrder` transaction. + +--- + +## Inputs + +Each input is a binary blob. Concatenate all input blobs to form the `inputs` +bytes passed to `encode_transaction`. + +### `encode_input_for_utxo` + +```python +def encode_input_for_utxo(self, outpoint_source_id: bytes, output_index: int) -> bytes: ... +``` + +Encodes a UTXO spending input. `outpoint_source_id` is the result of +`encode_outpoint_source_id`. + +### `encode_input_for_withdraw_from_delegation` + +```python +def encode_input_for_withdraw_from_delegation( + self, delegation_id: str, amount: Amount, nonce: int, network: Network +) -> bytes: ... +``` + +Encodes a delegation withdrawal input. `nonce` must match the current +`next_nonce` from the indexer's delegation record. + +### `encode_input_for_mint_tokens` + +```python +def encode_input_for_mint_tokens( + self, token_id: str, amount: Amount, nonce: int, network: Network +) -> bytes: ... +``` + +### `encode_input_for_unmint_tokens` + +```python +def encode_input_for_unmint_tokens(self, token_id: str, nonce: int, network: Network) -> bytes: ... +``` + +### `encode_input_for_lock_token_supply` + +```python +def encode_input_for_lock_token_supply( + self, token_id: str, nonce: int, network: Network +) -> bytes: ... +``` + +### `encode_input_for_freeze_token` + +```python +def encode_input_for_freeze_token( + self, token_id: str, is_token_unfreezable: TokenUnfreezable, nonce: int, network: Network +) -> bytes: ... +``` + +### `encode_input_for_unfreeze_token` + +```python +def encode_input_for_unfreeze_token(self, token_id: str, nonce: int, network: Network) -> bytes: ... +``` + +### `encode_input_for_change_token_authority` + +```python +def encode_input_for_change_token_authority( + self, token_id: str, new_authority: str, nonce: int, network: Network +) -> bytes: ... +``` + +### `encode_input_for_change_token_metadata_uri` + +```python +def encode_input_for_change_token_metadata_uri( + self, token_id: str, new_metadata_uri: str, nonce: int, network: Network +) -> bytes: ... +``` + +### `encode_input_for_conclude_order` + +```python +def encode_input_for_conclude_order( + self, order_id: str, nonce: int, current_block_height: int, network: Network +) -> bytes: ... +``` + +### `encode_input_for_fill_order` + +```python +def encode_input_for_fill_order( + self, + order_id: str, + fill_amount: Amount, + destination: str, + nonce: int, + current_block_height: int, + network: Network, +) -> bytes: ... +``` + +FillOrder inputs are not signed — use `encode_witness_no_signature`. + +### `encode_input_for_freeze_order` + +```python +def encode_input_for_freeze_order( + self, order_id: str, current_block_height: int, network: Network +) -> bytes: ... +``` + +Order freezing is available only after the orders V1 fork. + +--- + +## Outputs + +Each output is a binary blob. Concatenate all output blobs to form the +`outputs` bytes passed to `encode_transaction`. + +### `encode_output_transfer` + +```python +def encode_output_transfer(self, amount: Amount, address: str, network: Network) -> bytes: ... +``` + +Coin transfer to an address. This is the standard output type for sending ML. + +### `encode_output_token_transfer` + +```python +def encode_output_token_transfer( + self, amount: Amount, address: str, token_id: str, network: Network +) -> bytes: ... +``` + +Fungible token transfer to an address. + +### `encode_output_lock_then_transfer` + +```python +def encode_output_lock_then_transfer( + self, amount: Amount, address: str, lock: bytes, network: Network +) -> bytes: ... +``` + +Coin transfer with a timelock. The coins are sent to `address` but cannot be +spent until the lock expires. `lock` is the result of one of the +`encode_lock_*` functions. + +### `encode_output_token_lock_then_transfer` + +```python +def encode_output_token_lock_then_transfer( + self, amount: Amount, address: str, token_id: str, lock: bytes, network: Network +) -> bytes: ... +``` + +Token transfer with a timelock. + +### `encode_output_coin_burn` + +```python +def encode_output_coin_burn(self, amount: Amount) -> bytes: ... +``` + +Permanently destroys ML coins. + +### `encode_output_token_burn` + +```python +def encode_output_token_burn(self, amount: Amount, token_id: str, network: Network) -> bytes: ... +``` + +Permanently destroys fungible tokens. + +### `encode_output_create_delegation` + +```python +def encode_output_create_delegation( + self, pool_id: str, owner_address: str, network: Network +) -> bytes: ... +``` + +Creates a delegation for `owner_address` in `pool_id`. Use `get_delegation_id` +to predict the delegation ID before broadcasting. + +### `encode_output_delegate_staking` + +```python +def encode_output_delegate_staking( + self, amount: Amount, delegation_id: str, network: Network +) -> bytes: ... +``` + +Sends coins into an existing delegation. + +### `encode_output_create_stake_pool` + +```python +def encode_output_create_stake_pool( + self, pool_id: str, pool_data: bytes, network: Network +) -> bytes: ... +``` + +Creates a staking pool. `pool_data` is the result of `encode_stake_pool_data`. + +### `encode_output_produce_block_from_stake` + +```python +def encode_output_produce_block_from_stake( + self, pool_id: str, staker: str, network: Network +) -> bytes: ... +``` + +Reward output used in blocks produced by a pool. This UTXO is consumed when +decommissioning a pool (if the pool has staked at least once). Only relevant +for block producers. + +### `encode_output_data_deposit` + +```python +def encode_output_data_deposit(self, data: bytes) -> bytes: ... +``` + +Embeds arbitrary bytes on-chain. + +### `encode_output_htlc` + +```python +def encode_output_htlc( + self, + amount: Amount, + token_id: str | None, + secret_hash: str, + spend_address: str, + refund_address: str, + refund_timelock: bytes, + network: Network, +) -> bytes: ... +``` + +Hashed Time-Lock Contract output. Pass `None` for `token_id` to use coins. The +receiver can spend with the preimage; the sender can refund after the timelock +expires. + +### `encode_output_issue_fungible_token` + +```python +def encode_output_issue_fungible_token( + self, + authority: str, + token_ticker: str, + metadata_uri: str, + number_of_decimals: int, + total_supply: TotalSupply, + supply_amount: Amount | None, + is_token_freezable: FreezableToken, + current_block_height: int, + network: Network, +) -> bytes: ... +``` + +Issues a new fungible token. `supply_amount` is required only when +`total_supply` is `TotalSupply.FIXED` (pass `None` otherwise). + +### `encode_output_issue_nft` + +```python +def encode_output_issue_nft( + self, + token_id: str, + authority: str, + name: str, + ticker: str, + description: str, + media_hash: bytes, + creator: bytes | None, + media_uri: str | None, + icon_uri: str | None, + additional_metadata_uri: str | None, + current_block_height: int, + network: Network, +) -> bytes: ... +``` + +Issues a new NFT. `creator`, `media_uri`, `icon_uri` and +`additional_metadata_uri` may be `None`. The `token_id` is derived with +`get_token_id` (NFTs share the fungible-token ID scheme). + +### `encode_create_order_output` + +```python +def encode_create_order_output( + self, + ask_amount: Amount, + ask_token_id: str | None, + give_amount: Amount, + give_token_id: str | None, + conclude_address: str, + network: Network, +) -> bytes: ... +``` + +Creates a DEX order. Pass `None` for `ask_token_id` or `give_token_id` to use +the native coin. + +--- + +## Timelocks + +Timelocks are binary blobs passed to `encode_output_lock_then_transfer` and +`encode_output_token_lock_then_transfer`. + +```python +def encode_lock_for_block_count(self, block_count: int) -> bytes: ... +def encode_lock_for_seconds(self, seconds: int) -> bytes: ... +def encode_lock_until_height(self, block_height: int) -> bytes: ... +def encode_lock_until_time(self, timestamp_seconds: int) -> bytes: ... +``` + +| Function | Unlocks when | +|----------|-------------| +| `encode_lock_for_block_count(n)` | `n` blocks have been confirmed after the output | +| `encode_lock_for_seconds(s)` | `s` seconds have elapsed since the output was confirmed | +| `encode_lock_until_height(h)` | the chain tip reaches height `h` | +| `encode_lock_until_time(t)` | the median block time exceeds Unix timestamp `t` | + +--- + +## Transactions + +### `encode_outpoint_source_id` + +```python +def encode_outpoint_source_id(self, id_: bytes, source_id: SourceId) -> bytes: ... +``` + +Encodes an outpoint source. `id_` is the raw transaction or block hash +(hex-decoded); `source_id` is `SourceId.SOURCE_TRANSACTION` or +`SourceId.SOURCE_BLOCK_REWARD`. + +### `encode_transaction` + +```python +def encode_transaction(self, inputs: bytes, outputs: bytes, flags: int) -> bytes: ... +``` + +Encodes an unsigned transaction from concatenated input and output blobs. +`flags` should be `0` for standard transactions. + +### `get_transaction_id` + +```python +def get_transaction_id(self, transaction: bytes, strict_byte_size: bool) -> str: ... +``` + +Returns the hex transaction ID without broadcasting. Set +`strict_byte_size=True` to require the bytes to represent exactly one +Transaction object. + +### `estimate_transaction_size` + +```python +def estimate_transaction_size( + self, inputs: bytes, input_utxos_dests: list[str], outputs: bytes, network: Network +) -> int: ... +``` + +Estimates the byte size of the transaction after signing. +`input_utxos_dests` must contain one address string per input (the spending +destination of each UTXO), in input order. Use this to compute fees before +constructing the final output set — see [transactions.md](transactions.md). + +### `encode_signed_transaction` + +```python +def encode_signed_transaction(self, transaction: bytes, signatures: bytes) -> bytes: ... +``` + +Assembles a fully signed transaction from the unsigned transaction bytes and +the concatenated witness bytes. + +### `encode_partially_signed_transaction` + +```python +def encode_partially_signed_transaction( + self, + transaction: bytes, + signatures: bytes, + input_utxos: bytes, + input_destinations: bytes, + htlc_secrets: bytes, + additional_info: TxAdditionalInfo, + network: Network, +) -> bytes: ... +``` + +Creates a Partially Signed Transaction (PSBT)-style structure. Use this for +multi-party signing flows. + +### `decode_signed_transaction_to_js` + +```python +def decode_signed_transaction_to_js(self, transaction: bytes, network: Network) -> bytes: ... +``` + +Decodes a signed transaction to JSON (raw JSON bytes) for inspection. +`decode_partially_signed_transaction_to_js` does the same for partially signed +transactions. + +### `extract_htlc_secret` + +```python +def extract_htlc_secret( + self, + signed_tx: bytes, + strict_byte_size: bool, + htlc_outpoint_source_id: bytes, + htlc_output_index: int, +) -> bytes: ... +``` + +Extracts the HTLC preimage from a transaction that spends an HTLC output. Use +this to learn the secret after the counterparty reveals it on-chain. + +### `internal_verify_witness` + +```python +def internal_verify_witness( + self, + sighash_type: int, + input_owner_dest: str | None, + witness: bytes, + transaction: bytes, + input_utxos: bytes, + input_index: int, + additional_info: TxAdditionalInfo, + block_height: int, + network: Network, +) -> None: ... +``` + +Verifies an input witness against the transaction (`input_owner_dest` may be +`None` where the destination is not required). Useful for testing signing +flows. + +--- + +## Signing + +### `encode_witness` + +```python +def encode_witness( + self, + sighash_type: SignatureHashType, + private_key: bytes, + input_owner_dest: str, + transaction: bytes, + input_utxos: bytes, + input_index: int, + additional_info: TxAdditionalInfo, + block_height: int, + network: Network, +) -> bytes: ... +``` + +Signs one input and returns the witness bytes. Call once per input and +concatenate results. + +`input_owner_dest` is the bech32m address that owns the UTXO being spent. +`input_utxos` is a concatenation of per-input UTXO entries (see +[transactions.md](transactions.md) for the `0x00`/`0x01` prefix encoding). +`block_height` is the current block height (pass `0` for outputs without +time-lock constraints). + +Use `SignatureHashType.SIGHASH_ALL` for standard transactions. + +### `encode_witness_no_signature` + +```python +def encode_witness_no_signature(self) -> bytes: ... +``` + +Returns an empty witness. Required for `FillOrder` inputs, which do not need a +signature. + +### `encode_witness_htlc_spend` + +```python +def encode_witness_htlc_spend( + self, + sighash_type: SignatureHashType, + private_key: bytes, + input_owner_dest: str, + transaction: bytes, + input_utxos: bytes, + input_index: int, + secret: bytes, + additional_info: TxAdditionalInfo, + block_height: int, + network: Network, +) -> bytes: ... +``` + +Signs an HTLC spend input, embedding the preimage. + +### `encode_witness_htlc_refund_single_sig` + +```python +def encode_witness_htlc_refund_single_sig( + self, + sighash_type: SignatureHashType, + private_key: bytes, + input_owner_dest: str, + transaction: bytes, + input_utxos: bytes, + input_index: int, + additional_info: TxAdditionalInfo, + block_height: int, + network: Network, +) -> bytes: ... +``` + +Signs an HTLC refund for a single-signature refund address. + +### `encode_witness_htlc_refund_multisig` + +```python +def encode_witness_htlc_refund_multisig( + self, + sighash_type: SignatureHashType, + private_key: bytes, + key_index: int, + input_witness: bytes, + multisig_challenge: bytes, + transaction: bytes, + input_utxos: bytes, + input_index: int, + additional_info: TxAdditionalInfo, + block_height: int, + network: Network, +) -> bytes: ... +``` + +Adds a partial signature to an HTLC refund witness for a multisig refund +address. `key_index` is the index of `private_key` within the multisig +challenge; `input_witness` may be empty (first signer) or a previous partial +result. + +### `sign_challenge` + +```python +def sign_challenge(self, private_key: bytes, message: bytes) -> bytes: ... +``` + +Signs an arbitrary message for use in challenge-response authentication. + +### `verify_challenge` + +```python +def verify_challenge( + self, address: str, network: Network, signed_challenge: bytes, message: bytes +) -> bool: ... +``` + +Verifies a challenge signature against a bech32m (pubkeyhash) address. + +### `sign_message_for_spending` + +```python +def sign_message_for_spending(self, private_key: bytes, message: bytes) -> bytes: ... +``` + +Signs a spending message (used in transaction intents). + +### `verify_signature_for_spending` + +```python +def verify_signature_for_spending( + self, public_key: bytes, signature: bytes, message: bytes +) -> bool: ... +``` + +Verifies a spending message signature against a public key. + +--- + +## Additional info for signing + +Some transaction types (pool operations, orders) require extra data not +present in the UTXO itself: + +```python +from mintlayer.wasm import ( + Amount, + OrderBalance, + OrderInfo, + PoolInfo, + SimpleCurrencyAmount, + TxAdditionalInfo, +) + +info = TxAdditionalInfo( + pool_info={"mpool1...": PoolInfo(staker_balance=Amount.from_atoms("40000000000000"))}, + order_info={ + "mordr1...": OrderInfo( + initially_asked=SimpleCurrencyAmount.coins("500000000000"), + initially_given=SimpleCurrencyAmount.tokens("1000", "ttml1..."), + ask_balance=OrderBalance(atoms="500000000000"), + give_balance=OrderBalance(atoms="800", token_id="ttml1..."), + ) + }, +) +``` + +- Maps are keyed by the bech32m pool/order ID. Pass an empty `TxAdditionalInfo()` + for standard coin transfers. +- `SimpleCurrencyAmount` serialises as the externally tagged + `CurrencyAmount` enum — `{"coins":{"atoms":...}}` or + `{"tokens":{"amount":{"atoms":...},"token_id":...}}` — built with the + `.coins(atoms)` / `.tokens(atoms, token_id)` constructors. +- `OrderBalance` uses the redundant-but-required wire shape + `{"atoms":...,"amount":{"atoms":...},"token_id":null|"..."}`. +- The WASM module rejects `null` maps, so the `TxAdditionalInfo` defaults are + empty dicts rather than `None`. + +--- + +## Staking + +### `encode_stake_pool_data` + +```python +def encode_stake_pool_data( + self, + value: Amount, + staker: str, + vrf_public_key: str, + decommission_key: str, + margin_ratio_per_thousand: int, + cost_per_block: Amount, + network: Network, +) -> bytes: ... +``` + +Encodes the pool parameters for use in `encode_output_create_stake_pool`. +`staker` is the bech32m address allowed to produce blocks; `vrf_public_key` is +the bech32m VRF public key; `decommission_key` is the address that can +decommission the pool; `margin_ratio_per_thousand` is the staker's cut per +thousand (e.g. `100` = 10%); `cost_per_block` is a flat amount deducted from +rewards before the margin split. + +### `effective_pool_balance` + +```python +def effective_pool_balance( + self, network: Network, pledge_amount: Amount, pool_balance: Amount +) -> Amount: ... +``` + +Computes the effective balance used in the slot lottery, which applies +diminishing returns to large pools. + +### `staking_pool_spend_maturity_block_count` + +```python +def staking_pool_spend_maturity_block_count( + self, current_block_height: int, network: Network +) -> int: ... +``` + +Returns the number of blocks a pool output must mature before it can be spent +(after decommission). + +--- + +## Fees + +These functions return the minimum protocol fee for various operations at a +given block height: + +```python +def fungible_token_issuance_fee(self, current_block_height: int, network: Network) -> Amount: ... +def nft_issuance_fee(self, current_block_height: int, network: Network) -> Amount: ... +def data_deposit_fee(self, current_block_height: int, network: Network) -> Amount: ... +def token_supply_change_fee(self, current_block_height: int, network: Network) -> Amount: ... +def token_freeze_fee(self, current_block_height: int, network: Network) -> Amount: ... +def token_change_authority_fee(self, current_block_height: int, network: Network) -> Amount: ... +``` + +Add these fees to the transaction outputs when building the relevant +transaction types manually — see [tokens.md](tokens.md). + +--- + +## Transaction intents + +Transaction intents provide a signed declaration of what a transaction is +intended to do, independent of the transaction bytes themselves. + +### `make_transaction_intent_message_to_sign` + +```python +def make_transaction_intent_message_to_sign(self, intent: str, transaction_id: str) -> bytes: ... +``` + +Creates the message bytes that should be signed to bind an intent string to a +transaction ID (`transaction_id` is the hex ID from `get_transaction_id`). + +### `encode_signed_transaction_intent` + +```python +def encode_signed_transaction_intent( + self, signed_message: bytes, signatures: list[bytes] +) -> bytes: ... +``` + +Encodes a signed intent along with its signatures (one raw signature per +transaction input, each produced by `sign_challenge`). + +### `verify_transaction_intent` + +```python +def verify_transaction_intent( + self, + expected_signed_message: bytes, + encoded_signed_intent: bytes, + input_destinations: list[str], + network: Network, +) -> None: ... +``` + +Verifies that a signed intent matches the expected message and that the +signatures are valid for the given input destinations (one bech32m address per +transaction input). + +--- + +## Related + +- [transactions.md](transactions.md) — end-to-end manual transaction flow +- [staking.md](staking.md) — staking via wallet daemon and manual encoding +- [tokens.md](tokens.md) — token lifecycle via wallet daemon and manual encoding +- [wallet.md](wallet.md) — the wallet daemon client (does the encoding for you) diff --git a/docs/guides/_category_.json b/docs/guides/_category_.json index 1af9c27..635ad7d 100644 --- a/docs/guides/_category_.json +++ b/docs/guides/_category_.json @@ -1,6 +1,6 @@ { "label": "Guides", - "position": 3, + "position": 6, "link": { "type": "doc", "id": "guides/index" diff --git a/docs/guides/cli/index.md b/docs/guides/cli/index.md index 858754d..3af1ff9 100644 --- a/docs/guides/cli/index.md +++ b/docs/guides/cli/index.md @@ -6,7 +6,7 @@ sidebar_position: 1 # Command-line Guides -These guides walk through Mintlayer's main workflows with `wallet-cli`, the interactive command-line wallet. They are the reference versions of the workflows: the [Go](../go/index.md), [JavaScript](../javascript/index.md), and [Rust](../rust/index.md) guides implement the same flows in code. +These guides walk through Mintlayer's main workflows with `wallet-cli`, the interactive command-line wallet. They are the reference versions of the workflows: the [Go](../go/index.md), [JavaScript](../javascript/index.md), [Rust](../rust/index.md), and [Python](../python/index.md) guides implement the same flows in code. Prerequisites for all of them: a running, synced `node-daemon`, `wallet-cli` connected to the node (see [installation](../../getting-started/install/index.md)), and a small amount of ML for transaction fees. Examples use **testnet** values (TML); on mainnet the commands are identical apart from the network. diff --git a/docs/guides/go/index.md b/docs/guides/go/index.md index 021ec7b..e00936a 100644 --- a/docs/guides/go/index.md +++ b/docs/guides/go/index.md @@ -8,7 +8,7 @@ sidebar_position: 1 These guides implement Mintlayer's main workflows with the [Go SDK](../../build/sdks/go/index.md) (`go get github.com/mintlayer/go-sdk`). The `wallet` sub-client talks to a running `wallet-rpc-daemon`, the `indexer` client reads chain state, and the `wasm` package builds and signs transactions standalone, with no wallet daemon (see [Developer Setup](../../build/development.md) for ports and authentication). -The wallet-cli versions of the same workflows live in the [command-line guides](../cli/index.md); the JavaScript versions in the [JavaScript guides](../javascript/index.md), and the Rust versions in the [Rust guides](../rust/index.md). +The wallet-cli versions of the same workflows live in the [command-line guides](../cli/index.md); the JavaScript versions in the [JavaScript guides](../javascript/index.md), the Rust versions in the [Rust guides](../rust/index.md), and the Python versions in the [Python guides](../python/index.md). | Guide | Contents | | ----- | -------- | diff --git a/docs/guides/go/utxo-composition.md b/docs/guides/go/utxo-composition.md index 04b600c..82e591d 100644 --- a/docs/guides/go/utxo-composition.md +++ b/docs/guides/go/utxo-composition.md @@ -28,6 +28,12 @@ Two consequences worth internalizing: - The **new order gets a new ID** (IDs are derived from inputs, see [predicting IDs](custom-transactions.md#predicting-ids)). Publish the new ID and retire the old one. - If the transaction fails or is dropped, nothing changed: the old order is still live and its nonce unspent. +:::note[One order operation per transaction] + +Consensus allows at most **one order operation input** (fill, conclude, or freeze) per transaction, and that single slot is shared with token account commands: you cannot combine an order operation with a mint, an unmint, or any other command input. Creating orders is capped the same way — at most one `CreateOrder` output per transaction. A second command input or a second created order makes the transaction invalid (`MultipleAccountCommands` / `MultipleOrdersCreated` in the verifier). The update pattern above sits exactly at both limits: one conclude input, one new order. + +::: + ## The ingredients | Piece | Encoder | Source of truth | diff --git a/docs/guides/index.md b/docs/guides/index.md index b62208e..316a374 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -1,12 +1,12 @@ --- title: "Guides" -description: "Step-by-step Mintlayer guides in three flavors: wallet-cli on the command line, Go, and JavaScript, with room for more languages." +description: "Step-by-step Mintlayer guides in five flavors: wallet-cli on the command line, Go, JavaScript, Rust, and Python, with room for more languages." sidebar_position: 1 --- # Guides -The Mintlayer guides exist in four flavors, one folder per language or interface. They cover the same workflows: issuing tokens and NFTs, staking, trading with on-chain orders, and atomic swaps with Bitcoin. +The Mintlayer guides exist in five flavors, one folder per language or interface. They cover the same workflows: issuing tokens and NFTs, staking, trading with on-chain orders, and atomic swaps with Bitcoin. | Section | For | Contents | | ------- | --- | -------- | @@ -14,19 +14,20 @@ The Mintlayer guides exist in four flavors, one folder per language or interface | [Go guides](go/index.md) | Services, exchanges, backends | The `go-sdk`: wallet RPC client and the embedded WASM runtime | | [JavaScript guides](javascript/index.md) | dApps, bots, scripts | `@mintlayer/sdk`: the `Client` in the browser (Mojito) or Node.js | | [Rust guides](rust/index.md) | Services and full-custody tooling | `mintlayer-sdk`: RPC sub-clients plus native crypto backed by mintlayer-core | +| [Python guides](python/index.md) | Scripts, tooling, data jobs | `mintlayer`: the Go SDK ported to Python, WASM runtime bundled | New languages can be added alongside these; the workflows are the same, only the code changes. ## Workflow map -| Workflow | Command line | Go | JavaScript | Rust | -| -------- | ------------ | -- | ---------- | ---- | -| Issue and manage a fungible token | [guide](cli/issue-new-token.md) | [guide](go/issue-token.md) | [guide](javascript/issue-token.md) | [guide](rust/issue-token.md) | -| Issue and manage an NFT | [guide](cli/issuing-and-managing-an-nft.md) | [guide](go/issue-nft.md) | [guide](javascript/issue-nft.md) | [guide](rust/issue-nft.md) | -| Run a staking pool / delegate | [guide](cli/managing-a-staking-pool.md) | [guide](go/staking.md) (pools + delegations) | [guide](javascript/staking.md) (delegations) | [guide](rust/staking.md) (pools + delegations) | -| Trade with on-chain orders | [guide](cli/trading-with-orders.md) | [guide](go/trading-with-orders.md) | [guide](javascript/trading-with-orders.md) | [guide](rust/trading-with-orders.md) | -| Atomic swap with Bitcoin (HTLC) | [guide](cli/atomic-swap-with-bitcoin.md) | [guide](go/atomic-swap.md) | [guide](javascript/atomic-swap.md) | [guide](rust/atomic-swap.md) | -| Manage a multisig address | [guide](cli/managing-a-multisig-address.md) | CLI only | CLI only | CLI only | -| Migrate ERC-20 ML to mainnet | [guide](cli/migrate-erc20-token-to-mainnet.md) | Portal only | Portal only | Portal only | +| Workflow | Command line | Go | JavaScript | Rust | Python | +| -------- | ------------ | -- | ---------- | ---- | ------ | +| Issue and manage a fungible token | [guide](cli/issue-new-token.md) | [guide](go/issue-token.md) | [guide](javascript/issue-token.md) | [guide](rust/issue-token.md) | [guide](python/issue-token.md) | +| Issue and manage an NFT | [guide](cli/issuing-and-managing-an-nft.md) | [guide](go/issue-nft.md) | [guide](javascript/issue-nft.md) | [guide](rust/issue-nft.md) | [guide](python/issue-nft.md) | +| Run a staking pool / delegate | [guide](cli/managing-a-staking-pool.md) | [guide](go/staking.md) (pools + delegations) | [guide](javascript/staking.md) (delegations) | [guide](rust/staking.md) (pools + delegations) | [guide](python/staking.md) (pools + delegations) | +| Trade with on-chain orders | [guide](cli/trading-with-orders.md) | [guide](go/trading-with-orders.md) | [guide](javascript/trading-with-orders.md) | [guide](rust/trading-with-orders.md) | [guide](python/trading-with-orders.md) | +| Atomic swap with Bitcoin (HTLC) | [guide](cli/atomic-swap-with-bitcoin.md) | [guide](go/atomic-swap.md) | [guide](javascript/atomic-swap.md) | [guide](rust/atomic-swap.md) | [guide](python/atomic-swap.md) | +| Manage a multisig address | [guide](cli/managing-a-multisig-address.md) | CLI only | CLI only | CLI only | CLI only | +| Migrate ERC-20 ML to mainnet | [guide](cli/migrate-erc20-token-to-mainnet.md) | Portal only | Portal only | Portal only | Portal only | -Deeper WASM-specific dives (full custody in Go, no wallet daemon): [Wallet & signing](go/wallet-wasm.md), [Forging custom transactions](go/custom-transactions.md), and [Composing UTXOs](go/utxo-composition.md). The Rust equivalent is the `crypto` module, covered by [Wallet & signing in Rust](../build/sdks/rust/crypto.md) and [Building transactions](../build/sdks/rust/transactions.md). +Deeper full-custody dives with the embedded WASM runtime (Go and Python, no wallet daemon): [Wallet & signing](go/wallet-wasm.md), [Forging custom transactions](go/custom-transactions.md), and [Composing UTXOs](go/utxo-composition.md), each also available in the [Python guides](python/index.md). The Rust equivalent is the native `crypto` module, covered by [Cryptography](../build/sdks/rust/crypto.md) and [Building transactions](../build/sdks/rust/transactions.md). diff --git a/docs/guides/javascript/index.md b/docs/guides/javascript/index.md index e1f1f27..040e946 100644 --- a/docs/guides/javascript/index.md +++ b/docs/guides/javascript/index.md @@ -8,7 +8,7 @@ sidebar_position: 1 These guides implement Mintlayer's main workflows with the [JavaScript SDK](../../build/sdks/javascript/index.md) (`npm install @mintlayer/sdk`). In the browser the `Client` talks to the [Mojito wallet](../../wallet/mojito-wallet.md) extension; in Node.js use the standalone [account providers](../../build/sdks/javascript/account-providers.md). Start with [Getting Started](../../build/sdks/javascript/getting-started.md); the `Client` API is detailed in [SDK Client](../../build/sdks/javascript/client.md). -The wallet-cli versions of the same workflows live in the [command-line guides](../cli/index.md); the Go versions in the [Go guides](../go/index.md), and the Rust versions in the [Rust guides](../rust/index.md). +The wallet-cli versions of the same workflows live in the [command-line guides](../cli/index.md); the Go versions in the [Go guides](../go/index.md), the Rust versions in the [Rust guides](../rust/index.md), and the Python versions in the [Python guides](../python/index.md). | Guide | Contents | | ----- | -------- | diff --git a/docs/guides/python/atomic-swap.md b/docs/guides/python/atomic-swap.md new file mode 100644 index 0000000..7a655ce --- /dev/null +++ b/docs/guides/python/atomic-swap.md @@ -0,0 +1,108 @@ +--- +title: "Atomic Swap" +description: "Build the Mintlayer side of a Bitcoin atomic swap with HTLCs using the Python SDK's wasm module." +sidebar_position: 6 +--- + +# Atomic Swap + +The Python path to cross-chain atomic swaps. The same workflow exists for the [command line](../cli/atomic-swap-with-bitcoin.md) (the full protocol: roles, the secret/hash flow, and [timelock planning](../cli/atomic-swap-with-bitcoin.md#timelock-planning)), [Go](../go/atomic-swap.md), [JavaScript](../javascript/atomic-swap.md), and [Rust](../rust/atomic-swap.md). This page shows how to build the **Mintlayer side** with the `wasm` module. + +```mermaid +sequenceDiagram + autonumber + participant A as Alice (Mintlayer) + participant ML as Mintlayer HTLC + participant B as Bob (Bitcoin) + + A->>A: generate secret, share only its hash + A->>ML: lock ML in an HTLC (hash, 48h timelock) + B->>BTC: lock BTC with the same hash (24h timelock) + A->>BTC: claim with secret (secret becomes public) + B->>ML: extract the secret from Alice's claim + B->>ML: spend the ML HTLC with the secret +``` + +If a party stalls, the HTLC's refund path returns the funds once the timelock expires. + +## Locking funds in an HTLC + +```python +from mintlayer.wasm import ( + Amount, + Network, + SIGHASH_ALL, + SOURCE_TRANSACTION, + TxAdditionalInfo, +) +from mintlayer.wasm import Client as WasmClient + +c = WasmClient() + +src_id = c.encode_outpoint_source_id(tx_id_bytes, SOURCE_TRANSACTION) +htlc_input = c.encode_input_for_utxo(src_id, output_index) + +htlc_output = c.encode_output_htlc( + Amount(atoms="100000000000"), # 1 ML + None, # token_id: None for the base coin + secret_hash, # only the hash is shared + "mtc1q_spend...", # where a successful claim pays out + "mtc1q_refund...", # where a refund returns + refund_timelock, # encoded timelock + Network.MAINNET, +) +``` + +Assemble the transaction with the regular input/output encoders and submit; see [Building transactions](../../build/sdks/python/transactions.md). + +## Claiming and refunding + +The claim embeds the preimage in the witness; the refund path is available after the timelock: + +```python +# Spend (claim) the HTLC, revealing the secret +witness = c.encode_witness_htlc_spend( + SIGHASH_ALL, + private_key, + input_owner_dest, + transaction, + input_utxos, + input_index, + secret, + TxAdditionalInfo(), + block_height, + Network.MAINNET, +) + +# Refund after the timelock with a single-signature refund address +witness = c.encode_witness_htlc_refund_single_sig( + SIGHASH_ALL, + private_key, + input_owner_dest, + transaction, + input_utxos, + input_index, + TxAdditionalInfo(), + block_height, + Network.MAINNET, +) +``` + +## Learning the counterparty's secret + +When the counterparty claims their HTLC, the secret is revealed on-chain. Extract it from the signed claim transaction: + +```python +secret = c.extract_htlc_secret( + signed_tx, + htlc_outpoint_source_id, + htlc_output_index, +) +``` + +## Checklist + +- Agree off-chain on amounts, rate, and **timelock durations** before locking anything. +- Bitcoin timelock strictly shorter than the Mintlayer timelock. +- Only the hash is shared; the secret is revealed by the first claim. +- Verify the on-chain HTLC (amount, addresses, timelock) before locking the other side. diff --git a/docs/guides/python/custom-transactions.md b/docs/guides/python/custom-transactions.md new file mode 100644 index 0000000..b9ced39 --- /dev/null +++ b/docs/guides/python/custom-transactions.md @@ -0,0 +1,153 @@ +--- +title: "Forging Custom Transactions (Advanced)" +description: "Compose arbitrary Mintlayer transactions with the Python SDK's WASM runtime: mixed inputs and outputs, protocol fees, fee estimation, multi-party signing, and intents." +sidebar_position: 8 +--- + +# Forging Custom Transactions (Advanced) + +A Mintlayer transaction is just **inputs + outputs + witnesses**. The wallet daemon assembles standard shapes for you; the WASM runtime lets you forge any valid combination yourself. This is the advanced guide; the basic eight-step flow is documented in [Building transactions](../../build/sdks/python/transactions.md). Here we focus on what you can *combine*. + +```python +from mintlayer.wasm import Amount, Network, TxAdditionalInfo +from mintlayer.wasm import Client + +c = Client() +``` + +## The shape of a transaction + +```python +tx = c.encode_transaction(encoded_inputs, encoded_outputs, 0) # flags = 0 +``` + +- `encoded_inputs`: a **concatenation** of input blobs. Most inputs are UTXO spends (`encode_input_for_utxo`), but protocol inputs exist too: token operations (`encode_input_for_mint_tokens`, `encode_input_for_unmint_tokens`, `encode_input_for_freeze_token`, ...), delegation withdrawals, and order operations. All concatenate the same way. +- `encoded_outputs`: concatenation of output blobs. Any mix of transfers, burns, issuances, data deposits, HTLCs, order creations, and pool operations. +- `flags`: `0` for standard transactions. + +Nothing stops you from mixing output types freely in one transaction. + +## Mixed outputs in one transaction + +Payments, an on-chain memo, and a time-locked vesting payout, all atomic: + +```python +# 1. Plain coin payment +payment = c.encode_output_transfer( + Amount(atoms="500000000000"), "mtc1qrecipient...", Network.MAINNET +) + +# 2. Arbitrary bytes on-chain +memo = c.encode_output_data_deposit(b"order-ref:2026-09-16-042") + +# 3. Coins the recipient cannot spend for 1000 blocks +lock = c.encode_lock_for_block_count(1000) +vesting = c.encode_output_lock_then_transfer( + Amount(atoms="100000000000"), "mtc1qemployee...", lock, Network.MAINNET +) + +# 4. Token payment alongside the coins +token_out = c.encode_output_token_transfer( + Amount(atoms="25"), "mtc1qrecipient...", "ttml1tokenid...", Network.MAINNET +) + +all_outputs = payment + memo + vesting + token_out +``` + +## Mixing UTXO and protocol inputs + +Protocol operations are inputs like any other. Minting tokens, for example, consumes a **nonce input** keyed by the token ID, while the network fee still comes from a regular coin UTXO: + +```python +token_info = idx.get_token(token_id) # next_nonce + +mint_input = c.encode_input_for_mint_tokens( + token_id, Amount(atoms="100000"), token_info.next_nonce, Network.MAINNET +) + +fee_input = c.encode_input_for_utxo(src_id, 0) # coin UTXO covering the fee + +encoded_inputs = mint_input + fee_input +``` + +The mint input requires the token's current `next_nonce` from the indexer: every protocol input (freezes, authority changes, delegation withdrawals, order ops) consumes its nonce the same way. + +## Protocol fees + +Token operations carry minimum protocol fees that must be added to the transaction outputs. Query them at the current height: + +```python +tip = idx.get_tip() + +issuance_fee = c.fungible_token_issuance_fee(tip.block_height, Network.MAINNET) +mint_fee = c.token_supply_change_fee(tip.block_height, Network.MAINNET) +freeze_fee = c.token_freeze_fee(tip.block_height, Network.MAINNET) +authority_fee = c.token_change_authority_fee(tip.block_height, Network.MAINNET) +``` + +Add matching coin outputs (typically back to yourself or the new authority) covering the fee amounts. + +## Network fee estimation + +Size drives the network fee, and size depends on the *signed* transaction. Estimate before finalizing outputs, then deduct the fee from change: + +```python +rate = idx.get_fee_rate(1) # atoms per KB, top 1 MB of mempool + +size = c.estimate_transaction_size(encoded_inputs, dest_addresses, all_outputs, Network.MAINNET) + +fee = (size + 999) // 1000 * max(int(rate), 1) +``` + +`dest_addresses` is one owner address per input, in input order; for mixed inputs, that is the UTXO's address, or the relevant delegation/order owner for protocol inputs. + +## Predicting IDs + +Creation transactions get their IDs from their inputs, deterministically; compute them before broadcasting: + +```python +pool_id = c.get_pool_id(encoded_inputs, Network.MAINNET) +token_id = c.get_token_id(encoded_inputs, tip.block_height, Network.MAINNET) +delegation_id = c.get_delegation_id(encoded_inputs, Network.MAINNET) +order_id = c.get_order_id(encoded_inputs, Network.MAINNET) +``` + +This is how a transaction that creates a delegation can immediately delegate to it, or how you can show a user "your token will be `ttml1...`" before signing. + +## Multi-party signing (PSBT-style) + +`encode_signed_transaction` expects witnesses for every input. For flows where different parties sign different inputs, build partial signatures and merge: + +```python +partially_signed = c.encode_partially_signed_transaction( + transaction, witness_bytes, input_utxos, input_destinations, + htlc_secrets, # empty when unused + TxAdditionalInfo(), + Network.MAINNET, +) +``` + +Exchange the partially signed transaction out-of-band; the final assembler produces the complete signed transaction once every input has a witness. + +## Transaction intents + +An intent is a **signed declaration of purpose** (what a transaction is meant to do), bound to the transaction ID but independent of its bytes. Signers can verify what they are authorizing before producing a witness: + +```python +msg = c.make_transaction_intent_message_to_sign(intent, transaction_id) +sig = c.sign_message_for_spending(private_key, msg) + +signed_intent = c.encode_signed_transaction_intent(msg, [sig]) + +# Verifier side, before signing: +c.verify_transaction_intent(msg, signed_intent, input_destinations, Network.MAINNET) +``` + +This is the building block for co-signers and policy engines that must approve transactions they can't fully parse. + +## Sanity checks before broadcasting + +- Every input witness present, in input order (see [Wallet & Signing](wallet-wasm.md#signing-a-transaction)). +- `TxAdditionalInfo` populated for any pool or order input, or signing fails or produces an invalid sighash (see [the reference](../../build/sdks/python/wasm.md#additional-info-for-signing)). +- Protocol fees included as outputs; nonce values fresh from the indexer. +- Dry-run the result: `get_transaction_id` before `submit_transaction`. diff --git a/docs/guides/python/index.md b/docs/guides/python/index.md new file mode 100644 index 0000000..327f96e --- /dev/null +++ b/docs/guides/python/index.md @@ -0,0 +1,22 @@ +--- +title: "Python Guides" +description: "The Mintlayer workflows in Python: the mintlayer SDK's sub-clients plus the embedded WASM runtime for full-custody transaction building." +sidebar_position: 1 +--- + +# Python Guides + +These guides implement Mintlayer's main workflows with the [Python SDK](../../build/sdks/python/index.md) (`pip install mintlayer`), a port of the Go SDK: the `wallet` sub-client talks to a running `wallet-rpc-daemon`, the `indexer` client reads chain state, and the `wasm` module builds and signs transactions standalone, with no wallet daemon in the loop (see [Developer Setup](../../build/development.md) for ports and authentication). + +The wallet-cli versions of the same workflows live in the [command-line guides](../cli/index.md); the [Go](../go/index.md), [JavaScript](../javascript/index.md), and [Rust](../rust/index.md) versions in their own sections. + +| Guide | Contents | +| ----- | -------- | +| [Issue a token](issue-token.md) | MLS-01 lifecycle: issue, mint, transfer, freeze, authority | +| [Issue an NFT](issue-nft.md) | MLS-03 issuance and transfers | +| [Staking and delegation](staking.md) | Pool lifecycle and delegations | +| [Trading with orders](trading-with-orders.md) | On-chain orders: create, discover, fill, conclude | +| [Atomic swap](atomic-swap.md) | The Mintlayer side of a BTC ⇄ ML swap with HTLCs | +| [Wallet & signing (WASM)](wallet-wasm.md) | Generate wallets, derive addresses, sign without a daemon | +| [Forging custom transactions](custom-transactions.md) | Mixed inputs/outputs, protocol fees, multi-party signing, intents | +| [Composing UTXOs](utxo-composition.md) | Update an order atomically: conclude input + create output | diff --git a/docs/guides/python/issue-nft.md b/docs/guides/python/issue-nft.md new file mode 100644 index 0000000..eace5e8 --- /dev/null +++ b/docs/guides/python/issue-nft.md @@ -0,0 +1,61 @@ +--- +title: "Issue an NFT" +description: "Issue MLS-03 NFTs with the Python SDK via the wallet daemon or the wasm module's issuance encoders." +sidebar_position: 3 +--- + +# Issue an NFT + +The Python path to issuing MLS-03 NFTs. The same workflow exists for the [command line](../cli/issuing-and-managing-an-nft.md), [Go](../go/issue-nft.md), [JavaScript](../javascript/issue-nft.md), and [Rust](../rust/issue-nft.md). On-chain, an NFT is an MLS-03 issuance carrying the creator, name, description, ticker, icon, media URI, and media hash; the JSON standard for the off-chain metadata is defined in the [Token Metadata Standards](../../reference/token-standards/mls03.md). + +:::note[Immutable by design] + +NFT metadata is **set at issuance and cannot be changed**: there is no equivalent of `change_token_metadata_uri` for NFTs. Compute and record the media hash before issuing. Mintlayer does not prescribe an algorithm; use a standard one (e.g. SHA-256) and record which one you used. + +::: + +## Issuing via the wallet daemon + +```python +from mintlayer.wallet import Client, IssueNFTParams, NFTMetadata + +wc = Client("http://127.0.0.1:3034") + +owner_addr = wc.new_address(0) + +result = wc.issue_nft( + IssueNFTParams( + account=0, + destination_address=owner_addr, + metadata=NFTMetadata( + name="My NFT", + description="A unique digital collectible", + ticker="MYNFT", + media_hash="sha256hexhash...", + media_uri="https://example.com/media.png", + icon_uri="https://example.com/icon.png", + # creator and additional_metadata_uri default to None + ), + ) +) +print(f"nft id: {result.token_id}") +``` + +NFTs cannot be minted after issuance: each issuance transaction creates exactly one NFT. + +## Transferring + +An NFT transfer is a token transfer of the NFT id: use `send_token` with the NFT id (see [Issue a Token](issue-token.md#transferring-and-burning)) or the wallet transaction APIs. + +## Reading NFT state + +The indexer reports NFTs with owner and metadata: + +```python +nft = idx.get_nft(result.token_id) +print(f"owner: {nft.owner} name: {nft.metadata.name}") +``` + +## Metadata standard + +For the JSON document at the metadata URI (media details, editions, licensing, collections), follow the MLS-03 schema in [Token Metadata Standards](../../reference/token-standards/mls03.md): it includes ready-made templates for art, gaming, tickets, credentials, and more. diff --git a/docs/guides/python/issue-token.md b/docs/guides/python/issue-token.md new file mode 100644 index 0000000..a67b9f7 --- /dev/null +++ b/docs/guides/python/issue-token.md @@ -0,0 +1,124 @@ +--- +title: "Issue a Token" +description: "Issue, mint, and manage an MLS-01 fungible token with the Python SDK: wallet client, indexer reads, and wasm encoders." +sidebar_position: 2 +--- + +# Issue a Token + +This guide covers the full MLS-01 token lifecycle in Python: issuing, minting, transferring, and managing supply and authority. The same workflow exists for the [command line](../cli/issue-new-token.md) (which explains the underlying concepts: token id, authority address, reserve vs circulating supply), [Go](../go/issue-token.md), [JavaScript](../javascript/issue-token.md), and [Rust](../rust/issue-token.md). + +Prerequisites: a funded wallet and a running `wallet-rpc-daemon` plus indexer (see [Developer Setup](../../build/development.md) for ports and authentication). + +```mermaid +flowchart LR + ISSUE["issue
(defines supply policy)"] --> MINT["mint
(reserve into circulation)"] + MINT --> SEND["transfer / burn"] + MINT --> UNMINT["unmint"] + ISSUE -.-> LOCK["lock supply"] + ISSUE -.-> FREEZE["freeze"] +``` + +## Issuing + +```python +from mintlayer.wallet import Client, IssueTokenParams, TokenMetadata, TokenSupply + +wc = Client("http://127.0.0.1:3034") + +authority_addr = wc.new_address(0) + +result = wc.issue_token( + IssueTokenParams( + account=0, + destination_address=authority_addr, + metadata=TokenMetadata( + token_ticker="MYTOKEN", + number_of_decimals=8, + metadata_uri="https://example.com/mytoken.json", + token_supply=TokenSupply(type="Lockable"), + is_freezable=False, + ), + ) +) +print(f"token id: {result.token_id} tx: {result.tx_id}") +``` + +Supply policies: `TokenSupply(type="Lockable")` (unlimited minting until locked), `"Unlimited"`, or `"Fixed"` with a supply amount for a hard cap. The destination address becomes the **token authority**: the key that controls future operations (minting, freezing, authority transfer). Keep it secure. + +## Metadata + +Publish metadata at the metadata URI following the [Token Metadata Standards](../../reference/token-standards/mls01.md) (MLS-01 schema) so wallets and explorers can render your token. The URI can be updated later by the authority. + +## Minting and unminting + +Wait for the issuance transaction to confirm before minting. Minting moves tokens from the reserve into circulation; unminting reverses it. + +```python +from mintlayer.wallet import Amount, MintParams, UnmintParams + +mint_result = wc.mint_tokens( + MintParams( + account=0, + token_id=result.token_id, + address=recipient_addr, + amount=Amount(atoms="100000"), # in smallest token units + ) +) + +wc.unmint_tokens( + UnmintParams( + account=0, + token_id=result.token_id, + amount=Amount(atoms="50000"), + ) +) +``` + +## Transferring and burning + +```python +from mintlayer.wallet import TokenSendParams + +wc.send_token( + TokenSendParams( + account=0, + token_id=result.token_id, + address=recipient_addr, + amount=Amount(atoms="10000"), + ) +) +``` + +## Authority, freeze, and supply lock + +All management operations require the token authority key: + +```python +from mintlayer.wallet import ChangeAuthorityParams, FreezeParams, LockSupplyParams, UnfreezeParams + +# Irreversible: fixes the supply at the current circulating amount. +# Note the field is account_index, not account. +wc.lock_token_supply(LockSupplyParams(account_index=0, token_id=result.token_id)) + +wc.freeze_token(FreezeParams(account=0, token_id=result.token_id, is_unfreezable=True)) +wc.unfreeze_token(UnfreezeParams(account=0, token_id=result.token_id)) +wc.change_token_authority( + ChangeAuthorityParams(account=0, token_id=result.token_id, address=new_authority_addr) +) +``` + +## Reading token state + +```python +from mintlayer.indexer import Client as IndexerClient + +idx = IndexerClient("http://127.0.0.1:3000") + +token = idx.get_token(result.token_id) +print(f"ticker: {token.token_ticker} circulating: {token.circulating_supply.decimal}") +``` + +## Manual transaction building + +For full custody without the wallet daemon, the `wasm` module encodes issuance, mint, and freeze inputs/outputs directly and exposes the protocol fee functions (`fungible_token_issuance_fee`, `token_supply_change_fee`, `token_freeze_fee`, `token_change_authority_fee`). See [Tokens and NFTs](../../build/sdks/python/tokens.md#building-token-transactions-manually) and [Forging custom transactions](custom-transactions.md). diff --git a/docs/guides/python/staking.md b/docs/guides/python/staking.md new file mode 100644 index 0000000..d668724 --- /dev/null +++ b/docs/guides/python/staking.md @@ -0,0 +1,111 @@ +--- +title: "Staking and Delegation" +description: "Run a staking pool or delegate to one with the Python SDK: wallet daemon flows, indexer reads, and wasm encoders." +sidebar_position: 4 +--- + +# Staking and Delegation + +The Python SDK covers both sides of staking: operating a pool and delegating to one. The same workflow exists for the [command line](../cli/managing-a-staking-pool.md) (concepts: pledge, cost per block, margin ratio, decommission key), [Go](../go/staking.md), [JavaScript](../javascript/staking.md) (delegations only), and [Rust](../rust/staking.md). + +The reward split: the staker receives `cost_per_block + margin × (block_reward − cost_per_block)`; delegators share the remainder proportionally to their stake. + +## Creating a pool + +```python +from mintlayer.wallet import Amount, Client, CreatePoolParams + +wc = Client("http://127.0.0.1:3034") + +result = wc.create_stake_pool( + CreatePoolParams( + account=0, + amount=Amount(atoms="40000000000000"), # minimum pledge + cost_per_block=Amount(atoms="100000000"), # flat fee per block + margin_ratio_per_thousand="100", # 10% staker cut + decommission_address=decommission_addr, + # staker_address and vrf_public_key default to wallet-managed keys + ) +) +print(f"tx id: {result.tx_id}") +``` + +`margin_ratio_per_thousand` is the staker's cut of block rewards expressed in thousandths as a **string**: `"100"` = 10%, `"50"` = 5%, `"1000"` = 100%. The pool ID can be predicted before broadcasting with `wasm.get_pool_id`. + +## Operating the pool + +```python +wc.start_staking(0) # begin producing blocks + +status = wc.get_staking_status(0) # Staking | NotStaking +wc.stop_staking(0) # stops production; the pool stays alive + +pools = wc.list_owned_pools(0) +balance = wc.get_pool_balance(0, "mpool1...") + +wc.decommission_stake_pool( + DecommissionParams( + account=0, + pool_id="mpool1...", + output_address=return_addr, + ) +) +``` + +`stop_staking` pauses block production without touching delegations or staked funds; `decommission_stake_pool` shuts the pool down and returns the pledge to `output_address` after maturity. Delegators must withdraw separately. + +## Delegating + +Create the delegation record first, then fund it (waiting for the creation transaction to confirm in between): + +```python +from mintlayer.wallet import Amount, CreateDelegationParams, DelegateParams + +create_result = wc.create_delegation( + CreateDelegationParams( + account=0, + address=owner_addr, # address that can withdraw funds + pool_id="mpool1...", + ) +) +print(f"delegation id: {create_result.delegation_id}") + +wc.delegate_staking( + DelegateParams( + account=0, + amount=Amount(atoms="10000000000000"), # 100 ML + delegation_id=create_result.delegation_id, + ) +) +``` + +Multiple `delegate_staking` transactions to the same delegation increase the stake. Withdrawals arrive after the consensus lock period: + +```python +from mintlayer.wallet import WithdrawParams + +wc.withdraw_from_delegation( + WithdrawParams( + account=0, + address=recipient_addr, + amount=Amount(atoms="5000000000000"), # 50 ML + delegation_id="mdelg1...", + ) +) + +delegations = wc.list_delegations(0) +``` + +## Reading state from the indexer + +```python +pool = idx.get_pool("mpool1...") +print(f"staker: {pool.staker_balance.decimal} delegations: {pool.delegations_balance.decimal}") + +delegation = idx.get_delegation("mdelg1...") +print(f"next nonce: {delegation.next_nonce}") +``` + +## Manual transaction building + +The `wasm` module encodes pool and delegation outputs/inputs for full-custody flows: `encode_output_create_stake_pool` with `encode_stake_pool_data`, `encode_output_delegate_staking`, `encode_input_for_withdraw_from_delegation` (nonce from the indexer), and `get_pool_id` / `get_delegation_id` to predict ids from the inputs. See [Staking](../../build/sdks/python/staking.md) in the SDK reference for the full encoders. diff --git a/docs/guides/python/trading-with-orders.md b/docs/guides/python/trading-with-orders.md new file mode 100644 index 0000000..e213c8e --- /dev/null +++ b/docs/guides/python/trading-with-orders.md @@ -0,0 +1,78 @@ +--- +title: "Trading with Orders" +description: "Create, fill, and conclude on-chain orders with the Python SDK: wallet daemon lifecycle plus wasm encoders for manual flows." +sidebar_position: 5 +--- + +# Trading with Orders + +The Python path to Mintlayer's on-chain order trading. The same workflow exists for the [command line](../cli/trading-with-orders.md) (the order model: give/ask, conclude key, freeze semantics), [Go](../go/trading-with-orders.md), [JavaScript](../javascript/trading-with-orders.md), and [Rust](../rust/trading-with-orders.md). + +An order locks a `give` amount on-chain and asks for an `ask` amount in return. Anyone may fill it (fully or partially); the maker can freeze it and conclude it to reclaim the remainder: + +```mermaid +stateDiagram-v2 + [*] --> Open : create (locks the give amount) + Open --> Open : fill by takers (partial fills allowed) + Open --> Frozen : freeze (maker only) + Open --> Concluded : conclude + Frozen --> Concluded : conclude + Concluded --> [*] +``` + +## Creating an order + +```python +from mintlayer.wallet import Amount, CreateOrderParams, OutputValue + +created = wc.create_order( + CreateOrderParams( + account=0, + ask=OutputValue.coins("1000000000000"), # 1 ML asked + give=OutputValue.tokens(token_id, "500000"), # tokens given + conclude_address="mtc1q_conclude...", + ) +) +``` + +## Discovering orders + +```python +own = wc.list_own_orders(0) + +active = wc.list_all_active_orders(...) # optional currency filters +``` + +The indexer provides the same listings read-only: `idx.list_orders(...)`, `idx.list_orders_by_pair(...)`, `idx.get_order(...)`; the node exposes `order_info` and `orders_info_by_currencies`. + +## Filling an order + +The fill amount is denominated in the **ask** currency: + +```python +from mintlayer.wallet import Amount, FillOrderParams + +wc.fill_order( + FillOrderParams( + account=0, + order_id="mordr1...", + fill_amount_in_ask_currency=Amount(atoms="100000"), + output_address="mtc1q_destination...", + ) +) +``` + +## Concluding and freezing (maker) + +```python +from mintlayer.wallet import ConcludeOrderParams, FreezeOrderParams + +wc.conclude_order(ConcludeOrderParams(account=0, order_id="mordr1...")) +wc.freeze_order(FreezeOrderParams(account=0, order_id="mordr1...")) +``` + +Concluding returns the unclaimed `give` remainder plus any accumulated `ask` balance to the order's conclude destination. Only the maker can freeze or conclude. + +## Manual flows + +With the `wasm` module, `encode_create_order_output` builds the order output and `get_order_id` predicts the id from the inputs; fills use `encode_input_for_fill_order`, whose inputs must **not** be signed (`encode_witness_no_signature`). To **update** an existing order (new price or amounts), compose a transaction that consumes the conclude-order input and re-creates the order: see [Composing UTXOs](utxo-composition.md). See [Tokens and NFTs](../../build/sdks/python/tokens.md) in the SDK reference for the encoders. diff --git a/docs/guides/python/utxo-composition.md b/docs/guides/python/utxo-composition.md new file mode 100644 index 0000000..b6a899a --- /dev/null +++ b/docs/guides/python/utxo-composition.md @@ -0,0 +1,151 @@ +--- +title: "Composing UTXOs: Updating an Order" +description: "How to compose UTXOs on Mintlayer: update a DEX order atomically by consuming its conclude-order input and re-creating it with a create-order output." +sidebar_position: 9 +--- + +# Composing UTXOs: Updating an Order + +Mintlayer is a UTXO chain: **there is no mutable state**. A DEX order is an unspent output, and every protocol object behaves the same way, so "updating" an order means spending it and re-creating it *in the same transaction*. This atomic spend-and-recreate pattern is the essence of UTXO composition. + +## The pattern + +An order lives as a `CreateOrder` output holding the `give` balance plus any accumulated `ask` balance. Its spend authority is the maker, gated by the order's **nonce**. To update the order (new price, new amounts), build one transaction that: + +1. consumes the order via a **conclude-order input** (this releases the whole order balance), and +2. re-locks the funds into a fresh **create-order output** with the new parameters. + +```mermaid +flowchart LR + OLD["Order UTXO
(give 100, ask 0)"] -->|"conclude-order input
(nonce n)"| TX["Transaction"] + FEE["Coin UTXO
(fee payer)"] --> TX + TX -->|"create-order output
(give 100, ask 20)"| NEW["New order UTXO
(new order id)"] + TX -->|"change"| MAKER["Maker address"] +``` + +Two consequences worth internalizing: + +- The **new order gets a new ID** (IDs are derived from inputs; see [predicting IDs](custom-transactions.md#predicting-ids)). Publish the new ID and retire the old one. +- If the transaction fails or is dropped, nothing changed: the old order is still live and its nonce unspent. + +:::note[One order operation per transaction] + +Consensus allows at most **one order operation input** (fill, conclude, or freeze) per transaction, and that single slot is shared with token account commands: you cannot combine an order operation with a mint, an unmint, or any other command input. Creating orders is capped the same way — at most one `CreateOrder` output per transaction. A second command input or a second created order makes the transaction invalid (`MultipleAccountCommands` / `MultipleOrdersCreated` in the verifier). The update pattern above sits exactly at both limits: one conclude input, one new order. + +::: + +## The ingredients + +| Piece | Encoder | Source of truth | +| ----- | ------- | --------------- | +| Conclude input | `encode_input_for_conclude_order(order_id, nonce, current_block_height, network)` | `nonce` from the indexer's order record | +| New order output | `encode_create_order_output(ask_amount, ask_token_id, give_amount, give_token_id, conclude_address, network)` | Your new price/amounts | +| Surplus / change outputs | `encode_output_transfer` / `encode_output_token_transfer` | Old balance minus new give amount | +| Fee input | `encode_input_for_utxo` | A coin UTXO you own | +| Witness for the conclude input | `encode_witness` with `TxAdditionalInfo` | Order balances from the indexer | +| Witness for the coin input | `encode_witness` | n/a | + +## Fetch the order state + +```python +from mintlayer.indexer import Client + +idx = Client("http://127.0.0.1:3000") + +order = idx.get_order(old_order_id) +print(f"give: {order.give_balance.atoms} ask: {order.ask_balance.atoms} nonce: {order.nonce}") +``` + +The `ask_balance` matters: any asks accumulated from takers are part of the order you are concluding, so the new order (or a change output) must account for them. + +## Build the transaction + +Example: top up the `give` side and raise the ask. Old order: give 100 tokens / ask 10 coins. New order: give 150 / ask 12. The conclude releases 100 tokens plus accumulated asks; a separate token UTXO adds the extra 50. + +```python +# 1. Consume the old order +conclude_input = c.encode_input_for_conclude_order( + old_order_id, order.nonce, tip.block_height, Network.MAINNET +) + +# 2. Fee payer +src_id = c.encode_outpoint_source_id(fee_txid_bytes, SOURCE_TRANSACTION) +fee_input = c.encode_input_for_utxo(src_id, 0) + +# 3. Re-create the order with updated parameters +new_order_output = c.encode_create_order_output( + Amount(atoms="12"), None, # ask 12 coins (None token id = coin) + Amount(atoms="150"), give_token_id, # give 150 tokens + conclude_address, # same conclude destination + Network.MAINNET, +) + +# 4. Any balance not re-locked returns via change outputs +change_output = c.encode_output_transfer( + Amount(atoms="2000000000"), maker_addr, Network.MAINNET # fee change +) + +tx = c.encode_transaction(conclude_input + fee_input, new_order_output + change_output, 0) +``` + +Token inputs (e.g. a UTXO holding the additional 50 tokens) encode with `encode_input_for_utxo` like any other spend; only the *outputs* distinguish coin from token value. + +## Sign it + +The conclude input's sighash requires the order's balances via `TxAdditionalInfo`; this is the part that differs from a plain transfer: + +```python +from mintlayer.wasm import OrderBalance, OrderInfo, SimpleCurrencyAmount, TxAdditionalInfo + +additional_info = TxAdditionalInfo( + order_info={ + old_order_id: OrderInfo( + initially_asked=SimpleCurrencyAmount.coins(order.initially_asked.atoms), + initially_given=SimpleCurrencyAmount.tokens( + give_token_id, order.initially_given.atoms + ), + ask_balance=OrderBalance(atoms=order.ask_balance.atoms), + give_balance=OrderBalance(atoms=order.give_balance.atoms, token_id=give_token_id), + ) + } +) + +witness_bytes = b"" +for i in range(2): # one witness per input, in input order + witness_bytes += c.encode_witness( + SIGHASH_ALL, + maker_key, + maker_addr, + tx, + input_utxos, + i, + additional_info, # required for the order input + tip.block_height, + Network.MAINNET, + ) + +signed_tx = c.encode_signed_transaction(tx, witness_bytes) +``` + +Submit via the indexer or node as usual, then predict and store the new order ID: + +```python +new_order_id = c.get_order_id(conclude_input + fee_input, Network.MAINNET) +``` + +## Variations of the same pattern + +The conclude-then-recreate transaction is one instance of a general tool. With the same encoders you can: + +- **Cancel an order**: conclude input, no create-order output; the balance flows to change outputs. +- **Self-fill and requote**: conclude input + `FillOrder`-style split: partially fill your own order and re-create a smaller one. (A pure fill by a taker uses `encode_input_for_fill_order` and needs **no signature** for that input: `encode_witness_no_signature`.) +- **Freeze before restructuring**: `encode_input_for_freeze_order` stops takers while you prepare the replacement transaction. +- **Chain protocol objects**: the same spend-and-recreate logic composes delegations (withdraw input + `encode_output_delegate_staking` output) and any future output type. + +:::note + +Exactly which balances must be re-assigned to which outputs is enforced by consensus, not by the encoders: the runtime tells you *if* a transaction is invalid, the indexer tells you *what* the balances are, and the composition is yours to design. + +::: + +For the order model itself (give/ask, conclude key, freeze semantics), see [Trading with Orders](trading-with-orders.md) and the wallet-cli guide [Trading with orders](../cli/trading-with-orders.md). diff --git a/docs/guides/python/wallet-wasm.md b/docs/guides/python/wallet-wasm.md new file mode 100644 index 0000000..586d4dc --- /dev/null +++ b/docs/guides/python/wallet-wasm.md @@ -0,0 +1,120 @@ +--- +title: "Wallet & Signing (WASM)" +description: "Generate a Mintlayer wallet and sign transactions and messages with the Python SDK's embedded WASM runtime: no wallet daemon, full custody." +sidebar_position: 7 +--- + +# Wallet & Signing (WASM) + +The Python SDK embeds the same WASM cryptography runtime that the wallets use ([wasmtime](https://wasmtime.dev/), bundled with the package). With it you can generate and restore wallets, derive addresses, and sign transactions entirely in your own process: there is no wallet daemon holding your keys. + +This guide covers the wallet side; building arbitrary transactions is covered in [Forging custom transactions](custom-transactions.md) and the [Building transactions](../../build/sdks/python/transactions.md) reference. + +```python +from mintlayer.wasm import Client + +c = WasmClient() +``` + +Instantiation takes ~400 ms; create the client once per process and call `c.close()` when done. + +## Generating a wallet + +A Mintlayer wallet is a **BIP-39 mnemonic** plus the derivation hierarchy the runtime implements (`44'/coin_type'/0'` for the default account, then `/0/i` for receiving and `/1/i` for change keys). Generate the 12/24 words with any standard BIP-39 library, then hand them to the runtime: + +```python +mnemonic = "legal winner thank year wave sausage worth useful legal winner thank yellow" + +# Default account extended private key +account_key = c.make_default_account_privkey(mnemonic, Network.MAINNET) + +# First receiving address +recv_key = c.make_receiving_address(account_key, 0) +pub_key = c.public_key_from_private_key(recv_key) +addr = c.pubkey_to_pubkeyhash_address(pub_key, Network.MAINNET) +print(addr) # mmt1q... / mtc1q... depending on network +``` + +Derive more addresses by iterating the key index; change addresses derive identically via `make_change_address(account_key, i)`. + +:::danger + +The mnemonic is the wallet. Anyone with the words holds every derived key. Generate it in a secure environment, never log it, and persist it (encrypted) before using the derived keys for anything valuable. + +::: + +### Single-key wallets + +For ephemeral or single-purpose keys (faucet hot keys, tests), skip the mnemonic entirely: + +```python +priv_key = c.make_private_key() # random key +pub_key = c.public_key_from_private_key(priv_key) +addr = c.pubkey_to_pubkeyhash_address(pub_key, Network.TESTNET) +``` + +### Watch-only wallets + +From an extended private key you can export the extended public key and derive **addresses without any private key**, useful for monitoring services: + +```python +account_xpub = c.extended_public_key_from_extended_private_key(account_key) + +# First receiving address, public derivation only +recv_pub = c.make_receiving_address_public_key(account_xpub, 0) +``` + +## Signing a transaction + +Signing follows the eight-step flow documented in [Building transactions](../../build/sdks/python/transactions.md): encode inputs and outputs, build the unsigned transaction, produce witness bytes per input with `encode_witness`, and assemble with `encode_signed_transaction`. Condensed: + +```python +transaction = c.encode_transaction(encoded_inputs, encoded_outputs, 0) + +witness_bytes = b"" +for i in range(len(utxos)): + witness_bytes += c.encode_witness( + SIGHASH_ALL, + spend_key, # private key from derivation above + from_addr, # address owning the UTXO + transaction, + input_utxos, # per-input UTXO entries (see the reference) + i, + TxAdditionalInfo(), + 0, # block height, 0 when no timelock constraint + Network.MAINNET, + ) + +signed_tx = c.encode_signed_transaction(transaction, witness_bytes) +``` + +Inspect the result without broadcasting: `c.get_transaction_id(signed_tx, True)`. Broadcast via the indexer (`submit_transaction`) or the node (`p2p_submit_transaction`). + +Special inputs use dedicated witnesses: `encode_witness_no_signature` (fill-order inputs need no signature), `encode_witness_htlc_spend` and `encode_witness_htlc_refund_single_sig` (HTLC inputs). + +## Signing messages + +Prove control of an address with a challenge-response exchange: + +```python +# Signer side +signature = c.sign_challenge(spend_key, b"login-nonce-1234") + +# Verifier side +ok = c.verify_challenge(addr, Network.MAINNET, signature, b"login-nonce-1234") +``` + +`verify_challenge` checks the signature against the bech32m address. For spending-authorization messages (transaction intents), use `sign_message_for_spending` / `verify_signature_for_spending` instead; see [Transaction intents](custom-transactions.md#transaction-intents). + +## Cheat sheet + +| Task | Call | +| ---- | ---- | +| Wallet from mnemonic | `make_default_account_privkey` | +| Receiving / change key | `make_receiving_address` / `make_change_address` | +| Address from key | `public_key_from_private_key` → `pubkey_to_pubkeyhash_address` | +| Random key | `make_private_key` | +| Watch-only derivation | `extended_public_key_from_extended_private_key` + `make_receiving_address_public_key` | +| Sign one input | `encode_witness` | +| Assemble signed tx | `encode_signed_transaction` | +| Sign a message | `sign_challenge` / `verify_challenge` | diff --git a/docs/guides/rust/index.md b/docs/guides/rust/index.md index b740b99..12c306d 100644 --- a/docs/guides/rust/index.md +++ b/docs/guides/rust/index.md @@ -8,7 +8,7 @@ sidebar_position: 1 These guides implement Mintlayer's main workflows with the [Rust SDK](../../build/sdks/rust/index.md) (`mintlayer-sdk`). The `wallet` sub-client talks to a running `wallet-rpc-daemon`, the `indexer` client reads chain state, and the `crypto` module builds and signs transactions natively, with no wallet daemon in the loop. Start with the SDK [overview](../../build/sdks/rust/index.md) for features and the mandatory `parity-scale-codec` patch when using the `crypto` feature. -The wallet-cli versions of the same workflows live in the [command-line guides](../cli/index.md); the [Go](../go/index.md) and [JavaScript](../javascript/index.md) versions in their own sections. +The wallet-cli versions of the same workflows live in the [command-line guides](../cli/index.md); the [Go](../go/index.md), [JavaScript](../javascript/index.md), and [Python](../python/index.md) versions in their own sections. | Guide | Contents | | ----- | -------- | diff --git a/docs/intro.md b/docs/intro.md index 5cc3d48..3f4d9a5 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -13,18 +13,18 @@ Mintlayer is a Bitcoin sidechain with a built-in PoS consensus, native token iss - **Use the wallet**, the [Wallet CLI reference](wallet/cli/commands.md) documents every command; for automation use the [Wallet RPC API](wallet/rpc/overview.md). - **Issue a token**, the [token guide](guides/cli/issue-new-token.md) walks through issuing, minting, and sending MLS-01 tokens; there is a separate guide for [NFTs](guides/cli/issuing-and-managing-an-nft.md). - **Use the Mojito Wallet**, the non-custodial mobile and browser-extension wallet: see [Mojito Wallet](wallet/mojito-wallet.md). -- **Build dApps**, integrate wallets with the [JavaScript SDK](build/sdks/javascript/index.md), [Rust SDK](build/sdks/rust/index.md), or [Mojito Inject](build/mojito-inject.md), and query chain data via the [API](api/index.md). +- **Build dApps**, integrate wallets with the [JavaScript SDK](build/sdks/javascript/index.md), [Rust](build/sdks/rust/index.md) or [Python](build/sdks/python/index.md) SDKs, or [Mojito Inject](build/mojito-inject.md), and query chain data via the [API](api/index.md). ## Documentation map | Section | Contents | | ------- | -------- | | [Getting Started](getting-started/index.md) | Node vs. wallet, installation methods | -| [Guides](guides/cli/managing-a-staking-pool.md) | Staking pools, tokens, NFTs, multisig, orders, atomic swaps (wallet-cli) | | [Node](node/index.md) | Running, upgrading, firewall, node commands | | [Wallet](wallet/mojito-wallet.md) | Mojito Wallet, addresses, Wallet CLI, Wallet RPC, Trezor | | [API](api/index.md) | Indexer REST API: endpoints, conventions | -| [Build](build/index.md) | SDKs (Go, JavaScript, Rust), developer guides, Mojito Inject, MCP, Bridge | +| [Guides](guides/index.md) | Tokens, NFTs, staking, orders, atomic swaps: wallet-cli, Go, JavaScript, Rust, Python | +| [Build](build/index.md) | SDKs (JavaScript, Go, Rust, Python), developer guides, Mojito Inject, MCP, Bridge | | [Whitepaper](whitepaper/1-blockchain-architecture.md) | Protocol concepts and design | | [Reference](reference/block-and-transaction-serialization.md) | Serialization, CLI reference, advanced tools | | [FAQ](faq.md) | Testnet tokens, fees, finality, staking requirements | diff --git a/docs/node/_category_.json b/docs/node/_category_.json index 097a9d9..5b01658 100644 --- a/docs/node/_category_.json +++ b/docs/node/_category_.json @@ -1,4 +1,4 @@ { "label": "Node", - "position": 4 + "position": 3 } diff --git a/docs/node/upgrading/index.md b/docs/node/upgrading/index.md index 8834cf7..f62a867 100644 --- a/docs/node/upgrading/index.md +++ b/docs/node/upgrading/index.md @@ -62,7 +62,7 @@ After starting the new version: - Check the reported version: `node_version` over [RPC](../node-rpc.md), or `version` inside wallet-cli. - Confirm the node is syncing (or already at the chain tip): `chainstate_info` over RPC, or watch the logs. -- If you run an API server, verify it serves data (`GET /api/v2/chain/tip`); note that some releases bump the indexer storage version, which triggers an automatic full resync on first launch. +- If you run an API server, verify it serves data (`GET /api/v2/chain/tip`); note that some releases bump the indexer storage version, which triggers an automatic full resync on first launch (the scanner drops and re-initializes its database from scratch — see the [scanner reference](../../reference/cli/api-blockchain-scanner-daemon.md#storage-version-and-resyncs)). ## Version-specific guides diff --git a/docs/reference/cli/api-blockchain-scanner-daemon.md b/docs/reference/cli/api-blockchain-scanner-daemon.md index 36f9f50..7a192f5 100644 --- a/docs/reference/cli/api-blockchain-scanner-daemon.md +++ b/docs/reference/cli/api-blockchain-scanner-daemon.md @@ -58,6 +58,15 @@ api-blockchain-scanner-daemon [OPTIONS] - Env: `ML_API_SCANNER_DAEMON_POSTGRES_MAX_CONNECTIONS` - Default: `10` +## Storage version and resyncs + +The scanner's PostgreSQL database layout is versioned. On startup, the scanner compares the storage version recorded in the database against the version it was built with (version `27` in current releases). On a mismatch it drops and re-initializes the database from scratch and performs a full re-scan from genesis. + +This is automatic, but it has operational consequences: + +- Upgrading to a release that bumps the storage version triggers a full resync on first launch — plan the downtime (and disk space) accordingly. +- Until the re-scan catches up with the chain tip, the API server serves partial data (see [Data freshness](/docs/api/conventions.md#data-freshness)). + ## Related - [API Web Server](api-web-server.md), serves HTTP API requests using the data indexed by this scanner. diff --git a/docs/wallet/_category_.json b/docs/wallet/_category_.json index ddf90d0..f708dec 100644 --- a/docs/wallet/_category_.json +++ b/docs/wallet/_category_.json @@ -1,4 +1,4 @@ { "label": "Wallet", - "position": 5 + "position": 4 } diff --git a/docusaurus.config.ts b/docusaurus.config.ts index d548204..2ac94c6 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -60,6 +60,12 @@ const config: Config = { // The site is served from the domain root (docs.mintlayer.org). baseUrl: '/', + // Emit canonical URLs, og:url, and sitemap entries WITH trailing slashes so + // they match what Cloudflare Workers actually serves (auto-trailing-slash). + // Without this, every page exists as /x (sitemap) and /x/ (served) and + // Google records the duplicates as "Alternate page with proper canonical". + trailingSlash: true, + // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. organizationName: 'mintlayer', // Usually your GitHub org/user name. @@ -250,6 +256,10 @@ const config: Config = { label: 'Blog', to: 'https://www.mintlayer.org/blog', }, + { + label: 'Sitemap', + href: 'https://docs.mintlayer.org/sitemap', // generated postbuild (scripts/build-sitemap-page.mjs) + }, { label: 'GitHub', href: 'https://github.com/mintlayer', diff --git a/package.json b/package.json index 86c9a86..0518661 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc", - "postbuild": "node scripts/build-search-index.mjs" + "postbuild": "node scripts/build-search-index.mjs && node scripts/build-sitemap-page.mjs" }, "dependencies": { "@docusaurus/core": "3.7.0", diff --git a/scripts/build-sitemap-page.mjs b/scripts/build-sitemap-page.mjs new file mode 100644 index 0000000..f5f0175 --- /dev/null +++ b/scripts/build-sitemap-page.mjs @@ -0,0 +1,92 @@ +// Generates build/sitemap/index.html: a human-crawlable HTML sitemap +// (index of all documentation pages), grouped by docs section. +// Runs as part of `postbuild`, after the Docusaurus build has produced +// build/sitemap.xml. + +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + +const BASE = 'https://docs.mintlayer.org'; +const sitemapPath = join('build', 'sitemap.xml'); +const outPath = join('build', 'sitemap', 'index.html'); + +const xml = readFileSync(sitemapPath, 'utf8'); +const urls = [...xml.matchAll(/([^<]+)<\/loc>/g)] + .map((m) => m[1]) + .filter((url) => url !== `${BASE}/`); + +const escape = (s) => + s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + +const ACRONYMS = new Set(['api', 'sdk', 'sdks', 'faq', 'nft', 'cli', 'rpc', 'htlc', 'utxo']); +const prettySegment = (s) => + ACRONYMS.has(s) ? s.toUpperCase() : s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); + +// Group pages by the first two path segments (e.g. /docs/wallet/cli), +// so large sections like the wallet CLI reference break into subgroups. +const groups = new Map(); +for (const url of urls) { + const path = new URL(url).pathname; + const segs = path.replace(/\/+$/, '').split('/').filter(Boolean); + const key = segs.length > 2 ? `/${segs.slice(0, 2).join('/')}/` : `/${segs.join('/')}/`; + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(url); +} + +const groupEntries = [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0])); +const total = urls.length; + +const sectionsHtml = groupEntries + .map(([key, groupUrls]) => { + const title = key + .split('/') + .filter((s) => s && s.toLowerCase() !== 'docs') + .map(prettySegment) + .join(' › '); + const items = groupUrls + .sort((a, b) => a.localeCompare(b)) + .map((url) => { + const label = decodeURIComponent(new URL(url).pathname) + .replace(/\/+$/, '') + .split('/') + .pop(); + return `
  • ${escape(label || '/')}
  • `; + }) + .join('\n'); + return `
    \n

    ${escape(title || '/')} ${groupUrls.length}

    \n
      \n${items}\n
    \n
    `; + }) + .join('\n'); + +const html = ` + + + + + Sitemap | Mintlayer Documentation + + + + + +
    +

    ← docs.mintlayer.org

    +

    Mintlayer documentation sitemap

    +

    Index of all ${total} documentation pages. The machine-readable sitemap is at sitemap.xml.

    +${sectionsHtml} +
    + + +`; + +mkdirSync(dirname(outPath), { recursive: true }); +writeFileSync(outPath, html); +console.log(`sitemap page: ${total} URLs in ${groupEntries.length} sections -> ${outPath}`); diff --git a/static/_redirects b/static/_redirects index 6861bff..b2bb041 100644 --- a/static/_redirects +++ b/static/_redirects @@ -1,37 +1,68 @@ # Old Jekyll docs URLs -> new Docusaurus paths -/whitepaper/1-blockchain-architecture /docs/whitepaper/blockchain-architecture 301 -/whitepaper/2-mintlayer-wallet /docs/whitepaper/mintlayer-wallet 301 -/whitepaper/3-tokenization-standard /docs/whitepaper/tokenization-standard 301 -/whitepaper/4-decentralized-finance-defi /docs/whitepaper/decentralized-finance-defi 301 -/whitepaper/5-decentralized-exchange-dex /docs/whitepaper/decentralized-exchange-dex 301 -/whitepaper/6-bitcoin-mintlayer-atomic-swaps /docs/whitepaper/bitcoin-mintlayer-atomic-swaps 301 -/whitepaper/7-token-and-public-sale /docs/whitepaper/token-and-public-sale 301 -/testnet/node-setup-cli /docs/getting-started/install/install-from-binaries 301 -/testnet/node-setup-docker /docs/getting-started/install/install-from-docker 301 +/whitepaper/1-blockchain-architecture /docs/whitepaper/blockchain-architecture/ 301 +/whitepaper/1-blockchain-architecture/ /docs/whitepaper/blockchain-architecture/ 301 +/whitepaper/2-mintlayer-wallet /docs/whitepaper/mintlayer-wallet/ 301 +/whitepaper/2-mintlayer-wallet/ /docs/whitepaper/mintlayer-wallet/ 301 +/whitepaper/3-tokenization-standard /docs/whitepaper/tokenization-standard/ 301 +/whitepaper/3-tokenization-standard/ /docs/whitepaper/tokenization-standard/ 301 +/whitepaper/4-decentralized-finance-defi /docs/whitepaper/decentralized-finance-defi/ 301 +/whitepaper/4-decentralized-finance-defi/ /docs/whitepaper/decentralized-finance-defi/ 301 +/whitepaper/5-decentralized-exchange-dex /docs/whitepaper/decentralized-exchange-dex/ 301 +/whitepaper/5-decentralized-exchange-dex/ /docs/whitepaper/decentralized-exchange-dex/ 301 +/whitepaper/6-bitcoin-mintlayer-atomic-swaps /docs/whitepaper/bitcoin-mintlayer-atomic-swaps/ 301 +/whitepaper/6-bitcoin-mintlayer-atomic-swaps/ /docs/whitepaper/bitcoin-mintlayer-atomic-swaps/ 301 +/whitepaper/7-token-and-public-sale /docs/whitepaper/token-and-public-sale/ 301 +/whitepaper/7-token-and-public-sale/ /docs/whitepaper/token-and-public-sale/ 301 +/testnet/node-setup-cli /docs/getting-started/install/install-from-binaries/ 301 +/testnet/node-setup-cli/ /docs/getting-started/install/install-from-binaries/ 301 +/testnet/node-setup-docker /docs/getting-started/install/install-from-docker/ 301 +/testnet/node-setup-docker/ /docs/getting-started/install/install-from-docker/ 301 # Legacy consensus paper PDFs -> Pulsar paper on arXiv -/docs/consensus-paper.pdf https://arxiv.org/abs/2411.14245 301 -/docs/DSA-consensus-paper-draft.pdf https://arxiv.org/abs/2411.14245 301 +/docs/consensus-paper.pdf https://arxiv.org/abs/2411.14245/ 301 +/docs/consensus-paper.pdf/ https://arxiv.org/abs/2411.14245/ 301 +/docs/DSA-consensus-paper-draft.pdf https://arxiv.org/abs/2411.14245/ 301 +/docs/DSA-consensus-paper-draft.pdf/ https://arxiv.org/abs/2411.14245/ 301 # Chainstate DB dumper and DNS server moved under the command-line reference -/docs/reference/chainstate-db-dumper /docs/reference/cli/chainstate-db-dumper 301 -/docs/reference/dns-server /docs/reference/cli/dns-server 301 +/docs/reference/chainstate-db-dumper /docs/reference/cli/chainstate-db-dumper/ 301 +/docs/reference/chainstate-db-dumper/ /docs/reference/cli/chainstate-db-dumper/ 301 +/docs/reference/dns-server /docs/reference/cli/dns-server/ 301 +/docs/reference/dns-server/ /docs/reference/cli/dns-server/ 301 # Guides restructured: top-level /docs/guides with cli, go, javascript sections -/docs/category/guides /docs/guides/cli 301 -/docs/wallet/guides/issue-new-token /docs/guides/cli/issue-new-token 301 -/docs/wallet/guides/issuing-and-managing-an-nft /docs/guides/cli/issuing-and-managing-an-nft 301 -/docs/wallet/guides/managing-a-staking-pool /docs/guides/cli/managing-a-staking-pool 301 -/docs/wallet/guides/managing-a-multisig-address /docs/guides/cli/managing-a-multisig-address 301 -/docs/wallet/guides/trading-with-orders /docs/guides/cli/trading-with-orders 301 -/docs/wallet/guides/atomic-swap-with-bitcoin /docs/guides/cli/atomic-swap-with-bitcoin 301 -/docs/wallet/guides/migrate-erc20-token-to-mainnet /docs/guides/cli/migrate-erc20-token-to-mainnet 301 -/docs/build/guides /docs/guides 301 -/docs/build/guides/issue-token /docs/guides 301 -/docs/build/guides/issue-nft /docs/guides 301 -/docs/build/guides/staking /docs/guides 301 -/docs/build/guides/trading-with-orders /docs/guides 301 -/docs/build/guides/atomic-swap /docs/guides 301 -/docs/build/guides/wallet-wasm /docs/guides/go/wallet-wasm 301 -/docs/build/guides/custom-transactions /docs/guides/go/custom-transactions 301 -/docs/build/guides/utxo-composition /docs/guides/go/utxo-composition 301 +/docs/category/guides /docs/guides/cli/ 301 +/docs/category/guides/ /docs/guides/cli/ 301 +/docs/wallet/guides/issue-new-token /docs/guides/cli/issue-new-token/ 301 +/docs/wallet/guides/issue-new-token/ /docs/guides/cli/issue-new-token/ 301 +/docs/wallet/guides/issuing-and-managing-an-nft /docs/guides/cli/issuing-and-managing-an-nft/ 301 +/docs/wallet/guides/issuing-and-managing-an-nft/ /docs/guides/cli/issuing-and-managing-an-nft/ 301 +/docs/wallet/guides/managing-a-staking-pool /docs/guides/cli/managing-a-staking-pool/ 301 +/docs/wallet/guides/managing-a-staking-pool/ /docs/guides/cli/managing-a-staking-pool/ 301 +/docs/wallet/guides/managing-a-multisig-address /docs/guides/cli/managing-a-multisig-address/ 301 +/docs/wallet/guides/managing-a-multisig-address/ /docs/guides/cli/managing-a-multisig-address/ 301 +/docs/wallet/guides/trading-with-orders /docs/guides/cli/trading-with-orders/ 301 +/docs/wallet/guides/trading-with-orders/ /docs/guides/cli/trading-with-orders/ 301 +/docs/wallet/guides/atomic-swap-with-bitcoin /docs/guides/cli/atomic-swap-with-bitcoin/ 301 +/docs/wallet/guides/atomic-swap-with-bitcoin/ /docs/guides/cli/atomic-swap-with-bitcoin/ 301 +/docs/wallet/guides/migrate-erc20-token-to-mainnet /docs/guides/cli/migrate-erc20-token-to-mainnet/ 301 +/docs/wallet/guides/migrate-erc20-token-to-mainnet/ /docs/guides/cli/migrate-erc20-token-to-mainnet/ 301 +/docs/build/guides /docs/guides/ 301 +/docs/build/guides/ /docs/guides/ 301 +/docs/build/guides/issue-token /docs/guides/ 301 +/docs/build/guides/issue-token/ /docs/guides/ 301 +/docs/build/guides/issue-nft /docs/guides/ 301 +/docs/build/guides/issue-nft/ /docs/guides/ 301 +/docs/build/guides/staking /docs/guides/ 301 +/docs/build/guides/staking/ /docs/guides/ 301 +/docs/build/guides/trading-with-orders /docs/guides/ 301 +/docs/build/guides/trading-with-orders/ /docs/guides/ 301 +/docs/build/guides/atomic-swap /docs/guides/ 301 +/docs/build/guides/atomic-swap/ /docs/guides/ 301 +/docs/build/guides/wallet-wasm /docs/guides/go/wallet-wasm/ 301 +/docs/build/guides/wallet-wasm/ /docs/guides/go/wallet-wasm/ 301 +/docs/build/guides/custom-transactions /docs/guides/go/custom-transactions/ 301 +/docs/build/guides/custom-transactions/ /docs/guides/go/custom-transactions/ 301 +/docs/build/guides/utxo-composition /docs/guides/go/utxo-composition/ 301 +/docs/build/guides/utxo-composition/ /docs/guides/go/utxo-composition/ 301 + diff --git a/static/sitemap_index.xml b/static/sitemap_index.xml new file mode 100644 index 0000000..a083ebe --- /dev/null +++ b/static/sitemap_index.xml @@ -0,0 +1,6 @@ + + + + https://docs.mintlayer.org/sitemap.xml + +