diff --git a/docs/sdk/bindings.md b/docs/sdk/bindings.md index 1d490e9..dddd848 100644 --- a/docs/sdk/bindings.md +++ b/docs/sdk/bindings.md @@ -4,26 +4,37 @@ title: Bindings # Bindings -The Wallet SDK is written in Rust, but most of it is also available from JavaScript and Python. The -bindings are generated from one shared description of the API (the `chia-sdk-bindings` crate and its -`bindings/*.json` files), so the three packages expose the same classes and functions, adjusted to each -language's naming conventions. They are released together with the Rust crate and share its version -number. - -| Package | Language | Built with | Install | -| ---------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------- | ---------------------------------- | -| [`chia-wallet-sdk`](https://www.npmjs.com/package/chia-wallet-sdk) | Node.js (TypeScript types) | [napi-rs](https://napi.rs), native addon | `npm install chia-wallet-sdk` | -| [`chia-wallet-sdk-wasm`](https://www.npmjs.com/package/chia-wallet-sdk-wasm) | Browsers and bundlers (TypeScript types) | [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/) | `npm install chia-wallet-sdk-wasm` | -| [`chia-wallet-sdk`](https://pypi.org/project/chia-wallet-sdk/) | Python 3.8+ (`.pyi` stubs) | [PyO3](https://pyo3.rs) and maturin | `pip install chia-wallet-sdk` | +The Wallet SDK is written in Rust, but most of it is also available from JavaScript, Python, Go, .NET +and C++. The bindings are generated from one shared description of the API (the `chia-sdk-bindings` +crate and its `bindings/*.json` files), so every package exposes the same classes and functions, adjusted +to each language's naming conventions. The packages are released together with the Rust crate and share its +version number. The C++ bindings are built from source. + +| Package | Language | Built with | Install | +| -------------------------------------------------------------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------- | +| [`chia-wallet-sdk`](https://www.npmjs.com/package/chia-wallet-sdk) | Node.js (TypeScript types) | [napi-rs](https://napi.rs), native addon | `npm install chia-wallet-sdk` | +| [`chia-wallet-sdk-wasm`](https://www.npmjs.com/package/chia-wallet-sdk-wasm) | Browsers and bundlers (TypeScript types) | [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/) | `npm install chia-wallet-sdk-wasm` | +| [`chia-wallet-sdk`](https://pypi.org/project/chia-wallet-sdk/) | Python 3.8+ (`.pyi` stubs) | [PyO3](https://pyo3.rs) and maturin | `pip install chia-wallet-sdk` | +| [`github.com/xch-dev/chia-wallet-sdk/go`](https://github.com/xch-dev/chia-wallet-sdk/tree/main/go) | Go (cgo) | [UniFFI](https://mozilla.github.io/uniffi-rs/) and uniffi-bindgen-go | `go get github.com/xch-dev/chia-wallet-sdk/go` | +| [`ChiaWalletSdk`](https://www.nuget.org/packages/ChiaWalletSdk) | .NET 8 | UniFFI and uniffi-bindgen-cs | `dotnet add package ChiaWalletSdk` | +| [C++](https://github.com/xch-dev/chia-wallet-sdk/tree/main/cpp) | C++20 | UniFFI and uniffi-bindgen-cpp | Build from source | The Node.js package ships prebuilt binaries for Windows x64, macOS (x64 and Apple silicon) and Linux (x64 and arm64). The Python package has prebuilt wheels for common platforms and imports as `chia_wallet_sdk`. The WASM package runs anywhere WebAssembly does, including browsers, but it can't open raw sockets, so it has a smaller networking surface. +The Go, .NET and C++ bindings are generated with [UniFFI](https://mozilla.github.io/uniffi-rs/) from a +single library crate. Each release of the Go module (tagged `go/v`) includes the generated +bindings and prebuilt static libraries for Linux (amd64, arm64), macOS (amd64, arm64), Windows (amd64, +arm64) and Android (arm64), so it doesn't need a Rust toolchain, only a C compiler for cgo. The NuGet +package includes native libraries for Linux (x64, arm64), macOS (x64, arm64) and Windows (x64). The C++ +bindings aren't packaged: run `cpp/generate.sh` in the SDK repository to build the library and generate +the header and source file, then compile them into your project. + Choose the Rust crate when you can. It has the complete API, including everything that isn't exported -to the bindings. Use the bindings when your application is already in JavaScript or Python and you need -to build, sign, parse or validate spends there. +to the bindings. Use the bindings when your application is in another language and you need to build, +sign, parse or validate spends there. ## How the bindings map to Rust @@ -38,7 +49,7 @@ translation: | `Conditions` builder | An array/list of condition `Program`s from methods like `clvm.createCoin(...)` and `clvm.reserveFee(...)`. Parse them back with `program.parseCreateCoin()` and similar | | `StandardLayer::new(pk).spend(ctx, coin, conditions)` | `clvm.spendStandardCoin(coin, pk, clvm.delegatedSpend(conditions))` | | `StandardLayer::new(pk).spend_with_conditions(...)` | `clvm.standardSpend(pk, clvm.delegatedSpend(conditions))`, which returns a `Spend` to use as an inner spend (for example in `clvm.spendNft(nft, spend)`) | -| Primitive methods (`Cat::spend_all`, `Nft` minting, ...) | Methods on `Clvm`: `spendCats`, `mintNfts`, `spendNft`, `createEveDid`, `spendDid`, `mintVault`, `spendOption`, ... | +| Primitive methods (`Cat::spend_all`, `Nft` minting, ...) | Methods on `Clvm`: `spendCats`, `mintNfts`, `spendNft`, `createEveDid`, `spendDid`, `mintVault`, `spendOption`, `mintPlotNft`, ... | | [Action system](/sdk/action-system) (`Spends`, `Action`, `Id`, `Deltas`) | The same names, with `spends.apply(actions)`, `spends.prepare(deltas)` and `finished.spend()` | | `Simulator`, `BlsPair`, `K1Pair`, `R1Pair` | The same names. Key pairs are created with `BlsPair.fromSeed(seed)` | | `CoinsetClient` and `FullNodeClient` | One `RpcClient` class: `new RpcClient(url)`, `RpcClient.mainnet()`, `RpcClient.testnet11()`, `RpcClient.local(cert, key)` | @@ -56,10 +67,30 @@ Types are converted at the boundary: | `async fn` | Returns a `Promise` | Returns a `Promise` | Awaitable (use with `asyncio`) | | `snake_case` method names | `camelCase` | `camelCase` | `snake_case` | +The UniFFI bindings convert types like this: + +| Rust | Go | .NET | C++ | +| ------------------------- | ---------------------------- | ----------------------------------- | ------------------------------- | +| `Bytes32`, `Bytes` | `[]byte` | `byte[]` | `std::vector` | +| `u64` | `uint64` | `ulong` | `uint64_t` | +| `u128`, `BigInt` | Decimal `string` | Decimal `string` | Decimal `std::string` | +| `Option` | Pointer (`nil` when absent) | Nullable | `std::optional`, or `nullptr` | +| `Result` | Returns `(T, error)` | Returns `T`, throws `ChiaException` | Returns `T`, throws `ChiaError` | +| `async fn` | Blocks the calling goroutine | Returns a `Task` | Blocks the calling thread | +| `snake_case` method names | `PascalCase` | `PascalCase` | `snake_case` | + Structs such as `Coin`, `CoinSpend`, `SpendBundle` and `CatInfo` become classes with a constructor and properties for their fields. Passing an object to a method doesn't consume it, so you can keep using it afterwards. +In the UniFFI bindings, fields are read and written through methods (`coin.Amount()` and +`coin.SetAmount(...)` in Go and .NET, `coin->amount()` in C++), and setters change the object in place. +Use `Clone()` to copy an object. Static methods are free functions prefixed with the class name, such as +`ConstantsCatPuzzleHash()` in Go, `Sdk.ConstantsCatPuzzleHash()` in .NET and +`chiawalletsdk::constants_cat_puzzle_hash()` in C++. `clvm.Alloc` takes a `ClvmType` value, which covers +nil, booleans, integers, atoms, strings and the binding classes, but not lists; use `clvm.List(...)` for +those. + ## Node.js ```ts @@ -278,9 +309,107 @@ The Python package has the same networking classes as Node.js: `RpcClient`, `Pee `Certificate` and `FullNodeSimulator.start_server`. The binding packages are built with every feature enabled, so partial offers, auctions, the splitter, -NFT fusion, Secure the Bag and vault signed message verification are all available, with the same names +NFT fusion, Secure the Bag, plot NFTs and vault signed message verification are all available, with the same names as the Rust types (for example `PartialOffer`, `Auction`, `Fusion` and `SecureTheBag`). +## Go + +```go +package main + +import ( + "fmt" + + sdk "github.com/xch-dev/chia-wallet-sdk/go/chiawalletsdk" +) + +func main() { + sim, _ := sdk.NewSimulator() + clvm, _ := sdk.NewClvm() + alice, _ := sim.Bls(1000) + + createCoin, _ := clvm.CreateCoin(alice.PuzzleHash(), 900, nil) + reserveFee, _ := clvm.ReserveFee(100) + spend, _ := clvm.DelegatedSpend([]*sdk.Program{createCoin, reserveFee}) + + if err := clvm.SpendStandardCoin(alice.Coin(), alice.Pk(), spend); err != nil { + panic(err) + } + + coinSpends, _ := clvm.CoinSpends() + + if err := sim.SpendCoins(coinSpends, []*sdk.SecretKey{alice.Sk()}); err != nil { + panic(err) + } + + coins, _ := sim.UnspentCoins(alice.PuzzleHash(), false) + fmt.Println("Created a coin with", coins[0].Amount(), "mojos") +} +``` + +Objects are backed by Rust memory, which a finalizer frees when they're garbage collected. Call +`Destroy()` to free one sooner; using an object after that panics. Objects are safe to share between +goroutines. Optional objects are pointers to pointers (such as `**Program`). + +cgo uses `gcc` by default, which doesn't target every platform. For Windows arm64, set +`CC=aarch64-w64-mingw32-clang` from LLVM MinGW. For Android arm64, use the Android NDK's clang for API level +21 or later, along with `GOOS=android GOARCH=arm64 CGO_ENABLED=1`. + +## .NET + +```csharp +using ChiaWalletSdk; + +using var sim = new Simulator(); +using var clvm = new Clvm(); +using var alice = sim.Bls(1000); + +var spend = clvm.DelegatedSpend([ + clvm.CreateCoin(alice.PuzzleHash(), 900, null), + clvm.ReserveFee(100), +]); + +clvm.SpendStandardCoin(alice.Coin(), alice.Pk(), spend); +sim.SpendCoins(clvm.CoinSpends(), [alice.Sk()]); + +var coins = sim.UnspentCoins(alice.PuzzleHash(), false); +Console.WriteLine($"Created a coin with {coins[0].Amount()} mojos"); +``` + +Objects implement `IDisposable`, so `using` frees them deterministically. Otherwise a finalizer frees +them. + +## C++ + +```cpp +#include +#include + +namespace sdk = chiawalletsdk; + +int main() { + auto sim = sdk::Simulator::init(); + auto clvm = sdk::Clvm::init(); + auto alice = sim->bls(1000); + + auto spend = clvm->delegated_spend( + {clvm->create_coin(alice->puzzle_hash(), 900, nullptr), clvm->reserve_fee(100)}); + clvm->spend_standard_coin(alice->coin(), alice->pk(), spend); + sim->spend_coins(clvm->coin_spends(), {alice->sk()}); + + auto coins = sim->unspent_coins(alice->puzzle_hash(), false); + std::cout << "Created a coin with " << coins[0]->amount() << " mojos" << std::endl; +} +``` + +Objects are `std::shared_ptr`s, and constructors are named `init`. Compile the generated +`chiawalletsdk.cpp` with C++20 and link it against the Rust library. The C++ generator doesn't support +async functions, so the library is built with the `blocking` feature, which turns networking calls into +blocking calls. Methods whose names are C++ keywords get a trailing underscore, such as `clvm->int_(...)`. + +The Go, .NET and C++ bindings have the same networking classes as Node.js: `RpcClient`, `Peer`, +`Connector`, `Certificate` and `FullNodeSimulator` with `StartServer`. + ## What isn't in the bindings The bindings cover building, signing, parsing and validating spends for every primitive, offers, the @@ -298,4 +427,5 @@ If you need one of these from another language, call it from Rust and expose it issue on the [Wallet SDK repository](https://github.com/xch-dev/chia-wallet-sdk). For the complete API, read the type definitions that ship with each package: `index.d.ts` (Node.js), -`chia_wallet_sdk_wasm.d.ts` (WASM) and `chia_wallet_sdk.pyi` (Python). +`chia_wallet_sdk_wasm.d.ts` (WASM), `chia_wallet_sdk.pyi` (Python), the generated `chiawalletsdk` +package (Go), the `ChiaWalletSdk` assembly (.NET) and `chiawalletsdk.hpp` (C++). diff --git a/docs/sdk/clear-signing.md b/docs/sdk/clear-signing.md index 7fac3ca..ee19b19 100644 --- a/docs/sdk/clear-signing.md +++ b/docs/sdk/clear-signing.md @@ -45,7 +45,10 @@ and [linked offers](#linked-offers). before anything asserts its spend is visited again once a later spend asserts it, so the result doesn't depend on the order the spends are traversed in. 5. Settlement payments that the transaction asserts are matched against revealed requested payments, - which is how offers are summarized. + which is how offers are summarized. A requested NFT payment is matched for each owner its settlement + spend was revealed with, since the settlement coin's puzzle hash commits to the NFT's current owner. + An asserted payment of an option contract can't be reported, so it fails with + `UnsupportedAssertedOptionPayment`. A revealed spend that can't be parsed is ignored if nothing links it to the vault. If the spend receives a vault message or the transaction asserts it, the parse error is returned. If the delegated spend @@ -518,9 +521,12 @@ fn clear_sign_secure_the_bag() -> anyhow::Result<()> { A verified CAT spend that runs its TAIL is reported in `issuances` as an `Issuance`, with the coin id, asset id, `extra_delta` and `IssuanceKind`. The `extra_delta` comes from the spend's solution, which the -vault's message doesn't cover, so `Issuance::verified` is true only if the vault also sends a message that authorizes the extra delta (for a TAIL of kind `EverythingWithSingleton`). Only -verified extra deltas count toward the issued and melted amounts in `asset_flows`. An unverified one could -be different when the transaction is included in a block. +vault's message doesn't cover, so `Issuance::verified` is true only if the vault also sends a message +that authorizes the extra delta to a CAT whose TAIL is an `EverythingWithSingleton` TAIL of that same +vault (for a [controlled vault](#vaults-controlled-by-other-vaults), the controlled vault that sends the +message). A message to a CAT whose TAIL belongs to another singleton doesn't match anything, so it fails +with `UnmatchedVaultMessage`. Only verified extra deltas count toward the issued and melted amounts in +`asset_flows`. An unverified one could be different when the transaction is included in a block. ## Signed messages @@ -620,5 +626,6 @@ Parsing is conservative, so many unusual but valid transactions are rejected. Co | `WrongControlledVaultMessage` | The controller's message doesn't match the controlled vault's delegated puzzle hash | | `VaultControllerChangesVault` | A vault in the authorization path doesn't recreate itself unchanged | | `DuplicateControlledVault` / `ControlledVaultTooDeep` | A controlled vault appears twice, or the path is longer than 8 vaults | +| `UnsupportedAssertedOptionPayment` | The transaction asserts a requested payment of an option contract | All of these are variants of `DriverError`. diff --git a/docs/sdk/concepts/layers.md b/docs/sdk/concepts/layers.md index 9dddd3e..d55a210 100644 --- a/docs/sdk/concepts/layers.md +++ b/docs/sdk/concepts/layers.md @@ -365,32 +365,35 @@ If you only need to curry a puzzle and don't need parsing, `ctx.curry(args)` and ## Built-in layers -| Layer | Puzzle | Typically wraps / used by | -| -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `StandardLayer` (alias `P2DelegatedOrHiddenLayer`) | `p2_delegated_puzzle_or_hidden_puzzle`, the standard XCH puzzle | Ownership of most coins; [Standard](/sdk/primitives/standard) | -| `P2DelegatedConditionsLayer` | `p2_delegated_conditions`: a key signs a list of conditions | Simple key ownership | -| `CatLayer` | CAT v2 | [CAT](/sdk/primitives/cat) | -| `RevocationLayer` | Revocation layer for revocable CATs | [CAT](/sdk/primitives/cat) | -| `SingletonLayer` | Singleton top layer v1.1 | [Singletons](/sdk/primitives/singletons), NFTs, DIDs, vaults | -| `NftStateLayer` | NFT state layer (metadata and updater) | [NFT](/sdk/primitives/nft) | -| `NftOwnershipLayer` | NFT ownership layer (current DID owner and transfer program) | [NFT](/sdk/primitives/nft) | -| `RoyaltyTransferLayer` | NFT royalty transfer program | [NFT](/sdk/primitives/nft) | -| `DidLayer` | DID inner puzzle (metadata and recovery) | [DID](/sdk/primitives/did) | -| `SettlementLayer` | `settlement_payments`, used by offers | [Offers](/sdk/offers) | -| `OptionContractLayer` | Option contract | [Option Contracts](/sdk/primitives/options) | -| `P2SingletonLayer` | Coin spendable only alongside a singleton | [Singletons](/sdk/primitives/singletons), [Clear Signing](/sdk/clear-signing) | -| `P2OneOfManyLayer` | Pick one of many delegated puzzles from a merkle root | [Clawback](/sdk/primitives/clawback), [Option Contracts](/sdk/primitives/options), [MerkleTree](/sdk/utilities#merkle-trees) | -| `P2CurriedLayer` | Spend by revealing a puzzle that hashes to a curried puzzle hash | [Clawback](/sdk/primitives/clawback) | -| `AugmentedConditionLayer` | Adds a fixed condition to the inner puzzle's output | [Clawback](/sdk/primitives/clawback), [Option Contracts](/sdk/primitives/options) | -| `StreamLayer` | Streamed payments | [Streamed Assets](/sdk/primitives/streaming) | -| `BulletinLayer` | Nonce wrapper that distinguishes bulletin coins | [Bulletins](/sdk/primitives/bulletin) | -| `DelegatedPuzzleFeederLayer`, `IndexWrapperLayer`, `SingletonMemberLayer` | Meta Inner Puzzle Spec (MIPS) building blocks | [Vaults & MIPS](/sdk/primitives/vaults) | -| `DelegationLayer`, `WriterLayer`, `OracleLayer` | DataLayer delegation (feature `chip-0035`) | [DataLayer](/sdk/primitives/datalayer) | -| `P2ControllerPuzzleLayer`, `P2Eip712MessageLayer` | Coins controlled by another puzzle or by an EIP-712 signature (feature `chip-0037`) | Ethereum-style (secp256k1) wallets | -| `ActionLayer`, `PrecommitLayer`, `StateSchedulerLayer`, `MOfNLayer`, `P2DelegatedBySingletonLayer`, `ConditionsLayer`, `P2ParentLayer` | Action layer singletons (feature `action-layer`) | [Action Layer](/sdk/primitives/action-layer) | -| `SplitterLayer` | CHIP-0008 splitter: divides a coin between fixed recipients (feature `chip-0008`) | [Splitter](/sdk/primitives/splitter) | -| `FusionLayer` | CHIP-0021 fusion singleton inner puzzle (feature `chip-0021`) | [NFT Fusion](/sdk/primitives/fusion) | -| `P2FusionLayer` | p2 puzzle of NFTs locked in a fusion (feature `chip-0021`) | [NFT Fusion](/sdk/primitives/fusion) | +| Layer | Puzzle | Typically wraps / used by | +| -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `StandardLayer` (alias `P2DelegatedOrHiddenLayer`) | `p2_delegated_puzzle_or_hidden_puzzle`, the standard XCH puzzle | Ownership of most coins; [Standard](/sdk/primitives/standard) | +| `P2DelegatedConditionsLayer` | `p2_delegated_conditions`: a key signs a list of conditions | Simple key ownership | +| `CatLayer` | CAT v2 | [CAT](/sdk/primitives/cat) | +| `RevocationLayer` | Revocation layer for revocable CATs | [CAT](/sdk/primitives/cat) | +| `SingletonLayer` | Singleton top layer v1.1 | [Singletons](/sdk/primitives/singletons), NFTs, DIDs, vaults | +| `NftStateLayer` | NFT state layer (metadata and updater) | [NFT](/sdk/primitives/nft) | +| `NftOwnershipLayer` | NFT ownership layer (current DID owner and transfer program) | [NFT](/sdk/primitives/nft) | +| `RoyaltyTransferLayer` | NFT royalty transfer program | [NFT](/sdk/primitives/nft) | +| `DidLayer` | DID inner puzzle (metadata and recovery) | [DID](/sdk/primitives/did) | +| `SettlementLayer` | `settlement_payments`, used by offers | [Offers](/sdk/offers) | +| `OptionContractLayer` | Option contract | [Option Contracts](/sdk/primitives/options) | +| `P2SingletonLayer` | Coin spendable only alongside a singleton | [Singletons](/sdk/primitives/singletons), [Clear Signing](/sdk/clear-signing) | +| `P2OneOfManyLayer` | Pick one of many delegated puzzles from a merkle root | [Clawback](/sdk/primitives/clawback), [Option Contracts](/sdk/primitives/options), [MerkleTree](/sdk/utilities#merkle-trees) | +| `P2CurriedLayer` | Spend by revealing a puzzle that hashes to a curried puzzle hash | [Clawback](/sdk/primitives/clawback) | +| `AugmentedConditionLayer` | Adds a fixed condition to the inner puzzle's output | [Clawback](/sdk/primitives/clawback), [Option Contracts](/sdk/primitives/options) | +| `StreamLayer` | Streamed payments | [Streamed Assets](/sdk/primitives/streaming) | +| `BulletinLayer` | Nonce wrapper that distinguishes bulletin coins | [Bulletins](/sdk/primitives/bulletin) | +| `DelegatedPuzzleFeederLayer`, `IndexWrapperLayer`, `SingletonMemberLayer` | Meta Inner Puzzle Spec (MIPS) building blocks | [Vaults & MIPS](/sdk/primitives/vaults) | +| `DelegationLayer`, `WriterLayer`, `OracleLayer` | DataLayer delegation (feature `chip-0035`) | [DataLayer](/sdk/primitives/datalayer) | +| `P2ControllerPuzzleLayer`, `P2Eip712MessageLayer` | Coins controlled by another puzzle or by an EIP-712 signature (feature `chip-0037`) | Ethereum-style (secp256k1) wallets | +| `ActionLayer`, `PrecommitLayer`, `StateSchedulerLayer`, `MOfNLayer`, `P2DelegatedBySingletonLayer`, `ConditionsLayer`, `P2ParentLayer` | Action layer singletons (feature `action-layer`) | [Action Layer](/sdk/primitives/action-layer) | +| `SplitterLayer` | CHIP-0008 splitter: divides a coin between fixed recipients (feature `chip-0008`) | [Splitter](/sdk/primitives/splitter) | +| `FusionLayer` | CHIP-0021 fusion singleton inner puzzle (feature `chip-0021`) | [NFT Fusion](/sdk/primitives/fusion) | +| `P2FusionLayer` | p2 puzzle of NFTs locked in a fusion (feature `chip-0021`) | [NFT Fusion](/sdk/primitives/fusion) | +| `SingletonV1Layer` | Singleton top layer v1.0, used only by plot NFTs (feature `pooling`) | [Plot NFTs](/sdk/primitives/plot-nft) | +| `PoolMemberLayer`, `PoolWaitingRoomLayer` | Plot NFT inner puzzles for farming to a pool, and for self pooling or leaving a pool (feature `pooling`) | [Plot NFTs](/sdk/primitives/plot-nft) | +| `P2SingletonOrDelayedLayer` | The p2 puzzle that plot NFT rewards are farmed to (feature `pooling`) | [Plot NFTs](/sdk/primitives/plot-nft) | Besides these, any type that implements both `ToClvm` and `FromClvm` is a layer, whose puzzle is the value itself and whose solution is a `NodePtr`. `Puzzle` is a layer too, as shown above. Both are useful as placeholder inner layers in tests and when parsing. diff --git a/docs/sdk/index.md b/docs/sdk/index.md index 4174590..7421859 100644 --- a/docs/sdk/index.md +++ b/docs/sdk/index.md @@ -34,7 +34,7 @@ Networking needs a TLS backend, which you choose with a feature flag: ```toml [dependencies] -chia-wallet-sdk = { version = "0.37", features = ["rustls"] } +chia-wallet-sdk = { version = "0.38", features = ["rustls"] } ``` ### Feature flags @@ -48,6 +48,7 @@ chia-wallet-sdk = { version = "0.37", features = ["rustls"] } | `chip-0008` | The [splitter](/sdk/primitives/splitter) puzzle (CHIP-0008) | | `chip-0021` | [NFT fusion](/sdk/primitives/fusion) (CHIP-0021) | | `chip-0052` | [Partial offers](/sdk/primitives/partial-offers) (CHIP-0052) | +| `pooling` | [Plot NFTs](/sdk/primitives/plot-nft) for pooling | | `action-layer` | [Action layer singletons](/sdk/primitives/action-layer) such as CATalog and XCHandles (implies `chip-0035`) | | `auction` | [Auctions](/sdk/primitives/auctions) (implies `action-layer`) | | `offer-compression` | Compressing and decompressing [offer](/sdk/offers) strings | diff --git a/docs/sdk/networking/peer.md b/docs/sdk/networking/peer.md index 1023ecd..8b7479a 100644 --- a/docs/sdk/networking/peer.md +++ b/docs/sdk/networking/peer.md @@ -23,7 +23,7 @@ not compiled. `Peer`, `PeerOptions`, `Network` and the rate limiter types are al ```toml [dependencies] -chia-wallet-sdk = { version = "0.37", features = ["rustls"] } +chia-wallet-sdk = { version = "0.38", features = ["rustls"] } tokio = { version = "1", features = ["full"] } ``` diff --git a/docs/sdk/primitives/action-layer.md b/docs/sdk/primitives/action-layer.md index ee5981b..5bd85cd 100644 --- a/docs/sdk/primitives/action-layer.md +++ b/docs/sdk/primitives/action-layer.md @@ -416,3 +416,8 @@ fn p2_parent_coin() -> anyhow::Result<()> { - Keep your slot index up to date. Using a slot that was already spent (or the wrong neighbors) makes the spend invalid. - Precommit coins can't be used until `relative_block_height` blocks after they're created. +- When you sync a registry from the chain, `ActionLayer::parse_solution` returns + `DriverError::InvalidActionLayerSolution` if a selector doesn't point at one of the revealed action + puzzles or has no solution to go with it. Extra solutions after the last selector are allowed, since + the puzzle ignores them. `ActionLayer::construct_solution` returns the same error if the number of + merkle proofs doesn't match the number of action spends. diff --git a/docs/sdk/primitives/index.md b/docs/sdk/primitives/index.md index 393a092..aa2fca2 100644 --- a/docs/sdk/primitives/index.md +++ b/docs/sdk/primitives/index.md @@ -238,6 +238,7 @@ In practice you rarely build proofs by hand: | [Auctions](/sdk/primitives/auctions) | `Auction`, `AuctionInfo`, `AuctionSettings` | `auction` | | [Splitter](/sdk/primitives/splitter) | `SplitterLayer`, `SplitterArgs`, `SplitterShare` | `chip-0008` | | [NFT Fusion](/sdk/primitives/fusion) | `Fusion`, `FusionSwap`, `P2FusionLayer` | `chip-0021` | +| [Plot NFTs](/sdk/primitives/plot-nft) | `PlotNft`, `PlotNftInfo`, `PoolState` | `pooling` | | [Secure the Bag](/sdk/primitives/secure-the-bag) | `SecureTheBag`, `BagFormat`, `BagPayment`, `SecuredMint` | | All primitive types live in `chia_wallet_sdk::driver`, and the most common ones are re-exported from diff --git a/docs/sdk/primitives/plot-nft.md b/docs/sdk/primitives/plot-nft.md new file mode 100644 index 0000000..d49671b --- /dev/null +++ b/docs/sdk/primitives/plot-nft.md @@ -0,0 +1,447 @@ +--- +title: Plot NFTs +--- + +# Plot NFTs + +A plot NFT (called a pool wallet in the reference wallet) is a [singleton](/sdk/primitives/singletons) +that lets a farmer change where their pool rewards go without replotting. Plots are created for the plot +NFT's **p2 puzzle hash** (the "pool contract address") instead of for a pool, so the 7/8 pool share of +every block reward they win is locked to a puzzle that only the plot NFT can claim from. The plot NFT's +state decides where claimed rewards are paid: to the owner when self pooling, or to a pool. The farmer +can move between pools by spending the plot NFT, and the plots keep working. + +The types are behind the `pooling` feature flag, and live in +[`chia_wallet_sdk::driver`](https://docs.rs/chia-wallet-sdk/latest/chia_wallet_sdk/driver/index.html) +(they aren't in the prelude). The curried arguments and solutions of the puzzles, and helpers such as +`pool_reward_parent_id`, are in `chia_wallet_sdk::types::puzzles`. Background on the puzzles is at +[Pooling on chialisp.com](https://chialisp.com/pooling). + +## How it works + +Plot NFTs use the original singleton top layer (v1.0), not the v1.1 layer that every other singleton in +the SDK uses, so they have their own `SingletonV1Layer`. Inside it is one of two inner puzzles, depending +on the state: + +| State | Inner puzzle | Rewards are paid to | Moving out | +| --------------- | ------------ | ------------------------ | ------------------------------------------------------------------- | +| `SelfPooling` | Waiting room | The owner's target | The owner signs, once the coin is confirmed (a lock height of 0) | +| `FarmingToPool` | Pool member | The pool's target | The owner signs to move to the waiting room, which is `LeavingPool` | +| `LeavingPool` | Waiting room | The pool's target, still | The owner signs, once the pool's relative lock height has passed | + +The **target puzzle hash** is where rewards are paid when they're claimed. It's not the p2 puzzle hash, +which is what plots farm to and never changes. A pool can't be left immediately: the farmer first moves to +the waiting room, and only after the pool's **relative lock height** (a number of blocks, between 5 and +1000 for the reference wallet) can they move to self pooling or another pool. Rewards farmed in the +meantime still go to the pool. Self pooling uses the same waiting room puzzle, with the owner's own +target and a lock of 0. + +Reward coins are locked by the `P2SingletonOrDelayedLayer` puzzle, which has two paths: + +- **Claim**: the plot NFT absorbs a pool reward and pays it to the current target. Anyone can do this, + without a signature. A coin is only a pool reward if its parent coin id is the one consensus gives + pool rewards, which is the first half of the genesis challenge followed by the block height. +- **Delayed**: any coin, including one that isn't a pool reward, can be sent to the **delayed puzzle + hash** after it has existed for `seconds_delay`. This is the only way to recover XCH that someone sends + to the pool contract address directly. + +| Type | What it is | +| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PlotNft` | `Singleton`. Has `parse_launcher`, `parse_child`, `join_pool`, `leave_pool`, `self_pool` and `claim_rewards`. | +| `PlotNftInfo` | The launcher id, the `PoolState`, `seconds_delay`, `delayed_puzzle_hash` and the network's `pool_reward_prefix`. Has `p2_puzzle_hash` and `claim_delayed`. | +| `PoolState` | The state as it's stored on-chain: version, `PoolSingletonState`, target puzzle hash, owner public key, pool URL and relative lock height. | +| `PoolSingletonState` | `SelfPooling`, `LeavingPool` or `FarmingToPool`. | +| `SingletonV1Layer`, `PoolMemberLayer`, `PoolWaitingRoomLayer`, `P2SingletonOrDelayedLayer` | The [layers](/sdk/concepts/layers) for the four puzzles, if you need to build or parse the spends yourself. | + +The **owner public key** in the state signs every state change. It's a plain BLS key that signs directly, +not a synthetic key like the [standard puzzle](/sdk/primitives/standard) uses. + +## Launching + +`Launcher::mint_plot_nft` launches a plot NFT in the self pooling or farming to pool state. It takes the +initial `PoolState` (built with `PoolState::self_pooling` or `PoolState::farming_to_pool`), the delay +settings and the network's genesis challenge, and returns the conditions for the launcher's parent to +output along with the eve `PlotNft`. The reference wallet launches with an amount of 1 and a delay of +`DEFAULT_P2_SINGLETON_DELAY` (one week, in seconds), and the delayed puzzle hash is usually the owner's +wallet address. + +The p2 puzzle hash depends on the launcher id and the delay settings, so they can't be changed after the +launch. `plot_nft.info.p2_puzzle_hash()` is the pool contract address to create plots with. + +```rust +use chia_wallet_sdk::driver::{DEFAULT_P2_SINGLETON_DELAY, PlotNft, PoolSingletonState, PoolState}; +use chia_wallet_sdk::prelude::*; + +#[test] +fn launch_plot_nft() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let mut ctx = SpendContext::new(); + + let alice = sim.bls(1); + let owner = BlsPair::new(42); + let genesis_challenge = TESTNET11_CONSTANTS.genesis_challenge; + + // Self pool, paying rewards to Alice. Coins that aren't rewards go to her after a week. + let (conditions, plot_nft) = Launcher::new(alice.coin.coin_id(), 1).mint_plot_nft( + &mut ctx, + PoolState::self_pooling(alice.puzzle_hash, owner.pk), + DEFAULT_P2_SINGLETON_DELAY, + alice.puzzle_hash, + genesis_challenge, + )?; + StandardLayer::new(alice.pk).spend(&mut ctx, alice.coin, conditions)?; + sim.spend_coins(ctx.take(), &[alice.sk])?; + + // The address to create plots for + let pool_contract_puzzle_hash: Bytes32 = plot_nft.info.p2_puzzle_hash().into(); + assert_ne!(pool_contract_puzzle_hash, plot_nft.coin.puzzle_hash); + + // The launcher spend is enough to get the same plot NFT back + let launcher_spend = sim.coin_spend(plot_nft.info.launcher_id).expect("missing launcher"); + let solution = ctx.alloc(&launcher_spend.solution)?; + let parsed = PlotNft::parse_launcher(&ctx, launcher_spend.coin, solution, genesis_challenge)? + .expect("not a plot NFT"); + + assert_eq!(parsed, plot_nft); + assert_eq!(parsed.info.pool_state.state, PoolSingletonState::SelfPooling); + assert_eq!(parsed.info.seconds_delay, DEFAULT_P2_SINGLETON_DELAY); + + Ok(()) +} +``` + +`mint_plot_nft` returns `DriverError::InvalidPoolState` for a state the reference wallet wouldn't create +(see [validation](#validation)), for the `LeavingPool` state, and for an even amount. + +## Syncing + +Plot NFTs can't be parsed from a single parent spend like an NFT or DID can. The state is only revealed +by the spends that change it, and spends that only claim rewards don't reveal anything, so the state of +a coin depends on every spend before it. The delay settings are only revealed by the launcher spend. A +wallet has to start at the launcher and walk forward: + +1. Look up the launcher's coin spend, and parse it with `PlotNft::parse_launcher`. It returns `None` if + the launcher isn't for a plot NFT on the network with that genesis challenge. +2. While the current coin is spent, look up its solution and call `plot_nft.parse_child(&allocator, +solution)`, which returns the next coin in the lineage with its state. + +Unlike other `parse_child` functions, this one is a method on the parent `PlotNft`, and it only needs the +solution, since the puzzle is already known. Save the `PlotNftInfo` (or the whole `PlotNft`) once you +have it, so you don't have to walk the lineage again, and so that the delay settings aren't lost. Without them you can't compute the plot NFT's puzzle hash, so you +can't spend it or its rewards. + +`parse_child` checks the revealed state against the puzzle hash the singleton moved to, since the +puzzle doesn't enforce that they match. If the owner leaves the waiting room for a puzzle that doesn't +match the state they revealed, it returns `None`, and the SDK can't follow the plot NFT any further. +Leaving a pool can only go to one puzzle, so if a leave spend reveals a state that doesn't match it, the +child is still parsed, as the previous state with `LeavingPool`. + +With a [peer](/sdk/networking/peer), the solutions come from `request_puzzle_and_solution`, and the +launcher id comes from wherever you found the plot NFT (for example a launcher coin created by one of your +own coins). The example below uses the `Simulator`. + +## Joining and leaving pools + +Three methods change the state. Each spends the plot NFT, adds the spend to the context, and returns the +child, and each needs a signature from the owner key: + +- `join_pool(ctx, target_puzzle_hash, pool_url, relative_lock_height)` moves from the waiting room + (self pooling, or leaving a pool once the lock has passed) to farming to a pool. The target puzzle + hash and relative lock height come from the pool, which publishes them on its `/pool_info` endpoint. +- `leave_pool(ctx)` moves from farming to a pool to the waiting room, keeping the pool's target and lock + height. +- `self_pool(ctx, target_puzzle_hash)` moves from the waiting room to self pooling. It also changes the + target of a plot NFT that's already self pooling. + +They return `DriverError::InvalidPlotNftTransition` for a move the SDK doesn't support, such as leaving a +pool it isn't farming to, joining a pool without leaving the current one first, or moving to the state +the plot NFT is already in. + +The relative lock height is counted from when the current coin was created, so the chain rejects a move +out of the waiting room until enough blocks have passed (`ASSERT_HEIGHT_RELATIVE`). That includes a lock +of 0 on a coin created in the same block, which is why rewards have to be claimed in an earlier block +than the one that joins a pool (see [gotchas](#gotchas)). + +This example goes through the whole lifecycle with fake rewards, then syncs the plot NFT from its +launcher: + +```rust +use chia_wallet_sdk::driver::{PlotNft, PoolSingletonState, PoolState}; +use chia_wallet_sdk::prelude::*; +use chia_wallet_sdk::types::puzzles::pool_reward_parent_id; + +const REWARD: u64 = 1_750_000_000_000; + +/// Follows a plot NFT from its launcher to its latest coin. +fn sync( + sim: &Simulator, + ctx: &mut SpendContext, + launcher_id: Bytes32, + genesis_challenge: Bytes32, +) -> anyhow::Result> { + let launcher_spend = sim.coin_spend(launcher_id).expect("missing launcher"); + let solution = ctx.alloc(&launcher_spend.solution)?; + + let Some(mut plot_nft) = + PlotNft::parse_launcher(ctx, launcher_spend.coin, solution, genesis_challenge)? + else { + return Ok(None); + }; + + while let Some(solution) = sim.solution(plot_nft.coin.coin_id()) { + let solution = ctx.alloc(&solution)?; + let Some(child) = plot_nft.parse_child(ctx, solution)? else { + return Ok(None); + }; + plot_nft = child; + } + + Ok(Some(plot_nft)) +} + +/// Creates a pool reward coin, as if the plot NFT's plots won the block at `height`. +fn farm_reward(sim: &mut Simulator, plot_nft: &PlotNft, height: u32) -> Coin { + let reward = Coin::new( + pool_reward_parent_id(plot_nft.info.pool_reward_prefix, height), + plot_nft.info.p2_puzzle_hash().into(), + REWARD, + ); + sim.insert_coin(reward); + reward +} + +#[test] +fn pool_lifecycle() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let mut ctx = SpendContext::new(); + + let alice = sim.bls(1); + let owner = BlsPair::new(42); + let pool_puzzle_hash = Bytes32::new([1; 32]); + let genesis_challenge = TESTNET11_CONSTANTS.genesis_challenge; + + let (conditions, plot_nft) = Launcher::new(alice.coin.coin_id(), 1).mint_plot_nft( + &mut ctx, + PoolState::self_pooling(alice.puzzle_hash, owner.pk), + 604_800, + alice.puzzle_hash, + genesis_challenge, + )?; + StandardLayer::new(alice.pk).spend(&mut ctx, alice.coin, conditions)?; + sim.spend_coins(ctx.take(), &[alice.sk])?; + + // While self pooling, rewards are paid to Alice + let reward = farm_reward(&mut sim, &plot_nft, 100); + let (_, plot_nft) = plot_nft.claim_rewards(&mut ctx, &[reward])?; + sim.spend_coins(ctx.take(), &[])?; + assert!(sim.unspent_coins(alice.puzzle_hash, false).iter().any(|coin| coin.amount == REWARD)); + + // Join a pool with a relative lock height of 10 blocks + let plot_nft = plot_nft.join_pool( + &mut ctx, + pool_puzzle_hash, + "https://pool.example.com".to_string(), + 10, + )?; + sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk))?; + assert_eq!(plot_nft.info.pool_state.state, PoolSingletonState::FarmingToPool); + + // Rewards now go to the pool + let reward = farm_reward(&mut sim, &plot_nft, 101); + let (_, plot_nft) = plot_nft.claim_rewards(&mut ctx, &[reward])?; + sim.spend_coins(ctx.take(), &[])?; + assert_eq!(sim.unspent_coins(pool_puzzle_hash, false).len(), 1); + + // Leaving moves to the waiting room, which can't be left until the lock has passed + let plot_nft = plot_nft.leave_pool(&mut ctx)?; + sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk))?; + assert_eq!(plot_nft.info.pool_state.state, PoolSingletonState::LeavingPool); + + let _ = plot_nft.self_pool(&mut ctx, alice.puzzle_hash)?; + assert!(sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk)).is_err()); + + for _ in 0..10 { + sim.create_block(); + } + + let plot_nft = plot_nft.self_pool(&mut ctx, alice.puzzle_hash)?; + sim.spend_coins(ctx.take(), &[owner.sk])?; + assert_eq!(plot_nft.info.pool_state, PoolState::self_pooling(alice.puzzle_hash, owner.pk)); + + // A wallet that only knows the launcher id ends up at the same coin and state + let synced = sync(&sim, &mut ctx, plot_nft.info.launcher_id, genesis_challenge)?; + assert_eq!(synced, Some(plot_nft)); + + Ok(()) +} +``` + +## Claiming rewards + +`claim_rewards(ctx, rewards)` claims any number of pool rewards in one transaction. The plot NFT is +recreated once for each reward, in order, and the method returns the last child. Each reward is paid to +the target puzzle hash of the current state, whichever state the plot NFT was in when it was farmed. +Nothing is signed, so anyone can claim a plot NFT's rewards, and the pool usually does it for plot NFTs +that are farming to it. + +To find the rewards, look up the unspent coins of `plot_nft.info.p2_puzzle_hash()`. The SDK checks every +coin before it adds any spends, and returns `DriverError::InvalidPoolReward` if the list is empty, or if a coin isn't locked to the +p2 puzzle hash, isn't a pool reward, is listed twice, or can't be claimed by the puzzle (see +[gotchas](#gotchas)). + +The plot NFT and reward spends don't pay a fee. To pay one, add the returned conditions to the spend +that pays it. They assert an announcement from the first reward, so the fee can't be taken without the +claim: + +```rust +use chia_wallet_sdk::driver::PoolState; +use chia_wallet_sdk::prelude::*; +use chia_wallet_sdk::types::puzzles::pool_reward_parent_id; + +#[test] +fn claim_rewards_with_fee() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let mut ctx = SpendContext::new(); + + let alice = sim.bls(1); + let owner = BlsPair::new(42); + let pool_puzzle_hash = Bytes32::new([1; 32]); + + let (conditions, plot_nft) = Launcher::new(alice.coin.coin_id(), 1).mint_plot_nft( + &mut ctx, + PoolState::farming_to_pool( + pool_puzzle_hash, + owner.pk, + "https://pool.example.com".to_string(), + 32, + ), + 604_800, + alice.puzzle_hash, + TESTNET11_CONSTANTS.genesis_challenge, + )?; + StandardLayer::new(alice.pk).spend(&mut ctx, alice.coin, conditions)?; + sim.spend_coins(ctx.take(), &[alice.sk])?; + + // Three blocks were farmed by this plot NFT's plots + let rewards: Vec = [200, 201, 202] + .into_iter() + .map(|height| { + let reward = Coin::new( + pool_reward_parent_id(plot_nft.info.pool_reward_prefix, height), + plot_nft.info.p2_puzzle_hash().into(), + 1_750_000_000_000, + ); + sim.insert_coin(reward); + reward + }) + .collect(); + + let (conditions, plot_nft) = plot_nft.claim_rewards(&mut ctx, &rewards)?; + + // Bob pays the fee, which only works if the rewards are claimed in the same transaction + let bob = sim.bls(100); + StandardLayer::new(bob.pk).spend(&mut ctx, bob.coin, conditions.reserve_fee(100))?; + sim.spend_coins(ctx.take(), &[bob.sk])?; + + assert_eq!(sim.unspent_coins(pool_puzzle_hash, false).len(), 3); + assert!(sim.coin_state(plot_nft.coin.coin_id()).is_some()); + + Ok(()) +} +``` + +`join_pool`, `leave_pool` and `self_pool` don't return any conditions. To pay a fee for a state change, +spend another coin in the same spend bundle that reserves the fee and asserts the spend of the plot NFT +coin you called the method on (not the returned child) with +`assert_concurrent_spend(plot_nft.coin.coin_id())`, so the fee can't be taken without the state change. + +## Claiming other coins after the delay + +A coin sent to the p2 puzzle hash that isn't a pool reward, such as a payment someone sent to the pool +contract address by mistake, can't be claimed by the plot NFT. `plot_nft.info.claim_delayed(ctx, coin)` +sends it to the delayed puzzle hash instead, once it has existed for `seconds_delay` seconds +(`ASSERT_SECONDS_RELATIVE`), and returns the new coin. It doesn't spend the plot NFT, and anyone can do +it. + +```rust +use chia_wallet_sdk::driver::{DEFAULT_P2_SINGLETON_DELAY, PoolState}; +use chia_wallet_sdk::prelude::*; + +#[test] +fn claim_delayed() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let mut ctx = SpendContext::new(); + + let alice = sim.bls(1); + let owner = BlsPair::new(42); + + let (conditions, plot_nft) = Launcher::new(alice.coin.coin_id(), 1).mint_plot_nft( + &mut ctx, + PoolState::self_pooling(alice.puzzle_hash, owner.pk), + DEFAULT_P2_SINGLETON_DELAY, + alice.puzzle_hash, + TESTNET11_CONSTANTS.genesis_challenge, + )?; + StandardLayer::new(alice.pk).spend(&mut ctx, alice.coin, conditions)?; + sim.spend_coins(ctx.take(), &[alice.sk])?; + + // Someone sends XCH to the pool contract address + let coin = sim.new_coin(plot_nft.info.p2_puzzle_hash().into(), 1_000); + let created_timestamp = sim.next_timestamp(); + sim.create_block(); + + // It isn't a pool reward, so the plot NFT can't claim it + assert!(matches!( + plot_nft.claim_rewards(&mut ctx, &[coin]), + Err(DriverError::InvalidPoolReward(_)) + )); + + // It can only be sent to the delayed puzzle hash, after the delay + let _ = plot_nft.info.claim_delayed(&mut ctx, coin)?; + assert!(sim.spend_coins(ctx.take(), &[]).is_err()); + + sim.set_next_timestamp(created_timestamp + DEFAULT_P2_SINGLETON_DELAY)?; + let swept = plot_nft.info.claim_delayed(&mut ctx, coin)?; + sim.spend_coins(ctx.take(), &[])?; + + assert_eq!(swept.puzzle_hash, alice.puzzle_hash); + assert!(sim.coin_state(swept.coin_id()).is_some()); + + Ok(()) +} +``` + +## Validation + +`PoolState::validate` applies the checks the reference wallet makes to a state it's about to move to: +the version isn't newer than `POOL_PROTOCOL_VERSION`, the owner key isn't the infinity point (which could +never sign), a self pooling state has no pool URL and a lock height of 0, and a pool state has a URL and a +lock height between `MINIMUM_RELATIVE_LOCK_HEIGHT` (5) and `MAXIMUM_RELATIVE_LOCK_HEIGHT` (1000). +`mint_plot_nft`, `join_pool` and `self_pool` call it for you, and return `DriverError::InvalidPoolState`. + +The puzzles don't enforce any of this, so a state read from the chain can be anything. Parsing never +validates, and you shouldn't validate states you sync either, or a plot NFT that someone else's software +moved to an unusual state would stop syncing. + +## Gotchas + +- **Claim self pooling rewards before joining a pool, in an earlier block.** Rewards are paid to the + target at the time of the claim, not when they were farmed, so unclaimed self pooling rewards go to the + pool once you've joined. The claim has to be confirmed first, since the join asserts a relative height, + which isn't allowed on a coin created in the same block (`EPHEMERAL_RELATIVE_CONDITION`). +- **Claiming restarts the lock.** The relative lock height is counted from the creation of the current + coin, and every claim recreates it. Since anyone can claim, a pool (or anyone else) can delay a plot + NFT leaving it by claiming rewards during the waiting period. The puzzle works this way, and the + reference wallet has the same behaviour. +- **Odd amounts can't be claimed.** The singleton raises if it creates a second odd coin, so + `claim_rewards` rejects a reward with an odd amount. Real pool rewards are always even. While farming to a + pool, it also rejects a reward from height 0, which the pool member puzzle would read as a leave spend. +- **Rewards keep their address across states.** The p2 puzzle hash only depends on the launcher id and + the delay settings, so rewards farmed in one state can be claimed after the plot NFT has moved to + another, and they're paid to the new target. +- **Save the delay settings.** They're only in the launcher spend. Without them, you can't compute the plot + NFT's puzzle hash, so you can't spend it or its rewards. +- **The pool URL isn't committed to.** Neither is the difference between `SelfPooling` and `LeavingPool`. + They're only in the revealed state, so trust them as much as you trust the owner. +- **Use the right genesis challenge.** The reward prefix is part of the puzzles, so a plot NFT is only + recognized by `parse_launcher` on the network it was launched for. diff --git a/docs/sdk/primitives/singletons.md b/docs/sdk/primitives/singletons.md index 999ac8d..73a37d1 100644 --- a/docs/sdk/primitives/singletons.md +++ b/docs/sdk/primitives/singletons.md @@ -5,16 +5,18 @@ title: Singletons & Launchers # Singletons & Launchers A singleton is a coin with a unique, provable identity that persists as it is spent and recreated. NFTs, -DIDs, vaults, option contracts, DataLayer stores and the action layer registries are all singletons. The -singleton top layer (`singleton_top_layer_v1_1`) enforces that each spend creates at most one odd-amount -child with the same identity, and that identity is the coin ID of the **launcher** coin that created it. +DIDs, vaults, option contracts, DataLayer stores, the action layer registries and plot NFTs are all +singletons. The singleton top layer (`singleton_top_layer_v1_1`, or v1.0 for plot NFTs) enforces that +each spend creates at most one odd-amount child with the same identity, and that identity is the coin ID +of the **launcher** coin that created it. This page covers the pieces every singleton shares: `Launcher` and `IntermediateLauncher` for creating them, the generic `Singleton` type with the `SingletonInfo` trait, proofs, and melting. The specific singletons have their own pages: [NFT](/sdk/primitives/nft), [DID](/sdk/primitives/did), [Option Contracts](/sdk/primitives/options), [Vaults](/sdk/primitives/vaults), -[DataLayer](/sdk/primitives/datalayer) and [Action Layer](/sdk/primitives/action-layer). Background on -the puzzle is at [Singletons on chialisp.com](https://chialisp.com/singletons). +[DataLayer](/sdk/primitives/datalayer), [Action Layer](/sdk/primitives/action-layer) and +[Plot NFTs](/sdk/primitives/plot-nft). Background on the puzzle is at +[Singletons on chialisp.com](https://chialisp.com/singletons). :::tip With the [Action System](/sdk/action-system), `Action::create_did`, `Action::mint_nft` and @@ -68,8 +70,15 @@ fn launch_singleton() -> anyhow::Result<()> { The primitive-specific launch methods are extension methods on `Launcher`: `Launcher::mint_nft` and `mint_eve_nft` for NFTs, `create_did`, `create_simple_did` and `create_eve_did` for DIDs, and similar -`mint_vault` for [vaults](/sdk/primitives/vaults), and `mint_datastore` for [DataLayer](/sdk/primitives/datalayer) stores (`chip-0035`). They take the `Launcher` by value, so each launcher can only be -used once. +`mint_vault` for [vaults](/sdk/primitives/vaults), `mint_datastore` for +[DataLayer](/sdk/primitives/datalayer) stores (`chip-0035`), and `mint_plot_nft` for +[plot NFTs](/sdk/primitives/plot-nft) (`pooling`). They take the `Launcher` by value, so each launcher +can only be used once. + +`Launcher::spend` takes the singleton's inner puzzle hash and wraps it in the v1.1 singleton layer. For a +singleton with a different top layer, calculate the full puzzle hash yourself and use +`Launcher::spend_to_puzzle_hash(ctx, singleton_puzzle_hash, key_value_list)` instead, which returns the +same parent conditions and eve coin. ### Launcher variants @@ -158,6 +167,10 @@ The methods on `Singleton` are generic: - `child_with(info, amount)` returns the child `Singleton` for a new info value. Construct the child from the new info, rather than copying the singleton and mutating `info`, so the coin's puzzle hash matches. +The child's puzzle hash comes from `info.puzzle_hash()`, which wraps the inner puzzle hash in the v1.1 +singleton layer by default. An info type for a singleton with a different top layer overrides +`puzzle_hash`, as `PlotNftInfo` does for the v1.0 layer, and the generic methods use it. + ### Proofs The singleton puzzle needs to know its parent was a legitimate singleton with the same launcher ID. diff --git a/docs/sdk/testing/full-node-simulator.md b/docs/sdk/testing/full-node-simulator.md index 49939f6..7d3ee8e 100644 --- a/docs/sdk/testing/full-node-simulator.md +++ b/docs/sdk/testing/full-node-simulator.md @@ -322,7 +322,7 @@ the feature turned on. ```toml [dev-dependencies] -chia-sdk-test = { version = "0.37", features = ["full-node-simulator-http"] } +chia-sdk-test = { version = "0.38", features = ["full-node-simulator-http"] } ``` ::: diff --git a/docs/sdk/testing/peer-simulator.md b/docs/sdk/testing/peer-simulator.md index fd41834..26edf54 100644 --- a/docs/sdk/testing/peer-simulator.md +++ b/docs/sdk/testing/peer-simulator.md @@ -19,7 +19,7 @@ usually only needed in tests, so enable it in `dev-dependencies`: ```toml [dev-dependencies] -chia-wallet-sdk = { version = "0.37", features = ["peer-simulator"] } +chia-wallet-sdk = { version = "0.38", features = ["peer-simulator"] } tokio = { version = "1", features = ["full"] } ``` diff --git a/sdk-examples/Cargo.lock b/sdk-examples/Cargo.lock index 1fca45c..7d2c20a 100644 --- a/sdk-examples/Cargo.lock +++ b/sdk-examples/Cargo.lock @@ -489,8 +489,8 @@ dependencies = [ [[package]] name = "chia-sdk-client" -version = "0.37.1" -source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=ad7630cbae5bc2d3c1481eb2fc797249048a16a8#ad7630cbae5bc2d3c1481eb2fc797249048a16a8" +version = "0.38.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=facc8a8984665f0ad8c13ed35ef64f456f658acc#facc8a8984665f0ad8c13ed35ef64f456f658acc" dependencies = [ "aws-lc-rs", "chia-protocol", @@ -508,8 +508,8 @@ dependencies = [ [[package]] name = "chia-sdk-coinset" -version = "0.37.1" -source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=ad7630cbae5bc2d3c1481eb2fc797249048a16a8#ad7630cbae5bc2d3c1481eb2fc797249048a16a8" +version = "0.38.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=facc8a8984665f0ad8c13ed35ef64f456f658acc#facc8a8984665f0ad8c13ed35ef64f456f658acc" dependencies = [ "chia-protocol", "hex", @@ -521,8 +521,8 @@ dependencies = [ [[package]] name = "chia-sdk-daemon" -version = "0.37.1" -source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=ad7630cbae5bc2d3c1481eb2fc797249048a16a8#ad7630cbae5bc2d3c1481eb2fc797249048a16a8" +version = "0.38.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=facc8a8984665f0ad8c13ed35ef64f456f658acc#facc8a8984665f0ad8c13ed35ef64f456f658acc" dependencies = [ "chia-sdk-client", "chia-sdk-coinset", @@ -538,8 +538,8 @@ dependencies = [ [[package]] name = "chia-sdk-derive" -version = "0.37.1" -source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=ad7630cbae5bc2d3c1481eb2fc797249048a16a8#ad7630cbae5bc2d3c1481eb2fc797249048a16a8" +version = "0.38.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=facc8a8984665f0ad8c13ed35ef64f456f658acc#facc8a8984665f0ad8c13ed35ef64f456f658acc" dependencies = [ "convert_case 0.12.0", "quote", @@ -548,8 +548,8 @@ dependencies = [ [[package]] name = "chia-sdk-driver" -version = "0.37.1" -source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=ad7630cbae5bc2d3c1481eb2fc797249048a16a8#ad7630cbae5bc2d3c1481eb2fc797249048a16a8" +version = "0.38.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=facc8a8984665f0ad8c13ed35ef64f456f658acc#facc8a8984665f0ad8c13ed35ef64f456f658acc" dependencies = [ "bip39", "chia-bls 0.53.0", @@ -579,8 +579,8 @@ dependencies = [ [[package]] name = "chia-sdk-signer" -version = "0.37.1" -source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=ad7630cbae5bc2d3c1481eb2fc797249048a16a8#ad7630cbae5bc2d3c1481eb2fc797249048a16a8" +version = "0.38.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=facc8a8984665f0ad8c13ed35ef64f456f658acc#facc8a8984665f0ad8c13ed35ef64f456f658acc" dependencies = [ "chia-bls 0.53.0", "chia-consensus", @@ -599,8 +599,8 @@ dependencies = [ [[package]] name = "chia-sdk-test" -version = "0.37.1" -source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=ad7630cbae5bc2d3c1481eb2fc797249048a16a8#ad7630cbae5bc2d3c1481eb2fc797249048a16a8" +version = "0.38.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=facc8a8984665f0ad8c13ed35ef64f456f658acc#facc8a8984665f0ad8c13ed35ef64f456f658acc" dependencies = [ "anyhow", "axum", @@ -640,8 +640,8 @@ dependencies = [ [[package]] name = "chia-sdk-types" -version = "0.37.1" -source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=ad7630cbae5bc2d3c1481eb2fc797249048a16a8#ad7630cbae5bc2d3c1481eb2fc797249048a16a8" +version = "0.38.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=facc8a8984665f0ad8c13ed35ef64f456f658acc#facc8a8984665f0ad8c13ed35ef64f456f658acc" dependencies = [ "chia-bls 0.53.0", "chia-consensus", @@ -666,8 +666,8 @@ dependencies = [ [[package]] name = "chia-sdk-utils" -version = "0.37.1" -source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=ad7630cbae5bc2d3c1481eb2fc797249048a16a8#ad7630cbae5bc2d3c1481eb2fc797249048a16a8" +version = "0.38.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=facc8a8984665f0ad8c13ed35ef64f456f658acc#facc8a8984665f0ad8c13ed35ef64f456f658acc" dependencies = [ "bech32", "chia-protocol", @@ -755,8 +755,8 @@ dependencies = [ [[package]] name = "chia-wallet-sdk" -version = "0.37.1" -source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=ad7630cbae5bc2d3c1481eb2fc797249048a16a8#ad7630cbae5bc2d3c1481eb2fc797249048a16a8" +version = "0.38.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=facc8a8984665f0ad8c13ed35ef64f456f658acc#facc8a8984665f0ad8c13ed35ef64f456f658acc" dependencies = [ "chia-bls 0.53.0", "chia-consensus", diff --git a/sdk-examples/Cargo.toml b/sdk-examples/Cargo.toml index db9a240..0fed8de 100644 --- a/sdk-examples/Cargo.toml +++ b/sdk-examples/Cargo.toml @@ -11,7 +11,7 @@ edition = "2024" publish = false [dependencies] -chia-wallet-sdk = { git = "https://github.com/xch-dev/chia-wallet-sdk", rev = "ad7630cbae5bc2d3c1481eb2fc797249048a16a8", features = [ +chia-wallet-sdk = { git = "https://github.com/xch-dev/chia-wallet-sdk", rev = "facc8a8984665f0ad8c13ed35ef64f456f658acc", features = [ "rustls", "chip-0008", "chip-0021", @@ -20,11 +20,12 @@ chia-wallet-sdk = { git = "https://github.com/xch-dev/chia-wallet-sdk", rev = "a "chip-0052", "action-layer", "auction", + "pooling", "offer-compression", "peer-simulator", ] } # The HTTP full node simulator isn't exposed by a chia-wallet-sdk feature. -chia-sdk-test = { git = "https://github.com/xch-dev/chia-wallet-sdk", rev = "ad7630cbae5bc2d3c1481eb2fc797249048a16a8", features = [ +chia-sdk-test = { git = "https://github.com/xch-dev/chia-wallet-sdk", rev = "facc8a8984665f0ad8c13ed35ef64f456f658acc", features = [ "full-node-simulator-http", ] } anyhow = "1" diff --git a/sidebars.ts b/sidebars.ts index bcaad7f..2c50e4b 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -88,6 +88,7 @@ const sidebars: SidebarsConfig = { { type: "doc", id: "sdk/primitives/auctions", label: "Auctions" }, { type: "doc", id: "sdk/primitives/splitter", label: "Splitter" }, { type: "doc", id: "sdk/primitives/fusion", label: "NFT Fusion" }, + { type: "doc", id: "sdk/primitives/plot-nft", label: "Plot NFTs" }, { type: "doc", id: "sdk/primitives/secure-the-bag",