diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b218ef9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,108 @@ +name: CI + +env: + RUST_TOOLCHAIN: "1.90.0" + +permissions: + contents: read + +on: + pull_request: + branches: [main] + paths-ignore: + - "**/*.md" + - LICENSE + - "**/*.gitignore" + - .editorconfig + - docs/** + +jobs: + validate-pr: + name: Validate PR source and version bump + runs-on: ubuntu-latest + steps: + - name: Checkout PR head + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch main branch for comparison + run: git fetch origin main:refs/remotes/origin/main + + - name: Read versions (Cargo.toml) + id: versions + shell: bash + run: | + PR_CRATE_VER=$(grep -m1 '^version\s*=\s*"' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/') + MAIN_CRATE_VER=$(git show origin/main:Cargo.toml | grep -m1 '^version\s*=\s*"' | sed -E 's/.*"([^"]+)".*/\1/') + echo "pr_crate_ver=$PR_CRATE_VER" >> $GITHUB_OUTPUT + echo "main_crate_ver=$MAIN_CRATE_VER" >> $GITHUB_OUTPUT + echo "PR Cargo.toml version: $PR_CRATE_VER" + echo "Main Cargo.toml version: $MAIN_CRATE_VER" + + - name: Ensure workspace/package versions match (if both present) + if: ${{ github.event.pull_request.base.ref == 'main' }} + shell: bash + run: | + set -euo pipefail + PKG_VER=$(grep -m1 '^version\s*=\s*"' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/' || true) + WS_VER=$(awk '/^\[workspace.package\]/{f=1;next} /^\[/{f=0} f && /^version\s*=/{print; exit}' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/' || true) + if [ -n "${WS_VER}" ] && [ -n "${PKG_VER}" ] && [ "${WS_VER}" != "${PKG_VER}" ]; then + echo "[workspace.package].version (${WS_VER}) does not match [package].version (${PKG_VER}). Please keep them in sync before merging to main." >&2 + exit 1 + fi + + - name: Ensure version increased + if: ${{ github.event.pull_request.base.ref == 'main' }} + shell: bash + run: | + cmp_versions () { [ "$1" = "$2" ] && return 1; printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1 | grep -q "^$2$"; } + if ! cmp_versions "${{ steps.versions.outputs.pr_crate_ver }}" "${{ steps.versions.outputs.main_crate_ver }}"; then + echo "Cargo.toml version must be greater in the PR than on main (current: ${{ steps.versions.outputs.pr_crate_ver }}, main: ${{ steps.versions.outputs.main_crate_ver }})." >&2 + exit 1 + fi + + rust-checks: + name: Rust checks (${{ matrix.os }}) + needs: validate-pr + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + # --- Rust toolchain --- + - name: Setup Rust toolchain (pinned) + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + components: clippy,rustfmt + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Show versions + shell: bash + run: | + rustc -Vv + cargo -V + echo "Running on: $(uname -a || echo windows)" + + - name: Test + run: cargo test --all-features + + - name: Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Fmt + run: cargo fmt --all -- --check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6e5339b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,104 @@ +name: Release on push to main + +env: + RUST_TOOLCHAIN: "1.75.0" + +permissions: + contents: write + id-token: write + +on: + push: + branches: [main] + +jobs: + rust-tests: + name: Rust tests (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Rust toolchain (pinned) + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + + - name: Test + run: cargo test --all-features + + create-release: + name: Create GitHub Release + needs: rust-tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Read crate version + id: ver + shell: bash + run: | + set -euo pipefail + VERSION=$(grep -m1 '^version\s*=\s*"' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/') + if [ -z "$VERSION" ]; then + echo "Failed to read version from Cargo.toml" >&2 + exit 1 + fi + echo "version=v$VERSION" >> "$GITHUB_OUTPUT" + + - name: Create tag if missing + shell: bash + run: | + set -euo pipefail + TAG="${{ steps.ver.outputs.version }}" + if ! git rev-parse "$TAG" >/dev/null 2>&1; then + git tag "$TAG" "${GITHUB_SHA}" + git push origin "$TAG" + fi + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.ver.outputs.version }} + name: Release ${{ steps.ver.outputs.version }} + generate_release_notes: true + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-crates: + name: Publish to crates.io + needs: [rust-tests, create-release] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Rust toolchain (pinned) + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + + - name: Check crates.io token is present + shell: bash + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + if [ -z "${CARGO_REGISTRY_TOKEN}" ]; then + echo "CARGO_REGISTRY_TOKEN is empty/missing." >&2 + exit 1 + fi + + - name: Publish crate + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish --no-verify --allow-dirty diff --git a/Cargo.lock b/Cargo.lock index ce344e4..c277e0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,8 @@ version = 4 [[package]] name = "action-layer-driver" -version = "0.1.0" +version = "0.1.1" dependencies = [ - "anyhow", "chia", "chia-protocol", "chia-puzzles", @@ -17,7 +16,6 @@ dependencies = [ "hex", "hex-literal", "thiserror 2.0.17", - "tokio", ] [[package]] @@ -1766,7 +1764,7 @@ dependencies = [ [[package]] name = "l2-wallet" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "bech32 0.11.1", @@ -2784,7 +2782,7 @@ dependencies = [ [[package]] name = "singlelaunch" -version = "0.1.0" +version = "0.1.1" dependencies = [ "action-layer-driver", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 7a8a435..6104d25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "action-layer-driver" -version = "0.1.0" +version = "0.1.1" edition = "2021" -rust-version = "1.75.0" +rust-version = "1.90.0" description = "Generic driver for Action Layer (CHIP-0050) singleton spend construction on Chia" license = "MIT" authors = ["DIG Network"] @@ -32,10 +32,6 @@ hex-literal = "0.4" # Error handling thiserror = "2" -[dev-dependencies] -tokio = { version = "1", features = ["full"] } -anyhow = "1.0" - [workspace] members = [ "examples/singlelaunch", @@ -43,9 +39,9 @@ members = [ ] [workspace.package] -version = "0.1.0" +version = "0.1.1" edition = "2021" -rust-version = "1.75.0" +rust-version = "1.90.0" license = "MIT" authors = ["DIG Network"] diff --git a/examples/singlelaunch/src/driver.rs b/examples/singlelaunch/src/driver.rs index ab51f58..27960cb 100644 --- a/examples/singlelaunch/src/driver.rs +++ b/examples/singlelaunch/src/driver.rs @@ -6,13 +6,12 @@ use chia::protocol::{Bytes32, Coin}; use chia_wallet_sdk::driver::SpendContext; -use clvm_traits::{ToClvm, FromClvm}; +use clvm_traits::{FromClvm, ToClvm}; use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; // Import from action-layer-driver crate use action_layer_driver::{ - PuzzleModule, SingletonDriver, LaunchResult, - spawn_child_singleton, child_singleton_puzzle_hash, + child_singleton_puzzle_hash, spawn_child_singleton, LaunchResult, PuzzleModule, SingletonDriver, }; // ============================================================================ @@ -20,13 +19,15 @@ use action_layer_driver::{ // ============================================================================ /// The compiled emit_child_action.rue - curried with child_inner_puzzle_hash -const EMIT_CHILD_ACTION_HEX: &str = include_str!("../../../puzzles/output/emit_child_action.clvm.hex"); +const EMIT_CHILD_ACTION_HEX: &str = + include_str!("../../../puzzles/output/emit_child_action.clvm.hex"); /// The compiled child_inner_puzzle.rue (child type 1) const CHILD_PUZZLE_HEX: &str = include_str!("../../../puzzles/output/child_inner_puzzle.clvm.hex"); /// The compiled child_inner_puzzle_2.rue (child type 2) -const CHILD_PUZZLE_2_HEX: &str = include_str!("../../../puzzles/output/child_inner_puzzle_2.clvm.hex"); +const CHILD_PUZZLE_2_HEX: &str = + include_str!("../../../puzzles/output/child_inner_puzzle_2.clvm.hex"); // ============================================================================ // Application-Specific CLVM Types @@ -52,8 +53,11 @@ impl EmitChildActionCurriedArgs { fn curry_tree_hash(mod_hash: TreeHash, child_inner_puzzle_hash: Bytes32) -> TreeHash { CurriedProgram { program: mod_hash, - args: EmitChildActionCurriedArgs { child_inner_puzzle_hash }, - }.tree_hash() + args: EmitChildActionCurriedArgs { + child_inner_puzzle_hash, + }, + } + .tree_hash() } } @@ -163,18 +167,12 @@ impl TwoActionSingleton { } /// Emit a child singleton via action 1 (child_inner_puzzle) - pub fn emit_child_1( - &mut self, - ctx: &mut SpendContext, - ) -> anyhow::Result { + pub fn emit_child_1(&mut self, ctx: &mut SpendContext) -> anyhow::Result { self.emit_child_impl(ctx, 0, self.child_inner_1, child_inner_puzzle_hash()) } /// Emit a child singleton via action 2 (child_inner_puzzle_2) - pub fn emit_child_2( - &mut self, - ctx: &mut SpendContext, - ) -> anyhow::Result { + pub fn emit_child_2(&mut self, ctx: &mut SpendContext) -> anyhow::Result { self.emit_child_impl(ctx, 1, self.child_inner_2, child_inner_puzzle_2_hash()) } @@ -186,45 +184,59 @@ impl TwoActionSingleton { child_inner_hash: Bytes32, child_inner_tree_hash: TreeHash, ) -> anyhow::Result { - let singleton_coin = self.driver.current_coin() + let singleton_coin = self + .driver + .current_coin() .ok_or_else(|| anyhow::anyhow!("Singleton not launched"))? .clone(); // Compute child launcher ID from current singleton - let child_launcher_id = self.driver.expected_child_launcher_id() + let child_launcher_id = self + .driver + .expected_child_launcher_id() .ok_or_else(|| anyhow::anyhow!("Could not compute child launcher ID"))?; // Child singleton puzzle hash - let child_singleton_hash = child_singleton_puzzle_hash(child_launcher_id, child_inner_tree_hash); + let child_singleton_hash = + child_singleton_puzzle_hash(child_launcher_id, child_inner_tree_hash); // Build curried action puzzle let emit_module = get_emit_child_action_module(); - let action_puzzle = emit_module.curry_puzzle( - ctx, - EmitChildActionCurriedArgs { child_inner_puzzle_hash: child_inner_hash }, - ).map_err(|e| anyhow::anyhow!("{}", e))?; + let action_puzzle = emit_module + .curry_puzzle( + ctx, + EmitChildActionCurriedArgs { + child_inner_puzzle_hash: child_inner_hash, + }, + ) + .map_err(|e| anyhow::anyhow!("{}", e))?; // Build action solution let action_solution = EmitChildActionSolution { my_singleton_coin_id: singleton_coin.coin_id(), child_singleton_puzzle_hash: child_singleton_hash, }; - let action_solution_ptr = ctx.alloc(&action_solution) + let action_solution_ptr = ctx + .alloc(&action_solution) .map_err(|e| anyhow::anyhow!("Failed to alloc action solution: {:?}", e))?; // Build singleton spend using the driver - self.driver.build_action_spend(ctx, action_index, action_puzzle, action_solution_ptr) + self.driver + .build_action_spend(ctx, action_index, action_puzzle, action_solution_ptr) .map_err(|e| anyhow::anyhow!("{}", e))?; // Spawn child singleton (ephemeral launcher) - let child_result = spawn_child_singleton(ctx, singleton_coin.coin_id(), child_inner_tree_hash) - .map_err(|e| anyhow::anyhow!("{}", e))?; + let child_result = + spawn_child_singleton(ctx, singleton_coin.coin_id(), child_inner_tree_hash) + .map_err(|e| anyhow::anyhow!("{}", e))?; // Compute new state let new_state = self.driver.state().increment(); // Compute new parent coin (for tracking before confirmation) - let new_parent_coin = self.driver.expected_new_coin(&new_state) + let new_parent_coin = self + .driver + .expected_new_coin(&new_state) .ok_or_else(|| anyhow::anyhow!("Could not compute new parent coin"))?; Ok(EmitChildResult { diff --git a/examples/singlelaunch/src/main.rs b/examples/singlelaunch/src/main.rs index 36232a3..853101f 100644 --- a/examples/singlelaunch/src/main.rs +++ b/examples/singlelaunch/src/main.rs @@ -13,20 +13,20 @@ use clap::{Parser, Subcommand}; use console::style; use std::path::PathBuf; -use chia::protocol::{Bytes32, Coin, CoinSpend, SpendBundle}; use chia::bls::DerivableKey; +use chia::protocol::{Bytes32, Coin, CoinSpend, SpendBundle}; use chia_wallet_sdk::driver::{SpendContext, StandardLayer}; -use chia_wallet_sdk::types::{Conditions, MAINNET_CONSTANTS, TESTNET11_CONSTANTS}; use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature}; +use chia_wallet_sdk::types::{Conditions, MAINNET_CONSTANTS, TESTNET11_CONSTANTS}; use clvm_utils::ToTreeHash; use clvmr::Allocator; -use datalayer_driver::Signature as DLSignature; use datalayer_driver::async_api as dl; +use datalayer_driver::Signature as DLSignature; -use driver::{TwoActionSingleton, ActionState}; +use driver::{ActionState, TwoActionSingleton}; type StandardArgs = chia::puzzles::standard::StandardArgs; @@ -125,7 +125,12 @@ async fn main() -> anyhow::Result<()> { match cli.command { Commands::Wallet { cmd } => run_wallet_command(cmd).await?, - Commands::TwoActions { testnet, wallet, fee, password } => { + Commands::TwoActions { + testnet, + wallet, + fee, + password, + } => { test_two_actions(testnet, &wallet, fee, password).await?; } } @@ -137,7 +142,12 @@ async fn main() -> anyhow::Result<()> { // Two Actions Test // ============================================================================ -async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: Option) -> Result<()> { +async fn test_two_actions( + testnet: bool, + wallet_name: &str, + fee: u64, + password: Option, +) -> Result<()> { use dialoguer::Password; let singleton_amount: u64 = 1; @@ -177,8 +187,14 @@ async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: let initial_state = ActionState::new(1, 0xDEADBEEF); let mut singleton = TwoActionSingleton::new(wallet_puzzle_hash, initial_state); - println!(" Child inner 1: 0x{}...", &hex::encode(singleton.child_inner_1().to_bytes())[..16]); - println!(" Child inner 2: 0x{}...", &hex::encode(singleton.child_inner_2().to_bytes())[..16]); + println!( + " Child inner 1: 0x{}...", + &hex::encode(singleton.child_inner_1().to_bytes())[..16] + ); + println!( + " Child inner 2: 0x{}...", + &hex::encode(singleton.child_inner_2().to_bytes())[..16] + ); // Connect let peer = connect_peer(testnet).await?; @@ -197,7 +213,9 @@ async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: } let required = singleton_amount + 2 + fee * 3; // singleton + 2 children + 3 fees - let funding_coin_old = coins.coin_states.iter() + let funding_coin_old = coins + .coin_states + .iter() .find(|cs| cs.coin.amount >= required) .map(|cs| &cs.coin) .ok_or_else(|| Error::InsufficientFunds(format!("Need {} mojos", required)))?; @@ -212,11 +230,15 @@ async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: // STEP 1: Create singleton with TWO actions // ========================================================================= println!(); - println!("{}", style("--- Step 1: Create Singleton ---").yellow().bold()); + println!( + "{}", + style("--- Step 1: Create Singleton ---").yellow().bold() + ); let ctx = &mut SpendContext::new(); - let launch_result = singleton.launch(ctx, &funding_coin, singleton_amount) + let launch_result = singleton + .launch(ctx, &funding_coin, singleton_amount) .map_err(|e| Error::Transaction(format!("{:?}", e)))?; let launcher_id = launch_result.launcher_id; @@ -229,13 +251,18 @@ async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: let change_after_create = funding_coin.amount - singleton_amount - fee; let mut conditions = launcher_conditions; if change_after_create > 0 { - conditions = conditions.create_coin(wallet_puzzle_hash, change_after_create, chia::puzzles::Memos::None); + conditions = conditions.create_coin( + wallet_puzzle_hash, + change_after_create, + chia::puzzles::Memos::None, + ); } if fee > 0 { conditions = conditions.reserve_fee(fee); } - standard_layer.spend(ctx, funding_coin.clone(), conditions) + standard_layer + .spend(ctx, funding_coin.clone(), conditions) .map_err(|e| Error::Transaction(format!("{:?}", e)))?; // Sign and broadcast @@ -246,7 +273,14 @@ async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: println!(" Broadcasting..."); broadcast_bundle(&peer, &coin_spends, signature).await?; - wait_for_coin_confirmation(&peer, singleton_coin.puzzle_hash, singleton_coin.coin_id(), genesis, "Singleton").await?; + wait_for_coin_confirmation( + &peer, + singleton_coin.puzzle_hash, + singleton_coin.coin_id(), + genesis, + "Singleton", + ) + .await?; println!(" {} Singleton created!", style("OK").green().bold()); // ========================================================================= @@ -258,23 +292,42 @@ async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: let ctx = &mut SpendContext::new(); // The singleton driver handles proofs internally - let emit_result_1 = singleton.emit_child_1(ctx) + let emit_result_1 = singleton + .emit_child_1(ctx) .map_err(|e| Error::Transaction(format!("{:?}", e)))?; - println!(" Child 1 ID: 0x{}...", &hex::encode(emit_result_1.child_singleton.coin_id().to_bytes())[..16]); + println!( + " Child 1 ID: 0x{}...", + &hex::encode(emit_result_1.child_singleton.coin_id().to_bytes())[..16] + ); // Fund child and pay fee - let fee_coin1 = Coin::new(funding_coin.coin_id(), wallet_puzzle_hash, change_after_create); + let fee_coin1 = Coin::new( + funding_coin.coin_id(), + wallet_puzzle_hash, + change_after_create, + ); let change_after_spend1 = change_after_create - 1 - fee; let mut fee_conditions1 = Conditions::new(); - if fee > 0 { fee_conditions1 = fee_conditions1.reserve_fee(fee); } - fee_conditions1 = fee_conditions1.create_coin(emit_result_1.child_singleton.puzzle_hash, 0, chia::puzzles::Memos::None); + if fee > 0 { + fee_conditions1 = fee_conditions1.reserve_fee(fee); + } + fee_conditions1 = fee_conditions1.create_coin( + emit_result_1.child_singleton.puzzle_hash, + 0, + chia::puzzles::Memos::None, + ); if change_after_spend1 > 0 { - fee_conditions1 = fee_conditions1.create_coin(wallet_puzzle_hash, change_after_spend1, chia::puzzles::Memos::None); + fee_conditions1 = fee_conditions1.create_coin( + wallet_puzzle_hash, + change_after_spend1, + chia::puzzles::Memos::None, + ); } - standard_layer.spend(ctx, fee_coin1.clone(), fee_conditions1) + standard_layer + .spend(ctx, fee_coin1.clone(), fee_conditions1) .map_err(|e| Error::Transaction(format!("{:?}", e)))?; // Sign and broadcast @@ -285,8 +338,22 @@ async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: println!(" Broadcasting..."); broadcast_bundle(&peer, &coin_spends, signature).await?; - wait_for_coin_confirmation(&peer, emit_result_1.new_parent_coin.puzzle_hash, emit_result_1.new_parent_coin.coin_id(), genesis, "Parent singleton").await?; - wait_for_coin_confirmation(&peer, emit_result_1.child_singleton.puzzle_hash, emit_result_1.child_singleton.coin_id(), genesis, "Child 1").await?; + wait_for_coin_confirmation( + &peer, + emit_result_1.new_parent_coin.puzzle_hash, + emit_result_1.new_parent_coin.coin_id(), + genesis, + "Parent singleton", + ) + .await?; + wait_for_coin_confirmation( + &peer, + emit_result_1.child_singleton.puzzle_hash, + emit_result_1.child_singleton.coin_id(), + genesis, + "Child 1", + ) + .await?; println!(" {} Child 1 emitted!", style("OK").green().bold()); // Apply the state change to update internal tracking @@ -301,23 +368,38 @@ async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: let ctx = &mut SpendContext::new(); // The singleton driver handles proofs internally after apply_spend() - let emit_result_2 = singleton.emit_child_2(ctx) + let emit_result_2 = singleton + .emit_child_2(ctx) .map_err(|e| Error::Transaction(format!("{:?}", e)))?; - println!(" Child 2 ID: 0x{}...", &hex::encode(emit_result_2.child_singleton.coin_id().to_bytes())[..16]); + println!( + " Child 2 ID: 0x{}...", + &hex::encode(emit_result_2.child_singleton.coin_id().to_bytes())[..16] + ); // Fund child and pay fee let fee_coin2 = Coin::new(fee_coin1.coin_id(), wallet_puzzle_hash, change_after_spend1); let change_after_spend2 = change_after_spend1 - 1 - fee; let mut fee_conditions2 = Conditions::new(); - if fee > 0 { fee_conditions2 = fee_conditions2.reserve_fee(fee); } - fee_conditions2 = fee_conditions2.create_coin(emit_result_2.child_singleton.puzzle_hash, 0, chia::puzzles::Memos::None); + if fee > 0 { + fee_conditions2 = fee_conditions2.reserve_fee(fee); + } + fee_conditions2 = fee_conditions2.create_coin( + emit_result_2.child_singleton.puzzle_hash, + 0, + chia::puzzles::Memos::None, + ); if change_after_spend2 > 0 { - fee_conditions2 = fee_conditions2.create_coin(wallet_puzzle_hash, change_after_spend2, chia::puzzles::Memos::None); + fee_conditions2 = fee_conditions2.create_coin( + wallet_puzzle_hash, + change_after_spend2, + chia::puzzles::Memos::None, + ); } - standard_layer.spend(ctx, fee_coin2, fee_conditions2) + standard_layer + .spend(ctx, fee_coin2, fee_conditions2) .map_err(|e| Error::Transaction(format!("{:?}", e)))?; // Sign and broadcast @@ -328,8 +410,22 @@ async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: println!(" Broadcasting..."); broadcast_bundle(&peer, &coin_spends, signature).await?; - wait_for_coin_confirmation(&peer, emit_result_2.new_parent_coin.puzzle_hash, emit_result_2.new_parent_coin.coin_id(), genesis, "Parent singleton").await?; - wait_for_coin_confirmation(&peer, emit_result_2.child_singleton.puzzle_hash, emit_result_2.child_singleton.coin_id(), genesis, "Child 2").await?; + wait_for_coin_confirmation( + &peer, + emit_result_2.new_parent_coin.puzzle_hash, + emit_result_2.new_parent_coin.coin_id(), + genesis, + "Parent singleton", + ) + .await?; + wait_for_coin_confirmation( + &peer, + emit_result_2.child_singleton.puzzle_hash, + emit_result_2.child_singleton.coin_id(), + genesis, + "Child 2", + ) + .await?; println!(" {} Child 2 emitted!", style("OK").green().bold()); // ========================================================================= @@ -338,10 +434,22 @@ async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: println!(); println!("{}", style("=== TEST COMPLETE ===").green().bold()); println!(); - println!(" Parent launcher ID: 0x{}", hex::encode(launcher_id.to_bytes())); - println!(" Child 1 launcher ID: 0x{}", hex::encode(emit_result_1.child_launcher_id.to_bytes())); - println!(" Child 2 launcher ID: 0x{}", hex::encode(emit_result_2.child_launcher_id.to_bytes())); - println!(" State: counter {} -> {}", initial_state.counter, emit_result_2.new_state.counter); + println!( + " Parent launcher ID: 0x{}", + hex::encode(launcher_id.to_bytes()) + ); + println!( + " Child 1 launcher ID: 0x{}", + hex::encode(emit_result_1.child_launcher_id.to_bytes()) + ); + println!( + " Child 2 launcher ID: 0x{}", + hex::encode(emit_result_2.child_launcher_id.to_bytes()) + ); + println!( + " State: counter {} -> {}", + initial_state.counter, emit_result_2.new_state.counter + ); println!(); println!("Two actions successfully spawned two child singletons!"); @@ -354,19 +462,27 @@ async fn test_two_actions(testnet: bool, wallet_name: &str, fee: u64, password: async fn run_wallet_command(cmd: WalletCommands) -> Result<()> { match cmd { - WalletCommands::Create { name, show_mnemonic, force, password } => { - create_wallet(&name, show_mnemonic, force, password).await - } - WalletCommands::Show { name, password } => { - show_wallet(&name, password).await - } - WalletCommands::Balance { name, testnet, password } => { - check_balance(&name, testnet, password).await - } + WalletCommands::Create { + name, + show_mnemonic, + force, + password, + } => create_wallet(&name, show_mnemonic, force, password).await, + WalletCommands::Show { name, password } => show_wallet(&name, password).await, + WalletCommands::Balance { + name, + testnet, + password, + } => check_balance(&name, testnet, password).await, } } -async fn create_wallet(name: &str, show_mnemonic: bool, force: bool, password: Option) -> Result<()> { +async fn create_wallet( + name: &str, + show_mnemonic: bool, + force: bool, + password: Option, +) -> Result<()> { use dialoguer::Password; println!("Creating new wallet..."); @@ -376,7 +492,8 @@ async fn create_wallet(name: &str, show_mnemonic: bool, force: bool, password: O if wallet_path.exists() && !force { return Err(Error::Config(format!( - "Wallet '{}' already exists. Use --force to overwrite.", name + "Wallet '{}' already exists. Use --force to overwrite.", + name ))); } @@ -413,7 +530,10 @@ async fn create_wallet(name: &str, show_mnemonic: bool, force: bool, password: O if show_mnemonic { println!(); - println!("{}", style("IMPORTANT: Back up your mnemonic!").yellow().bold()); + println!( + "{}", + style("IMPORTANT: Back up your mnemonic!").yellow().bold() + ); println!(" {}", mnemonic); } @@ -480,7 +600,10 @@ async fn check_balance(name: &str, testnet: bool, password: Option) -> R let derived_pk = derived_sk.public_key(); let address = compute_address(&derived_pk); - println!("Connecting to {}...", if testnet { "testnet" } else { "mainnet" }); + println!( + "Connecting to {}...", + if testnet { "testnet" } else { "mainnet" } + ); let peer = connect_peer(testnet).await?; let genesis = get_genesis_challenge(testnet); @@ -497,7 +620,11 @@ async fn check_balance(name: &str, testnet: bool, password: Option) -> R println!(); println!("Wallet: {}", name); println!(" Address: {}", address); - println!(" Balance: {} mojos ({:.6} XCH)", total, total as f64 / 1e12); + println!( + " Balance: {} mojos ({:.6} XCH)", + total, + total as f64 / 1e12 + ); println!(" Coins: {}", coins.coin_states.len()); Ok(()) @@ -511,7 +638,11 @@ async fn connect_peer(testnet: bool) -> Result { use datalayer_driver::NetworkType; use tokio::time::{timeout, Duration}; - let network_type = if testnet { NetworkType::Testnet11 } else { NetworkType::Mainnet }; + let network_type = if testnet { + NetworkType::Testnet11 + } else { + NetworkType::Mainnet + }; println!(" Connecting..."); @@ -521,8 +652,10 @@ async fn connect_peer(testnet: bool) -> Result { match timeout( Duration::from_secs(30), - dl::connect_random(network_type.clone(), "wallet_node.crt", "wallet_node.key") - ).await { + dl::connect_random(network_type.clone(), "wallet_node.crt", "wallet_node.key"), + ) + .await + { Ok(Ok(peer)) => { println!("{}", style("connected").green()); return Ok(peer); @@ -542,7 +675,9 @@ async fn connect_peer(testnet: bool) -> Result { } } - Err(Error::Network("Failed to connect after 5 attempts".to_string())) + Err(Error::Network( + "Failed to connect after 5 attempts".to_string(), + )) } fn get_genesis_challenge(testnet: bool) -> Bytes32 { @@ -561,7 +696,7 @@ async fn wait_for_coin_confirmation( genesis: Bytes32, coin_name: &str, ) -> Result<()> { - use tokio::time::{Duration, Instant, timeout}; + use tokio::time::{timeout, Duration, Instant}; let start = Instant::now(); let timeout_duration = Duration::from_secs(300); @@ -571,22 +706,36 @@ async fn wait_for_coin_confirmation( loop { if start.elapsed() > timeout_duration { - return Err(Error::Transaction(format!("Timeout waiting for {}", coin_name))); + return Err(Error::Transaction(format!( + "Timeout waiting for {}", + coin_name + ))); } let result = timeout( Duration::from_secs(30), - dl::get_all_unspent_coins(peer, puzzle_hash, None, genesis) - ).await; + dl::get_all_unspent_coins(peer, puzzle_hash, None, genesis), + ) + .await; if let Ok(Ok(coins)) = result { for cs in &coins.coin_states { let this_coin_id = Bytes32::new( - chia_protocol::Coin::new(cs.coin.parent_coin_info, cs.coin.puzzle_hash, cs.coin.amount) - .coin_id().to_bytes() + chia_protocol::Coin::new( + cs.coin.parent_coin_info, + cs.coin.puzzle_hash, + cs.coin.amount, + ) + .coin_id() + .to_bytes(), ); if this_coin_id == expected_coin_id { - println!("\n {} {} confirmed in {}s", style("OK").green().bold(), coin_name, start.elapsed().as_secs()); + println!( + "\n {} {} confirmed in {}s", + style("OK").green().bold(), + coin_name, + start.elapsed().as_secs() + ); return Ok(()); } } @@ -612,7 +761,10 @@ async fn broadcast_bundle( .map_err(|e| Error::Network(format!("{:?}", e)))?; if result.status == 3 { - return Err(Error::Transaction(format!("Broadcast failed: {}", result.error.unwrap_or_default()))); + return Err(Error::Transaction(format!( + "Broadcast failed: {}", + result.error.unwrap_or_default() + ))); } Ok(()) @@ -654,21 +806,26 @@ fn sign_coin_spends( } } - Ok(sigs.into_iter().fold(chia::bls::Signature::default(), |a, b| a + &b)) + Ok(sigs + .into_iter() + .fold(chia::bls::Signature::default(), |a, b| a + &b)) } fn convert_spends_to_dl(spends: &[CoinSpend]) -> Vec { - spends.iter().map(|cs| { - CoinSpend::new( - Coin::new( - Bytes32::new(cs.coin.parent_coin_info.to_bytes()), - Bytes32::new(cs.coin.puzzle_hash.to_bytes()), - cs.coin.amount, - ), - Vec::::from(cs.puzzle_reveal.clone()).into(), - Vec::::from(cs.solution.clone()).into(), - ) - }).collect() + spends + .iter() + .map(|cs| { + CoinSpend::new( + Coin::new( + Bytes32::new(cs.coin.parent_coin_info.to_bytes()), + Bytes32::new(cs.coin.puzzle_hash.to_bytes()), + cs.coin.amount, + ), + Vec::::from(cs.puzzle_reveal.clone()).into(), + Vec::::from(cs.solution.clone()).into(), + ) + }) + .collect() } // ============================================================================ @@ -696,15 +853,20 @@ fn generate_mnemonic() -> Result { Ok(mnemonic.to_string()) } -fn save_encrypted_wallet(path: &PathBuf, secret_key: &chia::bls::SecretKey, passphrase: &str) -> Result<()> { - use sha2::{Sha256, Digest}; +fn save_encrypted_wallet( + path: &PathBuf, + secret_key: &chia::bls::SecretKey, + passphrase: &str, +) -> Result<()> { + use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); hasher.update(passphrase.as_bytes()); let key: [u8; 32] = hasher.finalize().into(); let sk_bytes = secret_key.to_bytes(); - let encrypted: Vec = sk_bytes.iter() + let encrypted: Vec = sk_bytes + .iter() .enumerate() .map(|(i, b)| b ^ key[i % 32]) .collect(); @@ -714,7 +876,7 @@ fn save_encrypted_wallet(path: &PathBuf, secret_key: &chia::bls::SecretKey, pass } fn load_encrypted_wallet(path: &PathBuf, passphrase: &str) -> Result { - use sha2::{Sha256, Digest}; + use sha2::{Digest, Sha256}; let encrypted = std::fs::read(path)?; @@ -722,16 +884,15 @@ fn load_encrypted_wallet(path: &PathBuf, passphrase: &str) -> Result = encrypted.iter() + let decrypted: Vec = encrypted + .iter() .enumerate() .map(|(i, b)| b ^ key[i % 32]) .collect(); - let sk_bytes: [u8; 32] = decrypted.try_into() - .map_err(|_| Error::InvalidPassphrase)?; + let sk_bytes: [u8; 32] = decrypted.try_into().map_err(|_| Error::InvalidPassphrase)?; - chia::bls::SecretKey::from_bytes(&sk_bytes) - .map_err(|_| Error::InvalidPassphrase) + chia::bls::SecretKey::from_bytes(&sk_bytes).map_err(|_| Error::InvalidPassphrase) } fn compute_puzzle_hash(public_key: &chia::bls::PublicKey) -> [u8; 32] { diff --git a/examples/wallet/src/address.rs b/examples/wallet/src/address.rs index 3509c6c..6678ae5 100644 --- a/examples/wallet/src/address.rs +++ b/examples/wallet/src/address.rs @@ -1,8 +1,8 @@ //! Address and puzzle hash utilities. +use crate::error::{WalletError, WalletResult}; use chia::bls::PublicKey; use chia::puzzles::standard::StandardArgs; -use crate::error::{WalletError, WalletResult}; /// Address utilities for Chia addresses. pub struct AddressUtils; @@ -58,8 +58,9 @@ impl AddressUtils { } else { // Hex puzzle hash let hex_str = dest.strip_prefix("0x").unwrap_or(dest); - let bytes = hex::decode(hex_str) - .map_err(|e| WalletError::InvalidAddress(format!("Invalid puzzle hash hex: {}", e)))?; + let bytes = hex::decode(hex_str).map_err(|e| { + WalletError::InvalidAddress(format!("Invalid puzzle hash hex: {}", e)) + })?; if bytes.len() != 32 { return Err(WalletError::InvalidAddress(format!( @@ -88,13 +89,19 @@ impl Address { /// Create an address from a puzzle hash (mainnet). pub fn from_puzzle_hash(puzzle_hash: [u8; 32]) -> Self { let bech32 = AddressUtils::puzzle_hash_to_address(&puzzle_hash, "xch"); - Self { puzzle_hash, bech32 } + Self { + puzzle_hash, + bech32, + } } /// Create a testnet address from a puzzle hash. pub fn from_puzzle_hash_testnet(puzzle_hash: [u8; 32]) -> Self { let bech32 = AddressUtils::puzzle_hash_to_address(&puzzle_hash, "txch"); - Self { puzzle_hash, bech32 } + Self { + puzzle_hash, + bech32, + } } /// Create an address from a public key (mainnet). diff --git a/examples/wallet/src/keys.rs b/examples/wallet/src/keys.rs index a0e41b6..e8b2fcf 100644 --- a/examples/wallet/src/keys.rs +++ b/examples/wallet/src/keys.rs @@ -1,8 +1,8 @@ //! Key derivation and synthetic key computation. -use chia::bls::{PublicKey, SecretKey, DerivableKey}; -use chia::puzzles::DeriveSynthetic; use crate::error::{WalletError, WalletResult}; +use chia::bls::{DerivableKey, PublicKey, SecretKey}; +use chia::puzzles::DeriveSynthetic; use chia_puzzle_types::standard::DEFAULT_HIDDEN_PUZZLE_HASH; /// Key derivation utilities following Chia HD path standards. @@ -108,7 +108,6 @@ impl SyntheticKey { } } - #[cfg(test)] mod tests { use super::*; diff --git a/examples/wallet/src/lib.rs b/examples/wallet/src/lib.rs index 076c6ad..01d938f 100644 --- a/examples/wallet/src/lib.rs +++ b/examples/wallet/src/lib.rs @@ -35,19 +35,19 @@ #![warn(missing_docs)] #![warn(clippy::all)] +mod address; mod error; -mod manager; mod keys; -mod address; -mod transaction; +mod manager; mod storage; +mod transaction; -pub use error::{WalletError, WalletResult}; -pub use manager::{WalletManager, Wallet, DerivationPath}; -pub use keys::{SyntheticKey, KeyDerivation}; pub use address::{Address, AddressUtils}; -pub use transaction::{TransactionSigner, SigningContext}; -pub use storage::{WalletStorage, WalletInfo}; +pub use error::{WalletError, WalletResult}; +pub use keys::{KeyDerivation, SyntheticKey}; +pub use manager::{DerivationPath, Wallet, WalletManager}; +pub use storage::{WalletInfo, WalletStorage}; +pub use transaction::{SigningContext, TransactionSigner}; /// Re-export commonly used types pub use chia::bls::{PublicKey, SecretKey}; diff --git a/examples/wallet/src/manager.rs b/examples/wallet/src/manager.rs index 059fdd5..eab9cee 100644 --- a/examples/wallet/src/manager.rs +++ b/examples/wallet/src/manager.rs @@ -1,10 +1,10 @@ //! Wallet manager for creating, loading, and managing wallets. -use chia::bls::{PublicKey, SecretKey}; +use crate::address::{Address, AddressUtils}; use crate::error::{WalletError, WalletResult}; -use crate::storage::WalletStorage; use crate::keys::{KeyDerivation, SyntheticKey}; -use crate::address::{Address, AddressUtils}; +use crate::storage::WalletStorage; +use chia::bls::{PublicKey, SecretKey}; /// Derivation path for wallet keys. #[derive(Debug, Clone, Copy)] @@ -173,13 +173,12 @@ impl WalletManager { // Parse secret key let sk_hex = secret_key_hex.strip_prefix("0x").unwrap_or(secret_key_hex); - let sk_bytes = hex::decode(sk_hex) - .map_err(|_| WalletError::InvalidSecretKey)?; + let sk_bytes = hex::decode(sk_hex).map_err(|_| WalletError::InvalidSecretKey)?; let sk_array: [u8; 32] = sk_bytes .try_into() .map_err(|_| WalletError::InvalidSecretKey)?; - let secret_key = SecretKey::from_bytes(&sk_array) - .map_err(|_| WalletError::InvalidSecretKey)?; + let secret_key = + SecretKey::from_bytes(&sk_array).map_err(|_| WalletError::InvalidSecretKey)?; // Save encrypted wallet WalletStorage::save_encrypted_wallet(wallet_path.as_path(), &secret_key, passphrase)?; @@ -237,12 +236,17 @@ mod tests { let manager = WalletManager::new(); // In a real test, we'd use a test-specific directory - let wallet = manager.create_wallet("test_wallet", "test_pass", true).unwrap(); + let wallet = manager + .create_wallet("test_wallet", "test_pass", true) + .unwrap(); // Load it back let loaded = manager.load_wallet("test_wallet", "test_pass").unwrap(); - assert_eq!(wallet.master_public_key().to_bytes(), loaded.master_public_key().to_bytes()); + assert_eq!( + wallet.master_public_key().to_bytes(), + loaded.master_public_key().to_bytes() + ); } #[test] diff --git a/examples/wallet/src/storage.rs b/examples/wallet/src/storage.rs index 98e3cad..d4635a9 100644 --- a/examples/wallet/src/storage.rs +++ b/examples/wallet/src/storage.rs @@ -1,9 +1,9 @@ //! Wallet storage and file management. -use std::path::{Path, PathBuf}; use crate::error::{WalletError, WalletResult}; use chia::bls::SecretKey; -use sha2::{Sha256, Digest}; +use sha2::{Digest, Sha256}; +use std::path::{Path, PathBuf}; /// Information about a stored wallet. #[derive(Debug, Clone)] @@ -113,7 +113,6 @@ impl WalletStorage { .try_into() .map_err(|_| WalletError::InvalidPassphrase)?; - SecretKey::from_bytes(&sk_bytes) - .map_err(|_| WalletError::InvalidPassphrase) + SecretKey::from_bytes(&sk_bytes).map_err(|_| WalletError::InvalidPassphrase) } } diff --git a/examples/wallet/src/transaction.rs b/examples/wallet/src/transaction.rs index 100bd75..bc6df02 100644 --- a/examples/wallet/src/transaction.rs +++ b/examples/wallet/src/transaction.rs @@ -1,8 +1,8 @@ //! Transaction signing utilities. +use crate::error::{WalletError, WalletResult}; use chia::bls::{SecretKey, Signature}; use chia::protocol::CoinSpend; -use crate::error::{WalletError, WalletResult}; /// Transaction signing context. pub struct SigningContext { @@ -53,7 +53,9 @@ impl TransactionSigner { coin_spends, &constants, ) - .map_err(|e| WalletError::Transaction(format!("Failed to parse required signatures: {:?}", e)))?; + .map_err(|e| { + WalletError::Transaction(format!("Failed to parse required signatures: {:?}", e)) + })?; // Map public key to secret key for signing let public_key = secret_key.public_key(); @@ -82,10 +84,9 @@ impl TransactionSigner { } // Aggregate all signatures - let aggregate = signatures.iter().fold( - Signature::default(), - |acc, sig| acc + sig, - ); + let aggregate = signatures + .iter() + .fold(Signature::default(), |acc, sig| acc + sig); Ok(aggregate) } @@ -97,9 +98,9 @@ impl TransactionSigner { if amount_lower.ends_with("xch") { // Parse as XCH let num_str = amount_lower.trim_end_matches("xch").trim(); - let xch: f64 = num_str - .parse() - .map_err(|_| WalletError::InvalidAmount(format!("Invalid amount: {}", amount_str)))?; + let xch: f64 = num_str.parse().map_err(|_| { + WalletError::InvalidAmount(format!("Invalid amount: {}", amount_str)) + })?; Ok((xch * 1_000_000_000_000.0) as u64) } else if amount_lower.ends_with("mojo") || amount_lower.ends_with("mojos") { // Parse as mojos @@ -107,14 +108,14 @@ impl TransactionSigner { .trim_end_matches("mojos") .trim_end_matches("mojo") .trim(); - num_str.parse().map_err(|_| { - WalletError::InvalidAmount(format!("Invalid amount: {}", amount_str)) - }) + num_str + .parse() + .map_err(|_| WalletError::InvalidAmount(format!("Invalid amount: {}", amount_str))) } else { // Assume mojos - amount_str.parse().map_err(|_| { - WalletError::InvalidAmount(format!("Invalid amount: {}", amount_str)) - }) + amount_str + .parse() + .map_err(|_| WalletError::InvalidAmount(format!("Invalid amount: {}", amount_str))) } } } @@ -125,18 +126,33 @@ mod tests { #[test] fn test_parse_amount_xch() { - assert_eq!(TransactionSigner::parse_amount("1.5xch").unwrap(), 1_500_000_000_000); - assert_eq!(TransactionSigner::parse_amount("0.1xch").unwrap(), 100_000_000_000); + assert_eq!( + TransactionSigner::parse_amount("1.5xch").unwrap(), + 1_500_000_000_000 + ); + assert_eq!( + TransactionSigner::parse_amount("0.1xch").unwrap(), + 100_000_000_000 + ); } #[test] fn test_parse_amount_mojos() { - assert_eq!(TransactionSigner::parse_amount("1000000mojo").unwrap(), 1_000_000); - assert_eq!(TransactionSigner::parse_amount("5000000mojos").unwrap(), 5_000_000); + assert_eq!( + TransactionSigner::parse_amount("1000000mojo").unwrap(), + 1_000_000 + ); + assert_eq!( + TransactionSigner::parse_amount("5000000mojos").unwrap(), + 5_000_000 + ); } #[test] fn test_parse_amount_plain() { - assert_eq!(TransactionSigner::parse_amount("1000000").unwrap(), 1_000_000); + assert_eq!( + TransactionSigner::parse_amount("1000000").unwrap(), + 1_000_000 + ); } } diff --git a/src/action_layer.rs b/src/action_layer.rs index f1905c7..e9d1b01 100644 --- a/src/action_layer.rs +++ b/src/action_layer.rs @@ -5,7 +5,9 @@ use std::marker::PhantomData; use chia::protocol::Bytes32; -use chia_wallet_sdk::driver::{ActionLayer, ActionLayerSolution, Finalizer, Layer, Spend, SpendContext}; +use chia_wallet_sdk::driver::{ + ActionLayer, ActionLayerSolution, Finalizer, Layer, Spend, SpendContext, +}; use chia_wallet_sdk::types::puzzles::{ActionLayerArgs, DefaultFinalizer2ndCurryArgs}; use chia_wallet_sdk::types::MerkleTree; use clvm_traits::{FromClvm, ToClvm}; diff --git a/src/lib.rs b/src/lib.rs index 0d9dee1..3ad306b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,19 +23,19 @@ //! let curried_ptr = puzzle.curry_puzzle(ctx, MyCurryArgs { some_hash })?; //! ``` -mod puzzle; mod action_layer; -mod singleton; mod error; +mod puzzle; +mod singleton; -pub use puzzle::PuzzleModule; pub use action_layer::ActionLayerConfig; -pub use singleton::*; pub use error::DriverError; +pub use puzzle::PuzzleModule; +pub use singleton::*; // Re-export commonly used types from dependencies pub use chia::protocol::{Bytes32, Coin, CoinSpend}; -pub use chia::puzzles::{Proof, EveProof, LineageProof}; +pub use chia::puzzles::{EveProof, LineageProof, Proof}; pub use chia_wallet_sdk::driver::SpendContext; pub use clvm_utils::TreeHash; pub use clvmr::NodePtr; diff --git a/src/puzzle.rs b/src/puzzle.rs index 86a2ef6..c2666a0 100644 --- a/src/puzzle.rs +++ b/src/puzzle.rs @@ -49,7 +49,10 @@ impl PuzzleModule { let ptr = node_from_bytes(&mut alloc, &bytes) .map_err(|e| DriverError::PuzzleParse(format!("{:?}", e)))?; let mod_hash = chia::clvm_utils::tree_hash(&alloc, ptr); - Ok(Self { mod_hash, mod_bytes: bytes }) + Ok(Self { + mod_hash, + mod_bytes: bytes, + }) } /// Create a PuzzleModule from a hex string (with optional whitespace) diff --git a/src/singleton/driver.rs b/src/singleton/driver.rs index 97d77df..e1c2ffb 100644 --- a/src/singleton/driver.rs +++ b/src/singleton/driver.rs @@ -1,357 +1,358 @@ -//! SingletonDriver - Core driver for Action Layer singletons - -use chia::protocol::{Bytes32, Coin, CoinSpend}; -use chia::puzzles::singleton::{SingletonArgs, SingletonSolution, SingletonStruct}; -use chia_wallet_sdk::driver::{Launcher, SpendContext}; -use clvm_traits::{FromClvm, ToClvm}; -use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; -use clvmr::{Allocator, NodePtr}; - -use crate::action_layer::ActionLayerConfig; -use crate::error::DriverError; -use crate::singleton::types::{LaunchResult, SingletonCoin, SingletonLineage}; - -/// Singleton launcher puzzle hash (standard) -pub const SINGLETON_LAUNCHER_PUZZLE_HASH: [u8; 32] = hex_literal::hex!( - "eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9" -); - -/// Core driver for Action Layer singletons. -/// -/// Generic over the state type `S`. Handles common singleton operations: -/// - Launching -/// - Building action spends -/// - State/lineage tracking -/// - Proof generation -/// -/// Specific singleton implementations (NetworkSingleton, CollateralSingleton, etc.) -/// wrap this driver and expose typed action methods. -pub struct SingletonDriver { - /// On-chain singleton info (None if not yet launched) - singleton: Option, - - /// Current state - state: S, - - /// Action layer configuration - action_config: ActionLayerConfig, - - /// Hint for the default finalizer - hint: Bytes32, -} - -impl SingletonDriver -where - S: Clone + ToClvm + FromClvm + ToTreeHash, -{ - // ======================================================================== - // Construction - // ======================================================================== - - /// Create a driver for a new singleton (not yet launched) - pub fn new(action_hashes: Vec, hint: Bytes32, initial_state: S) -> Self { - let action_config = ActionLayerConfig::new(action_hashes, hint); - Self { - singleton: None, - state: initial_state, - action_config, - hint, - } - } - - /// Create a driver for an existing on-chain singleton - pub fn from_coin( - singleton: SingletonCoin, - state: S, - action_hashes: Vec, - hint: Bytes32, - ) -> Self { - let action_config = ActionLayerConfig::new(action_hashes, hint); - Self { - singleton: Some(singleton), - state, - action_config, - hint, - } - } - - // ======================================================================== - // Accessors - // ======================================================================== - - /// Get the launcher ID (None if not launched) - pub fn launcher_id(&self) -> Option { - self.singleton.as_ref().map(|s| s.launcher_id) - } - - /// Get the current coin (None if not launched or melted) - pub fn current_coin(&self) -> Option<&Coin> { - self.singleton.as_ref().map(|s| &s.coin) - } - - /// Get the current state - pub fn state(&self) -> &S { - &self.state - } - - /// Get mutable reference to state (for direct updates) - pub fn state_mut(&mut self) -> &mut S { - &mut self.state - } - - /// Check if the singleton has been launched - pub fn is_launched(&self) -> bool { - self.singleton.is_some() - } - - /// Get the hint - pub fn hint(&self) -> Bytes32 { - self.hint - } - - /// Get the action layer config - pub fn action_config(&self) -> &ActionLayerConfig { - &self.action_config - } - - /// Compute the inner puzzle hash for the current state - pub fn inner_puzzle_hash(&self) -> TreeHash { - self.action_config.inner_puzzle_hash(&self.state) - } - - /// Compute the inner puzzle hash for a given state - pub fn inner_puzzle_hash_for_state(&self, state: &S) -> TreeHash { - self.action_config.inner_puzzle_hash(state) - } - - /// Compute the full singleton puzzle hash (None if not launched) - pub fn singleton_puzzle_hash(&self) -> Option { - self.launcher_id().map(|launcher_id| { - SingletonArgs::curry_tree_hash(launcher_id, self.inner_puzzle_hash()) - }) - } - - /// Compute singleton puzzle hash for a given state - pub fn singleton_puzzle_hash_for_state(&self, state: &S) -> Option { - self.launcher_id().map(|launcher_id| { - SingletonArgs::curry_tree_hash(launcher_id, self.inner_puzzle_hash_for_state(state)) - }) - } - - /// Get the proof for the next spend (None if not launched) - pub fn proof(&self) -> Option { - self.singleton.as_ref().map(|s| s.proof()) - } - - // ======================================================================== - // Action Hash Management - // ======================================================================== - - /// Update action hashes (needed after launch when network_id becomes known) - pub fn update_action_hashes(&mut self, action_hashes: Vec) { - self.action_config = ActionLayerConfig::new(action_hashes, self.hint); - } - - // ======================================================================== - // Lifecycle Operations - // ======================================================================== - - /// Launch the singleton - /// - /// Creates the launcher spend in the context. Returns the launcher ID and - /// conditions to be included in the funding coin spend. - pub fn launch( - &mut self, - ctx: &mut SpendContext, - funding_coin: &Coin, - amount: u64, - ) -> Result { - if self.is_launched() { - return Err(DriverError::AlreadyLaunched); - } - - let inner_hash: Bytes32 = self.inner_puzzle_hash().into(); - - let launcher = Launcher::new(funding_coin.coin_id(), amount); - let launcher_id = launcher.coin().coin_id(); - - let (launcher_conditions, singleton_coin) = launcher - .spend(ctx, inner_hash, ()) - .map_err(|e| DriverError::Launcher(format!("{:?}", e)))?; - - // Update internal state - let lineage = SingletonLineage::eve(funding_coin.coin_id(), amount); - self.singleton = Some(SingletonCoin::new(launcher_id, singleton_coin.clone(), lineage)); - - Ok(LaunchResult { - launcher_id, - coin: singleton_coin, - conditions: launcher_conditions, - }) - } - - /// Build an action spend. - /// - /// Adds the singleton spend to the context. Does NOT update internal state - - /// call `apply_spend()` after the transaction confirms. - /// - /// # Arguments - /// * `ctx` - Spend context - /// * `action_index` - Index of the action in the merkle tree - /// * `action_puzzle` - The curried action puzzle (NodePtr) - /// * `action_solution` - The action solution (NodePtr) - pub fn build_action_spend( - &self, - ctx: &mut SpendContext, - action_index: usize, - action_puzzle: NodePtr, - action_solution: NodePtr, - ) -> Result<(), DriverError> { - let singleton = self.singleton.as_ref() - .ok_or(DriverError::NotLaunched)?; - - // Build action layer spend (inner puzzle + solution) - let (inner_puzzle, inner_solution) = self.action_config.build_action_spend( - ctx, - self.state.clone(), - action_index, - action_puzzle, - action_solution, - )?; - - // Build singleton puzzle - let singleton_puzzle = self.build_singleton_puzzle(ctx, inner_puzzle)?; - - // Build singleton solution - let singleton_solution = self.build_singleton_solution( - ctx, - singleton.proof(), - singleton.coin.amount, - inner_solution, - )?; - - // Create and insert coin spend - let puzzle_reveal = ctx.serialize(&singleton_puzzle) - .map_err(|e| DriverError::Serialize(format!("{:?}", e)))?; - let solution = ctx.serialize(&singleton_solution) - .map_err(|e| DriverError::Serialize(format!("{:?}", e)))?; - - let coin_spend = CoinSpend::new(singleton.coin.clone(), puzzle_reveal, solution); - ctx.insert(coin_spend); - - Ok(()) - } - - /// Update internal state after a spend confirms. - /// - /// Call this after the transaction is confirmed on chain. - pub fn apply_spend(&mut self, new_state: S) { - if let Some(singleton) = &self.singleton { - let launcher_id = singleton.launcher_id; - let old_coin = singleton.coin.clone(); - let old_inner_hash = self.inner_puzzle_hash(); - - // Update state first (needed for new puzzle hash calculation) - self.state = new_state; - - // Compute new coin - let new_puzzle_hash: Bytes32 = self.singleton_puzzle_hash() - .expect("singleton should exist") - .into(); - let new_coin = Coin::new(old_coin.coin_id(), new_puzzle_hash, old_coin.amount); - - // Update lineage - let new_lineage = SingletonLineage::lineage(old_coin, old_inner_hash); - - self.singleton = Some(SingletonCoin::new(launcher_id, new_coin, new_lineage)); - } - } - - /// Mark the singleton as melted (destroyed). - /// - /// Call this after a melt action (like withdraw) confirms. - pub fn mark_melted(&mut self) { - self.singleton = None; - } - - // ======================================================================== - // Helpers - // ======================================================================== - - /// Compute the expected new coin after a spend with given new state - pub fn expected_new_coin(&self, new_state: &S) -> Option { - let singleton = self.singleton.as_ref()?; - let new_inner_hash = self.inner_puzzle_hash_for_state(new_state); - let new_puzzle_hash: Bytes32 = SingletonArgs::curry_tree_hash( - singleton.launcher_id, - new_inner_hash, - ).into(); - Some(Coin::new( - singleton.coin.coin_id(), - new_puzzle_hash, - singleton.coin.amount, - )) - } - - /// Compute the expected child launcher ID for a child singleton - /// emitted by the current singleton - pub fn expected_child_launcher_id(&self) -> Option { - let singleton = self.singleton.as_ref()?; - let child_launcher_coin = Coin::new( - singleton.coin.coin_id(), - Bytes32::new(SINGLETON_LAUNCHER_PUZZLE_HASH), - 0, - ); - Some(child_launcher_coin.coin_id()) - } - - /// Build the singleton puzzle (internal helper) - fn build_singleton_puzzle( - &self, - ctx: &mut SpendContext, - inner_puzzle: NodePtr, - ) -> Result { - let launcher_id = self.launcher_id() - .ok_or(DriverError::NotLaunched)?; - - let singleton_mod_hash = TreeHash::new(chia_puzzles::SINGLETON_TOP_LAYER_V1_1_HASH); - let singleton_ptr = ctx - .puzzle(singleton_mod_hash, &chia_puzzles::SINGLETON_TOP_LAYER_V1_1) - .map_err(|e| DriverError::PuzzleLoad(format!("singleton: {:?}", e)))?; - - ctx.alloc(&CurriedProgram { - program: singleton_ptr, - args: SingletonArgs { - singleton_struct: SingletonStruct::new(launcher_id), - inner_puzzle, - }, - }) - .map_err(|e| DriverError::Alloc(format!("singleton curry: {:?}", e))) - } - - /// Build the singleton solution (internal helper) - fn build_singleton_solution( - &self, - ctx: &mut SpendContext, - proof: chia::puzzles::Proof, - amount: u64, - inner_solution: NodePtr, - ) -> Result { - ctx.alloc(&SingletonSolution { - lineage_proof: proof, - amount, - inner_solution, - }) - .map_err(|e| DriverError::Alloc(format!("singleton solution: {:?}", e))) - } -} - -impl std::fmt::Debug for SingletonDriver { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SingletonDriver") - .field("launcher_id", &self.singleton.as_ref().map(|s| hex::encode(s.launcher_id))) - .field("is_launched", &self.singleton.is_some()) - .field("state", &self.state) - .finish() - } -} +//! SingletonDriver - Core driver for Action Layer singletons + +use chia::protocol::{Bytes32, Coin, CoinSpend}; +use chia::puzzles::singleton::{SingletonArgs, SingletonSolution, SingletonStruct}; +use chia_wallet_sdk::driver::{Launcher, SpendContext}; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; + +use crate::action_layer::ActionLayerConfig; +use crate::error::DriverError; +use crate::singleton::types::{LaunchResult, SingletonCoin, SingletonLineage}; + +/// Singleton launcher puzzle hash (standard) +pub const SINGLETON_LAUNCHER_PUZZLE_HASH: [u8; 32] = + hex_literal::hex!("eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9"); + +/// Core driver for Action Layer singletons. +/// +/// Generic over the state type `S`. Handles common singleton operations: +/// - Launching +/// - Building action spends +/// - State/lineage tracking +/// - Proof generation +/// +/// Specific singleton implementations (NetworkSingleton, CollateralSingleton, etc.) +/// wrap this driver and expose typed action methods. +pub struct SingletonDriver { + /// On-chain singleton info (None if not yet launched) + singleton: Option, + + /// Current state + state: S, + + /// Action layer configuration + action_config: ActionLayerConfig, + + /// Hint for the default finalizer + hint: Bytes32, +} + +impl SingletonDriver +where + S: Clone + ToClvm + FromClvm + ToTreeHash, +{ + // ======================================================================== + // Construction + // ======================================================================== + + /// Create a driver for a new singleton (not yet launched) + pub fn new(action_hashes: Vec, hint: Bytes32, initial_state: S) -> Self { + let action_config = ActionLayerConfig::new(action_hashes, hint); + Self { + singleton: None, + state: initial_state, + action_config, + hint, + } + } + + /// Create a driver for an existing on-chain singleton + pub fn from_coin( + singleton: SingletonCoin, + state: S, + action_hashes: Vec, + hint: Bytes32, + ) -> Self { + let action_config = ActionLayerConfig::new(action_hashes, hint); + Self { + singleton: Some(singleton), + state, + action_config, + hint, + } + } + + // ======================================================================== + // Accessors + // ======================================================================== + + /// Get the launcher ID (None if not launched) + pub fn launcher_id(&self) -> Option { + self.singleton.as_ref().map(|s| s.launcher_id) + } + + /// Get the current coin (None if not launched or melted) + pub fn current_coin(&self) -> Option<&Coin> { + self.singleton.as_ref().map(|s| &s.coin) + } + + /// Get the current state + pub fn state(&self) -> &S { + &self.state + } + + /// Get mutable reference to state (for direct updates) + pub fn state_mut(&mut self) -> &mut S { + &mut self.state + } + + /// Check if the singleton has been launched + pub fn is_launched(&self) -> bool { + self.singleton.is_some() + } + + /// Get the hint + pub fn hint(&self) -> Bytes32 { + self.hint + } + + /// Get the action layer config + pub fn action_config(&self) -> &ActionLayerConfig { + &self.action_config + } + + /// Compute the inner puzzle hash for the current state + pub fn inner_puzzle_hash(&self) -> TreeHash { + self.action_config.inner_puzzle_hash(&self.state) + } + + /// Compute the inner puzzle hash for a given state + pub fn inner_puzzle_hash_for_state(&self, state: &S) -> TreeHash { + self.action_config.inner_puzzle_hash(state) + } + + /// Compute the full singleton puzzle hash (None if not launched) + pub fn singleton_puzzle_hash(&self) -> Option { + self.launcher_id().map(|launcher_id| { + SingletonArgs::curry_tree_hash(launcher_id, self.inner_puzzle_hash()) + }) + } + + /// Compute singleton puzzle hash for a given state + pub fn singleton_puzzle_hash_for_state(&self, state: &S) -> Option { + self.launcher_id().map(|launcher_id| { + SingletonArgs::curry_tree_hash(launcher_id, self.inner_puzzle_hash_for_state(state)) + }) + } + + /// Get the proof for the next spend (None if not launched) + pub fn proof(&self) -> Option { + self.singleton.as_ref().map(|s| s.proof()) + } + + // ======================================================================== + // Action Hash Management + // ======================================================================== + + /// Update action hashes (needed after launch when network_id becomes known) + pub fn update_action_hashes(&mut self, action_hashes: Vec) { + self.action_config = ActionLayerConfig::new(action_hashes, self.hint); + } + + // ======================================================================== + // Lifecycle Operations + // ======================================================================== + + /// Launch the singleton + /// + /// Creates the launcher spend in the context. Returns the launcher ID and + /// conditions to be included in the funding coin spend. + pub fn launch( + &mut self, + ctx: &mut SpendContext, + funding_coin: &Coin, + amount: u64, + ) -> Result { + if self.is_launched() { + return Err(DriverError::AlreadyLaunched); + } + + let inner_hash: Bytes32 = self.inner_puzzle_hash().into(); + + let launcher = Launcher::new(funding_coin.coin_id(), amount); + let launcher_id = launcher.coin().coin_id(); + + let (launcher_conditions, singleton_coin) = launcher + .spend(ctx, inner_hash, ()) + .map_err(|e| DriverError::Launcher(format!("{:?}", e)))?; + + // Update internal state + let lineage = SingletonLineage::eve(funding_coin.coin_id(), amount); + self.singleton = Some(SingletonCoin::new(launcher_id, singleton_coin, lineage)); + + Ok(LaunchResult { + launcher_id, + coin: singleton_coin, + conditions: launcher_conditions, + }) + } + + /// Build an action spend. + /// + /// Adds the singleton spend to the context. Does NOT update internal state - + /// call `apply_spend()` after the transaction confirms. + /// + /// # Arguments + /// * `ctx` - Spend context + /// * `action_index` - Index of the action in the merkle tree + /// * `action_puzzle` - The curried action puzzle (NodePtr) + /// * `action_solution` - The action solution (NodePtr) + pub fn build_action_spend( + &self, + ctx: &mut SpendContext, + action_index: usize, + action_puzzle: NodePtr, + action_solution: NodePtr, + ) -> Result<(), DriverError> { + let singleton = self.singleton.as_ref().ok_or(DriverError::NotLaunched)?; + + // Build action layer spend (inner puzzle + solution) + let (inner_puzzle, inner_solution) = self.action_config.build_action_spend( + ctx, + self.state.clone(), + action_index, + action_puzzle, + action_solution, + )?; + + // Build singleton puzzle + let singleton_puzzle = self.build_singleton_puzzle(ctx, inner_puzzle)?; + + // Build singleton solution + let singleton_solution = self.build_singleton_solution( + ctx, + singleton.proof(), + singleton.coin.amount, + inner_solution, + )?; + + // Create and insert coin spend + let puzzle_reveal = ctx + .serialize(&singleton_puzzle) + .map_err(|e| DriverError::Serialize(format!("{:?}", e)))?; + let solution = ctx + .serialize(&singleton_solution) + .map_err(|e| DriverError::Serialize(format!("{:?}", e)))?; + + let coin_spend = CoinSpend::new(singleton.coin, puzzle_reveal, solution); + ctx.insert(coin_spend); + + Ok(()) + } + + /// Update internal state after a spend confirms. + /// + /// Call this after the transaction is confirmed on chain. + pub fn apply_spend(&mut self, new_state: S) { + if let Some(singleton) = &self.singleton { + let launcher_id = singleton.launcher_id; + let old_coin = singleton.coin; + let old_inner_hash = self.inner_puzzle_hash(); + + // Update state first (needed for new puzzle hash calculation) + self.state = new_state; + + // Compute new coin + let new_puzzle_hash: Bytes32 = self + .singleton_puzzle_hash() + .expect("singleton should exist") + .into(); + let new_coin = Coin::new(old_coin.coin_id(), new_puzzle_hash, old_coin.amount); + + // Update lineage + let new_lineage = SingletonLineage::lineage(old_coin, old_inner_hash); + + self.singleton = Some(SingletonCoin::new(launcher_id, new_coin, new_lineage)); + } + } + + /// Mark the singleton as melted (destroyed). + /// + /// Call this after a melt action (like withdraw) confirms. + pub fn mark_melted(&mut self) { + self.singleton = None; + } + + // ======================================================================== + // Helpers + // ======================================================================== + + /// Compute the expected new coin after a spend with given new state + pub fn expected_new_coin(&self, new_state: &S) -> Option { + let singleton = self.singleton.as_ref()?; + let new_inner_hash = self.inner_puzzle_hash_for_state(new_state); + let new_puzzle_hash: Bytes32 = + SingletonArgs::curry_tree_hash(singleton.launcher_id, new_inner_hash).into(); + Some(Coin::new( + singleton.coin.coin_id(), + new_puzzle_hash, + singleton.coin.amount, + )) + } + + /// Compute the expected child launcher ID for a child singleton + /// emitted by the current singleton + pub fn expected_child_launcher_id(&self) -> Option { + let singleton = self.singleton.as_ref()?; + let child_launcher_coin = Coin::new( + singleton.coin.coin_id(), + Bytes32::new(SINGLETON_LAUNCHER_PUZZLE_HASH), + 0, + ); + Some(child_launcher_coin.coin_id()) + } + + /// Build the singleton puzzle (internal helper) + fn build_singleton_puzzle( + &self, + ctx: &mut SpendContext, + inner_puzzle: NodePtr, + ) -> Result { + let launcher_id = self.launcher_id().ok_or(DriverError::NotLaunched)?; + + let singleton_mod_hash = TreeHash::new(chia_puzzles::SINGLETON_TOP_LAYER_V1_1_HASH); + let singleton_ptr = ctx + .puzzle(singleton_mod_hash, &chia_puzzles::SINGLETON_TOP_LAYER_V1_1) + .map_err(|e| DriverError::PuzzleLoad(format!("singleton: {:?}", e)))?; + + ctx.alloc(&CurriedProgram { + program: singleton_ptr, + args: SingletonArgs { + singleton_struct: SingletonStruct::new(launcher_id), + inner_puzzle, + }, + }) + .map_err(|e| DriverError::Alloc(format!("singleton curry: {:?}", e))) + } + + /// Build the singleton solution (internal helper) + fn build_singleton_solution( + &self, + ctx: &mut SpendContext, + proof: chia::puzzles::Proof, + amount: u64, + inner_solution: NodePtr, + ) -> Result { + ctx.alloc(&SingletonSolution { + lineage_proof: proof, + amount, + inner_solution, + }) + .map_err(|e| DriverError::Alloc(format!("singleton solution: {:?}", e))) + } +} + +impl std::fmt::Debug for SingletonDriver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SingletonDriver") + .field( + "launcher_id", + &self.singleton.as_ref().map(|s| hex::encode(s.launcher_id)), + ) + .field("is_launched", &self.singleton.is_some()) + .field("state", &self.state) + .finish() + } +} diff --git a/src/singleton/helpers.rs b/src/singleton/helpers.rs index d7bef7b..c56deb6 100644 --- a/src/singleton/helpers.rs +++ b/src/singleton/helpers.rs @@ -1,190 +1,190 @@ -//! Singleton helper functions -//! -//! Standalone helper functions for common singleton operations. - -use chia::protocol::{Bytes32, Coin}; -use chia::puzzles::singleton::SingletonArgs; -use chia::puzzles::{EveProof, LineageProof, Proof}; -use chia_wallet_sdk::driver::{Launcher, SpendContext}; -use chia_wallet_sdk::types::Conditions; -use clvm_utils::TreeHash; -use clvmr::NodePtr; - -use crate::DriverError; -use super::driver::SINGLETON_LAUNCHER_PUZZLE_HASH; - -// ============================================================================ -// Proof Creation (for backward compatibility) -// ============================================================================ - -/// Create an eve proof for the first singleton spend (after launch) -pub fn create_eve_proof(launcher_parent_coin_id: Bytes32, singleton_amount: u64) -> Proof { - Proof::Eve(EveProof { - parent_parent_coin_info: launcher_parent_coin_id, - parent_amount: singleton_amount, - }) -} - -/// Create a lineage proof for subsequent singleton spends -pub fn create_lineage_proof(parent_coin: &Coin, parent_inner_puzzle_hash: TreeHash) -> Proof { - Proof::Lineage(LineageProof { - parent_parent_coin_info: parent_coin.parent_coin_info, - parent_inner_puzzle_hash: parent_inner_puzzle_hash.into(), - parent_amount: parent_coin.amount, - }) -} - -// ============================================================================ -// Puzzle Hash Computation -// ============================================================================ - -/// Compute the full singleton puzzle hash given launcher_id and inner puzzle hash -pub fn singleton_puzzle_hash(launcher_id: Bytes32, inner_puzzle_hash: TreeHash) -> Bytes32 { - SingletonArgs::curry_tree_hash(launcher_id, inner_puzzle_hash).into() -} - -/// Compute the puzzle hash for a child singleton given its launcher ID and inner puzzle hash -pub fn child_singleton_puzzle_hash( - child_launcher_id: Bytes32, - child_inner_puzzle_hash: TreeHash, -) -> Bytes32 { - SingletonArgs::curry_tree_hash(child_launcher_id, child_inner_puzzle_hash).into() -} - -/// Compute the expected child launcher ID given the parent singleton coin ID -pub fn expected_child_launcher_id(parent_singleton_coin_id: Bytes32) -> Bytes32 { - Coin::new( - parent_singleton_coin_id, - Bytes32::new(SINGLETON_LAUNCHER_PUZZLE_HASH), - 0, - ) - .coin_id() -} - -// ============================================================================ -// Child Singleton Spawning -// ============================================================================ - -/// Result of spawning a child singleton via ephemeral launcher -#[derive(Debug, Clone)] -pub struct ChildLaunchResult { - pub child_launcher_id: Bytes32, - pub child_singleton: Coin, -} - -/// Spawn a child singleton via ephemeral (0-amount) launcher. -/// -/// The launcher coin is parented by the parent singleton coin. -/// This is used when an action emits a child singleton. -pub fn spawn_child_singleton( - ctx: &mut SpendContext, - parent_coin_id: Bytes32, - child_inner_puzzle_hash: TreeHash, -) -> Result { - // Child launcher coin (ephemeral, 0-amount) - let child_launcher_coin = Coin::new( - parent_coin_id, - Bytes32::new(SINGLETON_LAUNCHER_PUZZLE_HASH), - 0, - ); - let child_launcher_id = child_launcher_coin.coin_id(); - - // Spend the launcher to create the child singleton - let (_child_conds, child_singleton_info) = - Launcher::from_coin(child_launcher_coin, Conditions::new()) - .with_singleton_amount(1) - .mint_vault(ctx, child_inner_puzzle_hash, ()) - .map_err(|e| DriverError::Launcher(format!("child spawn: {:?}", e)))?; - - Ok(ChildLaunchResult { - child_launcher_id, - child_singleton: child_singleton_info.coin, - }) -} - -// ============================================================================ -// Low-Level Puzzle Building (for advanced use cases) -// ============================================================================ - -use chia::puzzles::singleton::{SingletonSolution, SingletonStruct}; -use clvm_utils::CurriedProgram; - -/// Build a singleton puzzle NodePtr -pub fn build_singleton_puzzle( - ctx: &mut SpendContext, - launcher_id: Bytes32, - inner_puzzle: NodePtr, -) -> Result { - let singleton_mod_hash = TreeHash::new(chia_puzzles::SINGLETON_TOP_LAYER_V1_1_HASH); - let singleton_ptr = ctx - .puzzle(singleton_mod_hash, &chia_puzzles::SINGLETON_TOP_LAYER_V1_1) - .map_err(|e| DriverError::PuzzleLoad(format!("singleton: {:?}", e)))?; - - ctx.alloc(&CurriedProgram { - program: singleton_ptr, - args: SingletonArgs { - singleton_struct: SingletonStruct::new(launcher_id), - inner_puzzle, - }, - }) - .map_err(|e| DriverError::Alloc(format!("singleton curry: {:?}", e))) -} - -/// Build a singleton solution NodePtr -pub fn build_singleton_solution( - ctx: &mut SpendContext, - proof: Proof, - amount: u64, - inner_solution: NodePtr, -) -> Result { - ctx.alloc(&SingletonSolution { - lineage_proof: proof, - amount, - inner_solution, - }) - .map_err(|e| DriverError::Alloc(format!("singleton solution: {:?}", e))) -} - -/// Create and insert a singleton coin spend -pub fn create_singleton_coin_spend( - ctx: &mut SpendContext, - singleton_coin: &Coin, - singleton_puzzle: NodePtr, - singleton_solution: NodePtr, -) -> Result<(), DriverError> { - use chia::protocol::CoinSpend; - - let puzzle_reveal = ctx - .serialize(&singleton_puzzle) - .map_err(|e| DriverError::Serialize(format!("{:?}", e)))?; - let solution = ctx - .serialize(&singleton_solution) - .map_err(|e| DriverError::Serialize(format!("{:?}", e)))?; - - let coin_spend = CoinSpend::new(singleton_coin.clone(), puzzle_reveal, solution); - ctx.insert(coin_spend); - Ok(()) -} - -/// Launch a new singleton with the given inner puzzle hash -/// -/// For most use cases, prefer using `SingletonDriver::launch()` instead. -pub fn launch_singleton( - ctx: &mut SpendContext, - funding_coin: &Coin, - inner_puzzle_hash: Bytes32, - singleton_amount: u64, -) -> Result { - let launcher = Launcher::new(funding_coin.coin_id(), singleton_amount); - let launcher_id = launcher.coin().coin_id(); - - let (launcher_conditions, singleton_coin) = launcher - .spend(ctx, inner_puzzle_hash, ()) - .map_err(|e| DriverError::Launcher(format!("{:?}", e)))?; - - Ok(super::LaunchResult { - launcher_id, - coin: singleton_coin, - conditions: launcher_conditions, - }) -} +//! Singleton helper functions +//! +//! Standalone helper functions for common singleton operations. + +use chia::protocol::{Bytes32, Coin}; +use chia::puzzles::singleton::SingletonArgs; +use chia::puzzles::{EveProof, LineageProof, Proof}; +use chia_wallet_sdk::driver::{Launcher, SpendContext}; +use chia_wallet_sdk::types::Conditions; +use clvm_utils::TreeHash; +use clvmr::NodePtr; + +use super::driver::SINGLETON_LAUNCHER_PUZZLE_HASH; +use crate::DriverError; + +// ============================================================================ +// Proof Creation (for backward compatibility) +// ============================================================================ + +/// Create an eve proof for the first singleton spend (after launch) +pub fn create_eve_proof(launcher_parent_coin_id: Bytes32, singleton_amount: u64) -> Proof { + Proof::Eve(EveProof { + parent_parent_coin_info: launcher_parent_coin_id, + parent_amount: singleton_amount, + }) +} + +/// Create a lineage proof for subsequent singleton spends +pub fn create_lineage_proof(parent_coin: &Coin, parent_inner_puzzle_hash: TreeHash) -> Proof { + Proof::Lineage(LineageProof { + parent_parent_coin_info: parent_coin.parent_coin_info, + parent_inner_puzzle_hash: parent_inner_puzzle_hash.into(), + parent_amount: parent_coin.amount, + }) +} + +// ============================================================================ +// Puzzle Hash Computation +// ============================================================================ + +/// Compute the full singleton puzzle hash given launcher_id and inner puzzle hash +pub fn singleton_puzzle_hash(launcher_id: Bytes32, inner_puzzle_hash: TreeHash) -> Bytes32 { + SingletonArgs::curry_tree_hash(launcher_id, inner_puzzle_hash).into() +} + +/// Compute the puzzle hash for a child singleton given its launcher ID and inner puzzle hash +pub fn child_singleton_puzzle_hash( + child_launcher_id: Bytes32, + child_inner_puzzle_hash: TreeHash, +) -> Bytes32 { + SingletonArgs::curry_tree_hash(child_launcher_id, child_inner_puzzle_hash).into() +} + +/// Compute the expected child launcher ID given the parent singleton coin ID +pub fn expected_child_launcher_id(parent_singleton_coin_id: Bytes32) -> Bytes32 { + Coin::new( + parent_singleton_coin_id, + Bytes32::new(SINGLETON_LAUNCHER_PUZZLE_HASH), + 0, + ) + .coin_id() +} + +// ============================================================================ +// Child Singleton Spawning +// ============================================================================ + +/// Result of spawning a child singleton via ephemeral launcher +#[derive(Debug, Clone)] +pub struct ChildLaunchResult { + pub child_launcher_id: Bytes32, + pub child_singleton: Coin, +} + +/// Spawn a child singleton via ephemeral (0-amount) launcher. +/// +/// The launcher coin is parented by the parent singleton coin. +/// This is used when an action emits a child singleton. +pub fn spawn_child_singleton( + ctx: &mut SpendContext, + parent_coin_id: Bytes32, + child_inner_puzzle_hash: TreeHash, +) -> Result { + // Child launcher coin (ephemeral, 0-amount) + let child_launcher_coin = Coin::new( + parent_coin_id, + Bytes32::new(SINGLETON_LAUNCHER_PUZZLE_HASH), + 0, + ); + let child_launcher_id = child_launcher_coin.coin_id(); + + // Spend the launcher to create the child singleton + let (_child_conds, child_singleton_info) = + Launcher::from_coin(child_launcher_coin, Conditions::new()) + .with_singleton_amount(1) + .mint_vault(ctx, child_inner_puzzle_hash, ()) + .map_err(|e| DriverError::Launcher(format!("child spawn: {:?}", e)))?; + + Ok(ChildLaunchResult { + child_launcher_id, + child_singleton: child_singleton_info.coin, + }) +} + +// ============================================================================ +// Low-Level Puzzle Building (for advanced use cases) +// ============================================================================ + +use chia::puzzles::singleton::{SingletonSolution, SingletonStruct}; +use clvm_utils::CurriedProgram; + +/// Build a singleton puzzle NodePtr +pub fn build_singleton_puzzle( + ctx: &mut SpendContext, + launcher_id: Bytes32, + inner_puzzle: NodePtr, +) -> Result { + let singleton_mod_hash = TreeHash::new(chia_puzzles::SINGLETON_TOP_LAYER_V1_1_HASH); + let singleton_ptr = ctx + .puzzle(singleton_mod_hash, &chia_puzzles::SINGLETON_TOP_LAYER_V1_1) + .map_err(|e| DriverError::PuzzleLoad(format!("singleton: {:?}", e)))?; + + ctx.alloc(&CurriedProgram { + program: singleton_ptr, + args: SingletonArgs { + singleton_struct: SingletonStruct::new(launcher_id), + inner_puzzle, + }, + }) + .map_err(|e| DriverError::Alloc(format!("singleton curry: {:?}", e))) +} + +/// Build a singleton solution NodePtr +pub fn build_singleton_solution( + ctx: &mut SpendContext, + proof: Proof, + amount: u64, + inner_solution: NodePtr, +) -> Result { + ctx.alloc(&SingletonSolution { + lineage_proof: proof, + amount, + inner_solution, + }) + .map_err(|e| DriverError::Alloc(format!("singleton solution: {:?}", e))) +} + +/// Create and insert a singleton coin spend +pub fn create_singleton_coin_spend( + ctx: &mut SpendContext, + singleton_coin: &Coin, + singleton_puzzle: NodePtr, + singleton_solution: NodePtr, +) -> Result<(), DriverError> { + use chia::protocol::CoinSpend; + + let puzzle_reveal = ctx + .serialize(&singleton_puzzle) + .map_err(|e| DriverError::Serialize(format!("{:?}", e)))?; + let solution = ctx + .serialize(&singleton_solution) + .map_err(|e| DriverError::Serialize(format!("{:?}", e)))?; + + let coin_spend = CoinSpend::new(*singleton_coin, puzzle_reveal, solution); + ctx.insert(coin_spend); + Ok(()) +} + +/// Launch a new singleton with the given inner puzzle hash +/// +/// For most use cases, prefer using `SingletonDriver::launch()` instead. +pub fn launch_singleton( + ctx: &mut SpendContext, + funding_coin: &Coin, + inner_puzzle_hash: Bytes32, + singleton_amount: u64, +) -> Result { + let launcher = Launcher::new(funding_coin.coin_id(), singleton_amount); + let launcher_id = launcher.coin().coin_id(); + + let (launcher_conditions, singleton_coin) = launcher + .spend(ctx, inner_puzzle_hash, ()) + .map_err(|e| DriverError::Launcher(format!("{:?}", e)))?; + + Ok(super::LaunchResult { + launcher_id, + coin: singleton_coin, + conditions: launcher_conditions, + }) +} diff --git a/src/singleton/mod.rs b/src/singleton/mod.rs index 191e2ad..0c116de 100644 --- a/src/singleton/mod.rs +++ b/src/singleton/mod.rs @@ -1,48 +1,48 @@ -//! Singleton management for Action Layer singletons -//! -//! This module provides the core infrastructure for working with CHIP-0050 -//! Action Layer singletons: -//! -//! - [`SingletonDriver`] - Core driver for singleton lifecycle management -//! - [`SingletonCoin`] - Tracks an on-chain singleton -//! - [`SingletonLineage`] - Lineage information for proof generation -//! - [`SpendOptions`] - Options for spend bundle handling and broadcast -//! -//! # Helper Functions -//! -//! For backward compatibility and advanced use cases, standalone helper functions -//! are also available: -//! - [`create_eve_proof`], [`create_lineage_proof`] - Proof creation -//! - [`spawn_child_singleton`] - Child singleton spawning -//! - [`singleton_puzzle_hash`], [`child_singleton_puzzle_hash`] - Hash computation - -mod driver; -mod helpers; -mod spend_options; -mod types; - -// Core driver and types -pub use driver::{SingletonDriver, SINGLETON_LAUNCHER_PUZZLE_HASH}; -pub use types::{ - ActionSpendResult, LaunchResult, Melted, NoOutput, SingletonCoin, SingletonLineage, -}; -pub use spend_options::{FeeOptions, SpendOptions}; - -// Helper functions (for backward compatibility and advanced use) -pub use helpers::{ - // Proof creation - create_eve_proof, - create_lineage_proof, - // Puzzle hash computation - child_singleton_puzzle_hash, - expected_child_launcher_id, - singleton_puzzle_hash, - // Child spawning - spawn_child_singleton, - ChildLaunchResult, - // Low-level puzzle building - build_singleton_puzzle, - build_singleton_solution, - create_singleton_coin_spend, - launch_singleton, -}; +//! Singleton management for Action Layer singletons +//! +//! This module provides the core infrastructure for working with CHIP-0050 +//! Action Layer singletons: +//! +//! - [`SingletonDriver`] - Core driver for singleton lifecycle management +//! - [`SingletonCoin`] - Tracks an on-chain singleton +//! - [`SingletonLineage`] - Lineage information for proof generation +//! - [`SpendOptions`] - Options for spend bundle handling and broadcast +//! +//! # Helper Functions +//! +//! For backward compatibility and advanced use cases, standalone helper functions +//! are also available: +//! - [`create_eve_proof`], [`create_lineage_proof`] - Proof creation +//! - [`spawn_child_singleton`] - Child singleton spawning +//! - [`singleton_puzzle_hash`], [`child_singleton_puzzle_hash`] - Hash computation + +mod driver; +mod helpers; +mod spend_options; +mod types; + +// Core driver and types +pub use driver::{SingletonDriver, SINGLETON_LAUNCHER_PUZZLE_HASH}; +pub use spend_options::{FeeOptions, SpendOptions}; +pub use types::{ + ActionSpendResult, LaunchResult, Melted, NoOutput, SingletonCoin, SingletonLineage, +}; + +// Helper functions (for backward compatibility and advanced use) +pub use helpers::{ + // Low-level puzzle building + build_singleton_puzzle, + build_singleton_solution, + // Puzzle hash computation + child_singleton_puzzle_hash, + // Proof creation + create_eve_proof, + create_lineage_proof, + create_singleton_coin_spend, + expected_child_launcher_id, + launch_singleton, + singleton_puzzle_hash, + // Child spawning + spawn_child_singleton, + ChildLaunchResult, +}; diff --git a/src/singleton/types.rs b/src/singleton/types.rs index ad6bc4c..2c9c406 100644 --- a/src/singleton/types.rs +++ b/src/singleton/types.rs @@ -1,148 +1,150 @@ -//! Core types for singleton management - -use chia::protocol::{Bytes32, Coin}; -use chia::puzzles::{EveProof, LineageProof, Proof}; -use clvm_utils::TreeHash; - -/// Lineage information for generating proofs -#[derive(Debug, Clone)] -pub enum SingletonLineage { - /// First spend after launch (eve proof) - Eve { - /// The parent coin ID of the launcher (funding coin ID) - launcher_parent_id: Bytes32, - /// The singleton amount - amount: u64, - }, - /// Subsequent spends (lineage proof) - Lineage { - /// The parent singleton coin - parent_coin: Coin, - /// The parent's inner puzzle hash - parent_inner_hash: TreeHash, - }, -} - -impl SingletonLineage { - /// Convert to a Proof for use in singleton solutions - pub fn to_proof(&self) -> Proof { - match self { - SingletonLineage::Eve { launcher_parent_id, amount } => { - Proof::Eve(EveProof { - parent_parent_coin_info: *launcher_parent_id, - parent_amount: *amount, - }) - } - SingletonLineage::Lineage { parent_coin, parent_inner_hash } => { - Proof::Lineage(LineageProof { - parent_parent_coin_info: parent_coin.parent_coin_info, - parent_inner_puzzle_hash: (*parent_inner_hash).into(), - parent_amount: parent_coin.amount, - }) - } - } - } - - /// Create eve lineage from funding coin info - pub fn eve(funding_coin_id: Bytes32, singleton_amount: u64) -> Self { - SingletonLineage::Eve { - launcher_parent_id: funding_coin_id, - amount: singleton_amount, - } - } - - /// Create lineage proof from parent coin and inner hash - pub fn lineage(parent_coin: Coin, parent_inner_hash: TreeHash) -> Self { - SingletonLineage::Lineage { - parent_coin, - parent_inner_hash, - } - } -} - -/// Tracks an on-chain singleton's current state -#[derive(Debug, Clone)] -pub struct SingletonCoin { - /// The launcher ID (singleton identity) - pub launcher_id: Bytes32, - /// The current unspent coin - pub coin: Coin, - /// Lineage for proof generation - pub lineage: SingletonLineage, -} - -impl SingletonCoin { - /// Create a new SingletonCoin - pub fn new(launcher_id: Bytes32, coin: Coin, lineage: SingletonLineage) -> Self { - Self { - launcher_id, - coin, - lineage, - } - } - - /// Get the proof for spending this singleton - pub fn proof(&self) -> Proof { - self.lineage.to_proof() - } - - /// Get the coin ID - pub fn coin_id(&self) -> Bytes32 { - self.coin.coin_id() - } -} - -/// Result of launching a singleton -#[derive(Debug, Clone)] -pub struct LaunchResult { - /// The launcher ID (singleton identity, same as network_id for network singletons) - pub launcher_id: Bytes32, - /// The newly created singleton coin - pub coin: Coin, - /// Conditions to include in the funding coin spend - pub conditions: chia_wallet_sdk::types::Conditions, -} - -/// Result of an action spend (generic over action-specific output) -#[derive(Debug, Clone)] -pub struct ActionSpendResult { - /// The recreated singleton coin (None if melted) - pub new_coin: Option, - /// New lineage for next spend (None if melted) - pub new_lineage: Option, - /// Action-specific output (child launcher IDs, etc.) - pub output: T, -} - -impl ActionSpendResult { - /// Create result for a normal action (singleton recreated) - pub fn normal(new_coin: Coin, new_lineage: SingletonLineage, output: T) -> Self { - Self { - new_coin: Some(new_coin), - new_lineage: Some(new_lineage), - output, - } - } - - /// Create result for a melt action (singleton destroyed) - pub fn melted(output: T) -> Self { - Self { - new_coin: None, - new_lineage: None, - output, - } - } - - /// Check if the singleton was melted - pub fn is_melted(&self) -> bool { - self.new_coin.is_none() - } -} - -/// Marker type for actions that produce no specific output -#[derive(Debug, Clone, Copy, Default)] -pub struct NoOutput; - -/// Marker type for actions that melt (destroy) the singleton -#[derive(Debug, Clone, Copy)] -pub struct Melted; +//! Core types for singleton management + +use chia::protocol::{Bytes32, Coin}; +use chia::puzzles::{EveProof, LineageProof, Proof}; +use clvm_utils::TreeHash; + +/// Lineage information for generating proofs +#[derive(Debug, Clone)] +pub enum SingletonLineage { + /// First spend after launch (eve proof) + Eve { + /// The parent coin ID of the launcher (funding coin ID) + launcher_parent_id: Bytes32, + /// The singleton amount + amount: u64, + }, + /// Subsequent spends (lineage proof) + Lineage { + /// The parent singleton coin + parent_coin: Coin, + /// The parent's inner puzzle hash + parent_inner_hash: TreeHash, + }, +} + +impl SingletonLineage { + /// Convert to a Proof for use in singleton solutions + pub fn to_proof(&self) -> Proof { + match self { + SingletonLineage::Eve { + launcher_parent_id, + amount, + } => Proof::Eve(EveProof { + parent_parent_coin_info: *launcher_parent_id, + parent_amount: *amount, + }), + SingletonLineage::Lineage { + parent_coin, + parent_inner_hash, + } => Proof::Lineage(LineageProof { + parent_parent_coin_info: parent_coin.parent_coin_info, + parent_inner_puzzle_hash: (*parent_inner_hash).into(), + parent_amount: parent_coin.amount, + }), + } + } + + /// Create eve lineage from funding coin info + pub fn eve(funding_coin_id: Bytes32, singleton_amount: u64) -> Self { + SingletonLineage::Eve { + launcher_parent_id: funding_coin_id, + amount: singleton_amount, + } + } + + /// Create lineage proof from parent coin and inner hash + pub fn lineage(parent_coin: Coin, parent_inner_hash: TreeHash) -> Self { + SingletonLineage::Lineage { + parent_coin, + parent_inner_hash, + } + } +} + +/// Tracks an on-chain singleton's current state +#[derive(Debug, Clone)] +pub struct SingletonCoin { + /// The launcher ID (singleton identity) + pub launcher_id: Bytes32, + /// The current unspent coin + pub coin: Coin, + /// Lineage for proof generation + pub lineage: SingletonLineage, +} + +impl SingletonCoin { + /// Create a new SingletonCoin + pub fn new(launcher_id: Bytes32, coin: Coin, lineage: SingletonLineage) -> Self { + Self { + launcher_id, + coin, + lineage, + } + } + + /// Get the proof for spending this singleton + pub fn proof(&self) -> Proof { + self.lineage.to_proof() + } + + /// Get the coin ID + pub fn coin_id(&self) -> Bytes32 { + self.coin.coin_id() + } +} + +/// Result of launching a singleton +#[derive(Debug, Clone)] +pub struct LaunchResult { + /// The launcher ID (singleton identity, same as network_id for network singletons) + pub launcher_id: Bytes32, + /// The newly created singleton coin + pub coin: Coin, + /// Conditions to include in the funding coin spend + pub conditions: chia_wallet_sdk::types::Conditions, +} + +/// Result of an action spend (generic over action-specific output) +#[derive(Debug, Clone)] +pub struct ActionSpendResult { + /// The recreated singleton coin (None if melted) + pub new_coin: Option, + /// New lineage for next spend (None if melted) + pub new_lineage: Option, + /// Action-specific output (child launcher IDs, etc.) + pub output: T, +} + +impl ActionSpendResult { + /// Create result for a normal action (singleton recreated) + pub fn normal(new_coin: Coin, new_lineage: SingletonLineage, output: T) -> Self { + Self { + new_coin: Some(new_coin), + new_lineage: Some(new_lineage), + output, + } + } + + /// Create result for a melt action (singleton destroyed) + pub fn melted(output: T) -> Self { + Self { + new_coin: None, + new_lineage: None, + output, + } + } + + /// Check if the singleton was melted + pub fn is_melted(&self) -> bool { + self.new_coin.is_none() + } +} + +/// Marker type for actions that produce no specific output +#[derive(Debug, Clone, Copy, Default)] +pub struct NoOutput; + +/// Marker type for actions that melt (destroy) the singleton +#[derive(Debug, Clone, Copy)] +pub struct Melted;