From 50725fad37be1c874adef4028cfa0ca4039e41bf Mon Sep 17 00:00:00 2001 From: William Wills Date: Fri, 5 Dec 2025 13:12:52 -0500 Subject: [PATCH 1/4] feat: dedicated collateral coin utility feat: dedicated dig coin utility feat: error handling improvements --- src/dig_coin.rs | 88 ++++++++++++ src/dig_collateral_coin.rs | 255 ++++++++++++++++++++++++++++++++ src/error.rs | 5 +- src/lib.rs | 23 +-- src/wallet.rs | 287 +++++-------------------------------- 5 files changed, 386 insertions(+), 272 deletions(-) create mode 100644 src/dig_coin.rs create mode 100644 src/dig_collateral_coin.rs diff --git a/src/dig_coin.rs b/src/dig_coin.rs new file mode 100644 index 0000000..b252574 --- /dev/null +++ b/src/dig_coin.rs @@ -0,0 +1,88 @@ +use crate::error::WalletError; +use crate::wallet::DIG_ASSET_ID; +use crate::{Bytes32, Coin, Peer}; +use chia::protocol::CoinState; +use chia::puzzles::cat::CatArgs; +use chia_wallet_sdk::driver::{Asset, Cat, Puzzle, SpendContext}; +use chia_wallet_sdk::prelude::{TreeHash, MAINNET_CONSTANTS}; + +pub struct DigCoin { + cat: Cat, +} + +impl DigCoin { + #[inline] + pub fn cat(&self) -> Cat { + self.cat + } + + pub fn puzzle_hash(wallet_puzzle_hash: Bytes32) -> Bytes32 { + let ph_bytes = + CatArgs::curry_tree_hash(DIG_ASSET_ID, TreeHash::from(wallet_puzzle_hash)).to_bytes(); + Bytes32::from(ph_bytes) + } + + pub async fn from_coin_state(peer: &Peer, coin_state: &CoinState) -> Result { + let coin_created_height = coin_state.created_height.ok_or(WalletError::Parse( + "Cannot determine coin creation height".to_string(), + ))?; + Self::from_coin(peer, &coin_state.coin, coin_created_height).await + } + + /// Function to validate that a coin is a $DIG CAT coin. Returns an instantiated DIG token + /// CAT for the coin if it's a valid $DIG CAT + pub async fn from_coin( + peer: &Peer, + coin: &Coin, + coin_created_height: u32, + ) -> Result { + let mut ctx = SpendContext::new(); + + // 1) Request parent coin state + let parent_state_response = peer + .request_coin_state( + vec![coin.parent_coin_info], + None, + MAINNET_CONSTANTS.genesis_challenge, + false, + ) + .await?; + + let parent_state = parent_state_response.map_err(|_| WalletError::RejectCoinState)?; + + // 2) Request parent puzzle and solution + let parent_puzzle_and_solution_response = peer + .request_puzzle_and_solution(parent_state.coin_ids[0], coin_created_height) + .await?; + + let parent_puzzle_and_solution = + parent_puzzle_and_solution_response.map_err(|_| WalletError::RejectPuzzleSolution)?; + + // 3) Convert puzzle to CLVM + let parent_puzzle_ptr = ctx.alloc(&parent_puzzle_and_solution.puzzle)?; + let parent_puzzle = Puzzle::parse(&ctx, parent_puzzle_ptr); + + // 4) Convert solution to CLVM + let parent_solution = ctx.alloc(&parent_puzzle_and_solution.solution)?; + + // 5) Parse CAT + let parsed_children = Cat::parse_children( + &mut ctx, + parent_state.coin_states[0].coin, + parent_puzzle, + parent_solution, + )? + .ok_or(WalletError::UnknownCoin)?; + + let proved_cat = parsed_children + .into_iter() + .find(|parsed_child| { + parsed_child.coin_id() == coin.coin_id() + && parsed_child.lineage_proof.is_some() + && parsed_child.info.asset_id == DIG_ASSET_ID + }) + .ok_or_else(|| WalletError::UnknownCoin)?; + + Ok(Self { cat: proved_cat }) + } +} diff --git a/src/dig_collateral_coin.rs b/src/dig_collateral_coin.rs new file mode 100644 index 0000000..70f572b --- /dev/null +++ b/src/dig_collateral_coin.rs @@ -0,0 +1,255 @@ +use crate::dig_coin::DigCoin; +use crate::error::WalletError; +use crate::wallet::DIG_ASSET_ID; +use crate::{Bytes, Bytes32, Coin, CoinSpend, CoinState, P2ParentCoin, Peer, PublicKey}; +use chia::puzzles::Memos; +use chia::traits::Streamable; +use chia_wallet_sdk::driver::{ + Action, Id, Puzzle, Relation, SpendContext, SpendWithConditions, Spends, StandardLayer, +}; +use chia_wallet_sdk::prelude::{AssertConcurrentSpend, Conditions, ToTreeHash, MAINNET_CONSTANTS}; +use clvm_traits::{FromClvm, ToClvm}; +use clvmr::Allocator; +use indexmap::indexmap; +use num_bigint::BigInt; + +pub struct DigCollateralCoin { + inner: P2ParentCoin, + morphed_store_id: Option, + mirror_urls: Option>, +} + +impl DigCollateralCoin { + /// Morphs a DIG store launcher ID into the DIG store collateral coin namespace. + pub fn morph_store_launcher_if_for_collateral(store_launcher_id: Bytes32) -> Bytes32 { + (store_launcher_id, "DIG_STORE_COLLATERAL") + .tree_hash() + .into() + } + + /// Morphs a DIG store launcher ID into the DIG mirror collateral coin namespace. + pub fn morph_store_launcher_id_for_mirror( + store_launcher_id: Bytes32, + offset: &BigInt, + ) -> Bytes32 { + let launcher_id_int = BigInt::from_signed_bytes_be(&store_launcher_id); + let offset_launcher_id = launcher_id_int + offset; + + (offset_launcher_id, "DIG_STORE_MIRROR_COLLATERAL") + .tree_hash() + .into() + } + + /// Instantiates a $DIG collateral coin + /// Verifies that coin is unspent and locked by the $DIG P2Parent puzzle + pub async fn from_coin_state(peer: &Peer, coin_state: CoinState) -> Result { + let coin = coin_state.coin; + + // verify coin is unspent + if matches!(coin_state.spent_height, Some(x) if x != 0) { + return Err(WalletError::CoinIsAlreadySpent); + } + + // verify that the coin is $DIG p2 parent + let p2_parent_hash = P2ParentCoin::puzzle_hash(Some(DIG_ASSET_ID)); + if coin.puzzle_hash != p2_parent_hash.into() { + return Err(WalletError::PuzzleHashMismatch(format!( + "Coin {} is not locked by the $DIG collateral puzzle", + coin.coin_id() + ))); + } + + let Some(created_height) = coin_state.created_height else { + return Err(WalletError::UnknownCoin); + }; + + let parent_state = peer + .request_coin_state( + vec![coin.parent_coin_info], + None, + MAINNET_CONSTANTS.genesis_challenge, + false, + ) + .await? + .map_err(|_| WalletError::RejectCoinState)? + .coin_states + .first() + .copied() + .ok_or(WalletError::UnknownCoin)?; + + let parent_puzzle_and_solution_response = peer + .request_puzzle_and_solution(coin.parent_coin_info, created_height) + .await? + .map_err(|_| WalletError::RejectPuzzleSolution)?; + + let mut allocator = Allocator::new(); + let parent_puzzle_ptr = parent_puzzle_and_solution_response + .puzzle + .to_clvm(&mut allocator)?; + let parent_solution_ptr = parent_puzzle_and_solution_response + .solution + .to_clvm(&mut allocator)?; + + let parent_puzzle = Puzzle::parse(&allocator, parent_puzzle_ptr); + + let (p2_parent, memos) = P2ParentCoin::parse_child( + &mut allocator, + parent_state.coin, + parent_puzzle, + parent_solution_ptr, + )? + .ok_or(WalletError::Parse( + "Failed to instantiate from parent state".to_string(), + ))?; + + let memos_vec = match memos { + Memos::Some(node) => Vec::::from_clvm(&mut allocator, node) + .ok() + .unwrap_or_default(), + Memos::None => Vec::new(), + }; + + let morphed_store_id: Option = if memos_vec.is_empty() { + None + } else { + Bytes32::from_bytes(&memos_vec[0]).ok() + }; + + let mut mirror_urls_vec = Vec::new(); + for i in 1..memos_vec.len() { + if let Ok(url_string) = String::from_utf8(memos_vec[i].to_vec()) { + mirror_urls_vec.push(url_string); + } + } + + let mirror_urls = if mirror_urls_vec.is_empty() { + None + } else { + Some(mirror_urls_vec) + }; + + Ok(Self { + inner: p2_parent, + morphed_store_id, + mirror_urls, + }) + } + + /// Uses the specified $DIG to create a collateral coin for the provided DIG store ID (launcher ID) + pub fn create( + dig_coins: Vec, + collateral_amount: u64, + store_id: Bytes32, + mirror_urls: Option>, + synthetic_key: PublicKey, + fee_coins: Vec, + fee: u64, + ) -> Result, WalletError> { + let p2_parent_inner_hash = P2ParentCoin::inner_puzzle_hash(Some(DIG_ASSET_ID)); + + let mut ctx = SpendContext::new(); + + let morphed_store_id = Self::morph_store_launcher_if_for_collateral(store_id); + + let memos = match mirror_urls { + Some(urls) => { + let mut memos_vec = Vec::with_capacity(urls.len() + 1); + memos_vec.push(morphed_store_id.to_vec()); + + for url in &urls { + memos_vec.push(url.as_bytes().to_vec()); + } + + let memos_node_ptr = ctx.alloc(&memos_vec)?; + Memos::Some(memos_node_ptr) + } + None => ctx.hint(morphed_store_id)?, + }; + + let actions = [ + Action::fee(fee), + Action::send( + Id::Existing(DIG_ASSET_ID), + p2_parent_inner_hash.into(), + collateral_amount, + memos, + ), + ]; + + let p2_layer = StandardLayer::new(synthetic_key); + let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into(); + let mut spends = Spends::new(p2_puzzle_hash); + + // add collateral coins to spends + for dig_coin in dig_coins { + spends.add(dig_coin.cat()); + } + + // add fee coins to spends + for fee_xch_coin in fee_coins { + spends.add(fee_xch_coin); + } + + let deltas = spends.apply(&mut ctx, &actions)?; + let index_map = indexmap! {p2_puzzle_hash => synthetic_key}; + + let _outputs = + spends.finish_with_keys(&mut ctx, &deltas, Relation::AssertConcurrent, &index_map)?; + + Ok(ctx.take()) + } + + /// Builds the spend bundle for spending the $DIG collateral coin to de-collateralize + /// the store and return spendable $DIG to the wallet that created the collateral coin. + pub fn spend( + &self, + synthetic_key: PublicKey, + fee_coins: Vec, + fee: u64, + ) -> Result, WalletError> { + let p2_layer = StandardLayer::new(synthetic_key); + let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into(); + + if p2_puzzle_hash != self.inner.proof.parent_inner_puzzle_hash { + return Err(WalletError::PuzzleHashMismatch( + "This coin is not owned by this wallet".to_string(), + )); + } + + let collateral_spend_conditions = + Conditions::new().create_coin(p2_puzzle_hash, self.inner.coin.amount, Memos::None); + + let mut ctx = SpendContext::new(); + + // add the collateral p2 parent spend to the spend context + let p2_delegated_spend = + p2_layer.spend_with_conditions(&mut ctx, collateral_spend_conditions)?; + + self.inner.spend(&mut ctx, p2_delegated_spend, ())?; + + // use actions and spends to attach fee to transaction and generate change + let actions = [Action::fee(fee)]; + let mut fee_spends = Spends::new(p2_puzzle_hash); + fee_spends + .conditions + .required + .push(AssertConcurrentSpend::new(self.inner.coin.coin_id())); + + // add fee coins to spends + for fee_xch_coin in fee_coins { + fee_spends.add(fee_xch_coin); + } + + let deltas = fee_spends.apply(&mut ctx, &actions)?; + let index_map = indexmap! {p2_puzzle_hash => synthetic_key}; + + let _outputs = fee_spends.finish_with_keys( + &mut ctx, + &deltas, + Relation::AssertConcurrent, + &index_map, + )?; + + Ok(ctx.take()) + } +} diff --git a/src/error.rs b/src/error.rs index 1de55e8..d00568d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -23,14 +23,15 @@ pub enum WalletError { #[error("{0:?}")] Driver(#[from] DriverError), - #[error("ParseError")] - Parse, + #[error("ParseError: {0}")] + Parse(String), #[error("UnknownCoin")] UnknownCoin, #[error("Clvm error")] Clvm, + #[error("ToClvm error: {0}")] ToClvm(#[from] chia::clvm_traits::ToClvmError), diff --git a/src/lib.rs b/src/lib.rs index 487a5b9..13a8b1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,8 @@ pub use async_api::{connect_peer, connect_random, create_tls_connector, NetworkT pub use constants::{get_mainnet_genesis_challenge, get_testnet11_genesis_challenge}; // Internal modules +mod dig_coin; +mod dig_collateral_coin; mod error; pub mod types; pub mod wallet; @@ -47,6 +49,7 @@ pub use wallet::{ SyncStoreResponse, TargetNetwork, }; pub use xch_server_coin::{morph_launcher_id, XchServerCoin}; +pub use {dig_coin::DigCoin, dig_collateral_coin::DigCollateralCoin}; use hex_literal::hex; @@ -64,15 +67,6 @@ pub const DIG_MIN_HEIGHT_HEADER_HASH: Bytes32 = Bytes32::new(hex!( "b29a4daac2434fd17a36e15ba1aac5d65012d4a66f99bed0bf2b5342e92e562c" )); -pub const DIG_STORE_LAUNCHER_ID_MORPH: &str = "DIG_STORE"; - -/// Morphs a DIG store launcher ID into the DIG namespace. Store launcher IDs should be morphed when hinted on coins -pub fn morph_store_launcher_id(store_launcher_id: Bytes32) -> Bytes32 { - (store_launcher_id, DIG_STORE_LAUNCHER_ID_MORPH) - .tree_hash() - .into() -} - /// Converts a master public key to a wallet synthetic key. pub fn master_public_key_to_wallet_synthetic_key(public_key: &PublicKey) -> PublicKey { master_to_wallet_unhardened(public_key, 0).derive_synthetic() @@ -346,7 +340,6 @@ pub fn create_server_coin( /// Async functions for blockchain interaction (Rust API versions) pub mod async_api { use super::*; - use chia_wallet_sdk::prelude::Cat; use futures_util::stream::{FuturesUnordered, StreamExt}; use rand::seq::SliceRandom; use std::net::SocketAddr; @@ -629,16 +622,6 @@ pub mod async_api { ) -> Result { Ok(wallet::broadcast_spend_bundle(peer, spend_bundle).await?) } - - /// Utility function to validate that a coin is a $DIG CAT coin. Returns an instantiated Cat - /// utility for the coin if it's a valid $DIG CAT - pub async fn prove_dig_cat_coin( - peer: &Peer, - coin: &Coin, - coin_created_height: u32, - ) -> Result { - Ok(wallet::prove_dig_cat_coin(peer, coin, coin_created_height).await?) - } } /// Constants for different networks diff --git a/src/wallet.rs b/src/wallet.rs index f2a3570..6923058 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,12 +1,17 @@ #![allow(clippy::result_large_err)] -use indexmap::indexmap; use std::collections::HashMap; use std::time::{SystemTime, UNIX_EPOCH}; +// Import proof types from our own crate's rust module +use crate::error::WalletError; +pub use crate::types::{coin_records_to_states, SuccessResponse, XchServerCoin}; +use crate::types::{EveProof, LineageProof, Proof}; +use crate::xch_server_coin::{urls_from_conditions, MirrorArgs, MirrorSolution, NewXchServerCoin}; +use crate::{NetworkType, UnspentCoinStates}; use chia::bls::{sign, verify, PublicKey, SecretKey, Signature}; use chia::clvm_traits::{clvm_tuple, FromClvm, ToClvm}; -use chia::clvm_utils::{tree_hash, ToTreeHash}; +use chia::clvm_utils::tree_hash; use chia::consensus::consensus_constants::ConsensusConstants; use chia::consensus::flags::{DONT_VALIDATE_SIGNATURE, MEMPOOL_MODE}; use chia::consensus::owned_conditions::OwnedSpendBundleConditions; @@ -25,18 +30,10 @@ use chia::puzzles::{ use chia_puzzles::SINGLETON_LAUNCHER_HASH; use chia_wallet_sdk::client::Peer; use chia_wallet_sdk::driver::{ - get_merkle_tree, Action, Asset, Cat, DataStore, DataStoreMetadata, DelegatedPuzzle, Did, - DidInfo, DriverError, HashedPtr, Id, IntermediateLauncher, Launcher, Layer, NftMint, - OracleLayer, P2ParentCoin, Puzzle, Relation, SpendContext, SpendWithConditions, Spends, - StandardLayer, WriterLayer, + get_merkle_tree, Asset, DataStore, DataStoreMetadata, DelegatedPuzzle, Did, DidInfo, + DriverError, HashedPtr, IntermediateLauncher, Launcher, Layer, NftMint, OracleLayer, + SpendContext, SpendWithConditions, StandardLayer, WriterLayer, }; -use chia_wallet_sdk::prelude::AssertConcurrentSpend; -// Import proof types from our own crate's rust module -use crate::error::WalletError; -pub use crate::types::{coin_records_to_states, SuccessResponse, XchServerCoin}; -use crate::types::{EveProof, LineageProof, Proof}; -use crate::xch_server_coin::{urls_from_conditions, MirrorArgs, MirrorSolution, NewXchServerCoin}; -use crate::{morph_store_launcher_id, NetworkType, UnspentCoinStates}; use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature, SignerError}; use chia_wallet_sdk::types::{ announcement_id, @@ -72,70 +69,6 @@ pub async fn get_unspent_coin_states_by_hint( get_unspent_coin_states(peer, hint, None, header_hash, true).await } -/// Instantiates a $DIG collateral coin -/// Verifies that coin is unspent and locked by the $DIG P2Parent puzzle -pub async fn fetch_dig_collateral_coin( - peer: &Peer, - coin_state: CoinState, -) -> Result<(P2ParentCoin, Memos), WalletError> { - let coin = coin_state.coin; - - // verify coin is unspent - if matches!(coin_state.spent_height, Some(x) if x != 0) { - return Err(WalletError::CoinIsAlreadySpent); - } - - // verify that the coin is $DIG p2 parent - let p2_parent_hash = P2ParentCoin::puzzle_hash(Some(DIG_ASSET_ID)); - if coin.puzzle_hash != p2_parent_hash.into() { - return Err(WalletError::PuzzleHashMismatch(format!( - "Coin {} is not locked by the $DIG collateral puzzle", - coin.coin_id() - ))); - } - - let Some(created_height) = coin_state.created_height else { - return Err(WalletError::UnknownCoin); - }; - - // 1) Request parent coin state - let parent_state = peer - .request_coin_state( - vec![coin.parent_coin_info], - None, - MAINNET_CONSTANTS.genesis_challenge, - false, - ) - .await? - .map_err(|_| WalletError::RejectCoinState)? - .coin_states - .first() - .copied() - .ok_or(WalletError::UnknownCoin)?; - - let parent_puzzle_and_solution_response = peer - .request_puzzle_and_solution(coin.parent_coin_info, created_height) - .await? - .map_err(|_| WalletError::RejectPuzzleSolution)?; - - let mut allocator = Allocator::new(); - let parent_puzzle_ptr = parent_puzzle_and_solution_response - .puzzle - .to_clvm(&mut allocator)?; - let parent_solution_ptr = parent_puzzle_and_solution_response - .solution - .to_clvm(&mut allocator)?; - let parent_puzzle = Puzzle::parse(&allocator, parent_puzzle_ptr); - - P2ParentCoin::parse_child( - &mut allocator, - parent_state.coin, - parent_puzzle, - parent_solution_ptr, - )? - .ok_or(WalletError::Parse) -} - pub async fn get_unspent_coin_states( peer: &Peer, puzzle_hash: Bytes32, @@ -260,55 +193,6 @@ pub fn send_xch( Ok(ctx.take()) } -/// Uses the specified $DIG to create a collateral coin for the provided DIG store ID (launcher ID) -pub fn create_dig_collateral_coin( - dig_cats: Vec, - collateral_amount: u64, - store_id: Bytes32, - synthetic_key: PublicKey, - fee_coins: Vec, - fee: u64, -) -> Result, WalletError> { - let p2_parent_inner_hash = P2ParentCoin::inner_puzzle_hash(Some(DIG_ASSET_ID)); - - let mut ctx = SpendContext::new(); - - let morphed_store_id = morph_store_launcher_id(store_id); - let hint = ctx.hint(morphed_store_id)?; - - let actions = [ - Action::fee(fee), - Action::send( - Id::Existing(DIG_ASSET_ID), - p2_parent_inner_hash.into(), - collateral_amount, - hint, - ), - ]; - - let p2_layer = StandardLayer::new(synthetic_key); - let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into(); - let mut spends = Spends::new(p2_puzzle_hash); - - // add collateral coins to spends - for cat in dig_cats { - spends.add(cat); - } - - // add fee coins to spends - for fee_xch_coin in fee_coins { - spends.add(fee_xch_coin); - } - - let deltas = spends.apply(&mut ctx, &actions)?; - let index_map = indexmap! {p2_puzzle_hash => synthetic_key}; - - let _outputs = - spends.finish_with_keys(&mut ctx, &deltas, Relation::AssertConcurrent, &index_map)?; - - Ok(ctx.take()) -} - pub fn create_server_coin( synthetic_key: PublicKey, selected_coins: Vec, @@ -363,54 +247,6 @@ pub fn create_server_coin( }) } -/// Spends the specified $DIG collateral coin to de-collateralize the store and return spendable -/// $DIG to the wallet that created the collateral coin. -pub fn spend_dig_collateral_coin( - synthetic_key: PublicKey, - fee_coins: Vec, - selected_collateral_coin: P2ParentCoin, - fee: u64, -) -> Result, WalletError> { - let mut ctx = SpendContext::new(); - let p2_layer = StandardLayer::new(synthetic_key); - let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into(); - - let collateral_spend_conditions = Conditions::new().create_coin( - p2_puzzle_hash, - selected_collateral_coin.coin.amount, - Memos::None, - ); - - // add the collateral p2 parent spend to the spend context - let p2_delegated_spend = - p2_layer.spend_with_conditions(&mut ctx, collateral_spend_conditions)?; - - selected_collateral_coin.spend(&mut ctx, p2_delegated_spend, ())?; - - // use actions and spends to attach fee to transaction and generate change - let actions = [Action::fee(fee)]; - let mut fee_spends = Spends::new(p2_puzzle_hash); - fee_spends - .conditions - .required - .push(AssertConcurrentSpend::new( - selected_collateral_coin.coin.coin_id(), - )); - - // add fee coins to spends - for fee_xch_coin in fee_coins { - fee_spends.add(fee_xch_coin); - } - - let deltas = fee_spends.apply(&mut ctx, &actions)?; - let index_map = indexmap! {p2_puzzle_hash => synthetic_key}; - - let _outputs = - fee_spends.finish_with_keys(&mut ctx, &deltas, Relation::AssertConcurrent, &index_map)?; - - Ok(ctx.take()) -} - pub async fn spend_xch_server_coins( peer: &Peer, synthetic_key: PublicKey, @@ -526,11 +362,15 @@ pub async fn fetch_xch_server_coin( }; let Ok(conditions) = Vec::::from_clvm(&allocator, output.1) else { - return Err(WalletError::Parse); + return Err(WalletError::Parse( + "Failed to get conditions from clvm allocator".to_string(), + )); }; let Some(urls) = urls_from_conditions(&allocator, &coin_state.coin, &conditions) else { - return Err(WalletError::Parse); + return Err(WalletError::Parse( + "Failed to get urls from conditions".to_string(), + )); }; let puzzle = spend @@ -687,9 +527,8 @@ pub async fn sync_store( &mut ctx, &cs, &latest_store.info.delegated_puzzles, - ) - .map_err(|_| WalletError::Parse)? - .ok_or(WalletError::Parse)?; + )? + .ok_or(WalletError::Parse("Store from spend is None".to_string()))?; if with_history { let resp: Result = peer @@ -775,9 +614,8 @@ pub async fn sync_store_using_launcher_id( solution: puzzle_and_solution_req.solution, }; - let first_store = DataStore::::from_spend(&mut ctx, &cs, &[]) - .map_err(|_| WalletError::Parse)? - .ok_or(WalletError::Parse)?; + let first_store = DataStore::::from_spend(&mut ctx, &cs, &[])? + .ok_or(WalletError::Parse("Store from spend is None".to_string()))?; let res = sync_store( peer, @@ -883,7 +721,7 @@ fn update_store_with_conditions( let new_datastore = DataStore::::from_spend(ctx, &new_spend, &parent_delegated_puzzles)? - .ok_or(WalletError::Parse)?; + .ok_or(WalletError::Parse("Store from spend is None".to_string()))?; Ok(SuccessResponse { coin_spends: vec![new_spend], @@ -1069,7 +907,7 @@ pub fn oracle_spend( let new_spend = datastore.spend(ctx, inner_datastore_spend)?; let new_datastore = DataStore::from_spend(ctx, &new_spend, &parent_delegated_puzzles)? - .ok_or(WalletError::Parse)?; + .ok_or(WalletError::Parse("Store from spend is None".to_string()))?; ctx.insert(new_spend.clone()); Ok(SuccessResponse { @@ -1342,62 +1180,6 @@ pub async fn look_up_possible_launchers( }) } -/// Utility function to validate that a coin is a $DIG CAT coin. Returns an instantiated Cat -/// utility for the coin if it's a valid $DIG CAT -pub async fn prove_dig_cat_coin( - peer: &Peer, - coin: &Coin, - coin_created_height: u32, -) -> Result { - let mut ctx = SpendContext::new(); - - // 1) Request parent coin state - let parent_state_response = peer - .request_coin_state( - vec![coin.parent_coin_info], - None, - MAINNET_CONSTANTS.genesis_challenge, - false, - ) - .await?; - - let parent_state = parent_state_response.map_err(|_| WalletError::RejectCoinState)?; - - // 2) Request parent puzzle and solution - let parent_puzzle_and_solution_response = peer - .request_puzzle_and_solution(parent_state.coin_ids[0], coin_created_height) - .await?; - - let parent_puzzle_and_solution = - parent_puzzle_and_solution_response.map_err(|_| WalletError::RejectPuzzleSolution)?; - - // 3) Convert puzzle to CLVM - let parent_puzzle_ptr = ctx.alloc(&parent_puzzle_and_solution.puzzle)?; - let parent_puzzle = Puzzle::parse(&ctx, parent_puzzle_ptr); - - // 4) Convert solution to CLVM - let parent_solution = ctx.alloc(&parent_puzzle_and_solution.solution)?; - - // 5) Parse CAT - let parsed_children = Cat::parse_children( - &mut ctx, - parent_state.coin_states[0].coin, - parent_puzzle, - parent_solution, - )? - .ok_or(WalletError::UnknownCoin)?; - - let proved_cat = parsed_children - .into_iter() - .find(|parsed_child| { - parsed_child.coin_id() == coin.coin_id() - && parsed_child.lineage_proof.is_some() - && parsed_child.info.asset_id == DIG_ASSET_ID - }) - .ok_or_else(|| WalletError::UnknownCoin)?; - Ok(proved_cat) -} - pub async fn subscribe_to_coin_states( peer: &Peer, coin_id: Bytes32, @@ -1517,7 +1299,7 @@ pub async fn mint_nft( let total_needed = fee + 1; // 1 mojo for the NFT if total_input < total_needed { - return Err(WalletError::Parse); // Not enough coins + return Err(WalletError::InsufficientCoinAmount); // Not enough coins } let _change = total_input - total_needed; @@ -1582,7 +1364,9 @@ pub fn generate_did_proof_manual( } // Lineage proof - subsequent spends Some(parent) => { - let parent_inner_puzzle_hash = parent_inner_puzzle_hash.ok_or(WalletError::Parse)?; // Need inner puzzle hash for lineage proof + let parent_inner_puzzle_hash = parent_inner_puzzle_hash.ok_or(WalletError::Parse( + "Parent inner puzzle hash is required".to_string(), + ))?; // Need inner puzzle hash for lineage proof Ok(chia::puzzles::Proof::Lineage(chia::puzzles::LineageProof { parent_parent_coin_info: parent.parent_coin_info, @@ -1678,7 +1462,7 @@ pub fn create_simple_did( let total_needed = fee + 1; // 1 mojo for the DID if total_input < total_needed { - return Err(WalletError::Parse); // Not enough coins + return Err(WalletError::InsufficientCoinAmount); // Not enough coins } let change = total_input - total_needed; @@ -1739,14 +1523,15 @@ pub async fn resolve_did_string_and_generate_proof( let parts: Vec<&str> = did_string.split(':').collect(); if parts.len() != 3 || parts[0] != "did" || parts[1] != "chia" { - return Err(WalletError::Parse); + return Err(WalletError::Parse("Invalid DID string".to_string())); } let bech32_part = parts[2]; // Decode the bech32 address to get the launcher ID use chia_wallet_sdk::utils::Address; - let address = Address::decode(bech32_part).map_err(|_| WalletError::Parse)?; + let address = Address::decode(bech32_part) + .map_err(|_| WalletError::Parse("Cannot decode address".to_string()))?; let did_id = address.puzzle_hash; @@ -1769,7 +1554,9 @@ pub async fn resolve_did_string_and_generate_proof( // Verify this is actually a launcher if launcher_state.coin.puzzle_hash != SINGLETON_LAUNCHER_HASH.into() { - return Err(WalletError::Parse); + return Err(WalletError::PuzzleHashMismatch( + "Coin puzzle hash does not match datastore singleton launcher hash".to_string(), + )); } // Get the spend of the launcher to find the first DID coin @@ -1798,7 +1585,7 @@ pub async fn resolve_did_string_and_generate_proof( .map_err(|_| WalletError::Clvm)?; let conditions = - Vec::::from_clvm(&allocator, output.1).map_err(|_| WalletError::Parse)?; + Vec::::from_clvm(&allocator, output.1).map_err(|_| WalletError::Clvm)?; // Find the CREATE_COIN condition to get the first DID coin let mut first_did_coin: Option = None; @@ -1816,7 +1603,7 @@ pub async fn resolve_did_string_and_generate_proof( } } - let first_did_coin = first_did_coin.ok_or(WalletError::Parse)?; + let first_did_coin = first_did_coin.ok_or(WalletError::UnknownCoin)?; // Now we need to trace the DID through all its spends to find the current coin let mut current_did_coin = first_did_coin; @@ -1865,7 +1652,7 @@ pub async fn resolve_did_string_and_generate_proof( .map_err(|_| WalletError::Clvm)?; let spend_conditions = Vec::::from_clvm(&allocator, spend_output.1) - .map_err(|_| WalletError::Parse)?; + .map_err(|_| WalletError::Clvm)?; // Find the CREATE_COIN condition for the child DID let mut child_did_coin: Option = None; @@ -1883,7 +1670,7 @@ pub async fn resolve_did_string_and_generate_proof( } } - current_did_coin = child_did_coin.ok_or(WalletError::Parse)?; + current_did_coin = child_did_coin.ok_or(WalletError::UnknownCoin)?; } // Now generate the proof for the current DID coin From ec857abd6d9c7ca719b20e9c4f9efed596ba5e45 Mon Sep 17 00:00:00 2001 From: William Wills Date: Fri, 5 Dec 2025 13:40:38 -0500 Subject: [PATCH 2/4] fix: cargo clippy WIP --- src/dig_collateral_coin.rs | 8 +++++--- src/lib.rs | 1 - src/wallet.rs | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/dig_collateral_coin.rs b/src/dig_collateral_coin.rs index 70f572b..85c98fb 100644 --- a/src/dig_collateral_coin.rs +++ b/src/dig_collateral_coin.rs @@ -15,7 +15,9 @@ use num_bigint::BigInt; pub struct DigCollateralCoin { inner: P2ParentCoin, + #[allow(dead_code)] morphed_store_id: Option, + #[allow(dead_code)] mirror_urls: Option>, } @@ -103,7 +105,7 @@ impl DigCollateralCoin { ))?; let memos_vec = match memos { - Memos::Some(node) => Vec::::from_clvm(&mut allocator, node) + Memos::Some(node) => Vec::::from_clvm(&allocator, node) .ok() .unwrap_or_default(), Memos::None => Vec::new(), @@ -116,8 +118,8 @@ impl DigCollateralCoin { }; let mut mirror_urls_vec = Vec::new(); - for i in 1..memos_vec.len() { - if let Ok(url_string) = String::from_utf8(memos_vec[i].to_vec()) { + for memo in memos_vec.iter().skip(1) { + if let Ok(url_string) = String::from_utf8(memo.to_vec()) { mirror_urls_vec.push(url_string); } } diff --git a/src/lib.rs b/src/lib.rs index 13a8b1b..a4c6f79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,6 @@ pub type Result = std::result::Result Date: Fri, 5 Dec 2025 14:05:29 -0500 Subject: [PATCH 3/4] fix: cargo clippy - allow large client error --- src/dig_collateral_coin.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dig_collateral_coin.rs b/src/dig_collateral_coin.rs index 85c98fb..7c00682 100644 --- a/src/dig_collateral_coin.rs +++ b/src/dig_collateral_coin.rs @@ -138,6 +138,7 @@ impl DigCollateralCoin { } /// Uses the specified $DIG to create a collateral coin for the provided DIG store ID (launcher ID) + #[allow(clippy::result_large_err)] pub fn create( dig_coins: Vec, collateral_amount: u64, @@ -203,6 +204,7 @@ impl DigCollateralCoin { /// Builds the spend bundle for spending the $DIG collateral coin to de-collateralize /// the store and return spendable $DIG to the wallet that created the collateral coin. + #[allow(clippy::result_large_err)] pub fn spend( &self, synthetic_key: PublicKey, From d0f7a99b42d1aaa961f50ccac416b4730e063e18 Mon Sep 17 00:00:00 2001 From: William Wills Date: Mon, 8 Dec 2025 17:38:40 -0500 Subject: [PATCH 4/4] feat: complete dig collateral coin utility --- src/dig_collateral_coin.rs | 151 +++++++++++++++++++++++++------------ 1 file changed, 101 insertions(+), 50 deletions(-) diff --git a/src/dig_collateral_coin.rs b/src/dig_collateral_coin.rs index 7c00682..232af49 100644 --- a/src/dig_collateral_coin.rs +++ b/src/dig_collateral_coin.rs @@ -1,7 +1,9 @@ use crate::dig_coin::DigCoin; use crate::error::WalletError; use crate::wallet::DIG_ASSET_ID; -use crate::{Bytes, Bytes32, Coin, CoinSpend, CoinState, P2ParentCoin, Peer, PublicKey}; +use crate::{ + Bytes, Bytes32, Coin, CoinSpend, CoinState, LineageProof, P2ParentCoin, Peer, PublicKey, +}; use chia::puzzles::Memos; use chia::traits::Streamable; use chia_wallet_sdk::driver::{ @@ -13,6 +15,7 @@ use clvmr::Allocator; use indexmap::indexmap; use num_bigint::BigInt; +#[derive(Debug, Clone)] pub struct DigCollateralCoin { inner: P2ParentCoin, #[allow(dead_code)] @@ -22,8 +25,16 @@ pub struct DigCollateralCoin { } impl DigCollateralCoin { + pub fn coin(&self) -> Coin { + self.inner.coin + } + + pub fn proof(&self) -> LineageProof { + self.inner.proof + } + /// Morphs a DIG store launcher ID into the DIG store collateral coin namespace. - pub fn morph_store_launcher_if_for_collateral(store_launcher_id: Bytes32) -> Bytes32 { + pub fn morph_store_launcher_id_for_collateral(store_launcher_id: Bytes32) -> Bytes32 { (store_launcher_id, "DIG_STORE_COLLATERAL") .tree_hash() .into() @@ -139,67 +150,62 @@ impl DigCollateralCoin { /// Uses the specified $DIG to create a collateral coin for the provided DIG store ID (launcher ID) #[allow(clippy::result_large_err)] - pub fn create( + pub fn create_collateral( dig_coins: Vec, - collateral_amount: u64, + amount: u64, store_id: Bytes32, - mirror_urls: Option>, synthetic_key: PublicKey, fee_coins: Vec, fee: u64, ) -> Result, WalletError> { - let p2_parent_inner_hash = P2ParentCoin::inner_puzzle_hash(Some(DIG_ASSET_ID)); - let mut ctx = SpendContext::new(); - let morphed_store_id = Self::morph_store_launcher_if_for_collateral(store_id); - - let memos = match mirror_urls { - Some(urls) => { - let mut memos_vec = Vec::with_capacity(urls.len() + 1); - memos_vec.push(morphed_store_id.to_vec()); - - for url in &urls { - memos_vec.push(url.as_bytes().to_vec()); - } - - let memos_node_ptr = ctx.alloc(&memos_vec)?; - Memos::Some(memos_node_ptr) - } - None => ctx.hint(morphed_store_id)?, - }; - - let actions = [ - Action::fee(fee), - Action::send( - Id::Existing(DIG_ASSET_ID), - p2_parent_inner_hash.into(), - collateral_amount, - memos, - ), - ]; + let morphed_store_id = Self::morph_store_launcher_id_for_collateral(store_id); + let hint = ctx.hint(morphed_store_id)?; - let p2_layer = StandardLayer::new(synthetic_key); - let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into(); - let mut spends = Spends::new(p2_puzzle_hash); + Self::build_coin_spends( + &mut ctx, + hint, + dig_coins, + amount, + synthetic_key, + fee_coins, + fee, + ) + } - // add collateral coins to spends - for dig_coin in dig_coins { - spends.add(dig_coin.cat()); - } + #[allow(clippy::result_large_err, clippy::too_many_arguments)] + pub fn create_mirror( + dig_coins: Vec, + amount: u64, + store_id: Bytes32, + mirror_urls: Vec, + epoch: BigInt, + synthetic_key: PublicKey, + fee_coins: Vec, + fee: u64, + ) -> Result, WalletError> { + let mut ctx = SpendContext::new(); + let morphed_store_id = Self::morph_store_launcher_id_for_mirror(store_id, &epoch); + let mut memos_vec = Vec::with_capacity(mirror_urls.len() + 1); + memos_vec.push(morphed_store_id.to_vec()); - // add fee coins to spends - for fee_xch_coin in fee_coins { - spends.add(fee_xch_coin); + for url in &mirror_urls { + memos_vec.push(url.as_bytes().to_vec()); } - let deltas = spends.apply(&mut ctx, &actions)?; - let index_map = indexmap! {p2_puzzle_hash => synthetic_key}; + let memos_node_ptr = ctx.alloc(&memos_vec)?; + let memos = Memos::Some(memos_node_ptr); - let _outputs = - spends.finish_with_keys(&mut ctx, &deltas, Relation::AssertConcurrent, &index_map)?; - - Ok(ctx.take()) + Self::build_coin_spends( + &mut ctx, + memos, + dig_coins, + amount, + synthetic_key, + fee_coins, + fee, + ) } /// Builds the spend bundle for spending the $DIG collateral coin to de-collateralize @@ -216,7 +222,7 @@ impl DigCollateralCoin { if p2_puzzle_hash != self.inner.proof.parent_inner_puzzle_hash { return Err(WalletError::PuzzleHashMismatch( - "This coin is not owned by this wallet".to_string(), + "Collateral coin controlled by another wallet".to_string(), )); } @@ -256,4 +262,49 @@ impl DigCollateralCoin { Ok(ctx.take()) } + + #[allow(clippy::result_large_err)] + fn build_coin_spends( + ctx: &mut SpendContext, + memos: Memos, + dig_coins: Vec, + amount: u64, + synthetic_key: PublicKey, + fee_coins: Vec, + fee: u64, + ) -> Result, WalletError> { + let p2_parent_inner_hash = P2ParentCoin::inner_puzzle_hash(Some(DIG_ASSET_ID)); + + let actions = [ + Action::fee(fee), + Action::send( + Id::Existing(DIG_ASSET_ID), + p2_parent_inner_hash.into(), + amount, + memos, + ), + ]; + + let p2_layer = StandardLayer::new(synthetic_key); + let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into(); + let mut spends = Spends::new(p2_puzzle_hash); + + // add collateral coins to spends + for dig_coin in dig_coins { + spends.add(dig_coin.cat()); + } + + // add fee coins to spends + for fee_xch_coin in fee_coins { + spends.add(fee_xch_coin); + } + + let deltas = spends.apply(ctx, &actions)?; + let index_map = indexmap! {p2_puzzle_hash => synthetic_key}; + + let _outputs = + spends.finish_with_keys(ctx, &deltas, Relation::AssertConcurrent, &index_map)?; + + Ok(ctx.take()) + } }