datalayer_driver/
wallet.rs

1#![allow(clippy::result_large_err)]
2
3use indexmap::indexmap;
4use std::collections::HashMap;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use chia::bls::{sign, verify, PublicKey, SecretKey, Signature};
8use chia::clvm_traits::{clvm_tuple, FromClvm, ToClvm};
9use chia::clvm_utils::{tree_hash, ToTreeHash};
10use chia::consensus::consensus_constants::ConsensusConstants;
11use chia::consensus::flags::{DONT_VALIDATE_SIGNATURE, MEMPOOL_MODE};
12use chia::consensus::owned_conditions::OwnedSpendBundleConditions;
13use chia::consensus::run_block_generator::run_block_generator;
14use chia::consensus::solution_generator::solution_generator;
15use chia::protocol::{
16    Bytes, Bytes32, Coin, CoinSpend, CoinState, CoinStateFilters, RejectHeaderRequest,
17    RequestBlockHeader, RequestFeeEstimates, RespondBlockHeader, RespondFeeEstimates, SpendBundle,
18    TransactionAck,
19};
20use chia::puzzles::{
21    nft::NftMetadata,
22    standard::{StandardArgs, StandardSolution},
23    DeriveSynthetic,
24};
25use chia_puzzles::SINGLETON_LAUNCHER_HASH;
26use chia_wallet_sdk::client::Peer;
27use chia_wallet_sdk::driver::{
28    get_merkle_tree, Action, Asset, Cat, DataStore, DataStoreMetadata, DelegatedPuzzle, Did,
29    DidInfo, DriverError, HashedPtr, Id, IntermediateLauncher, Launcher, Layer, NftMint,
30    OracleLayer, P2ParentCoin, Puzzle, Relation, SpendContext, SpendWithConditions, Spends,
31    StandardLayer, WriterLayer,
32};
33use chia_wallet_sdk::prelude::AssertConcurrentSpend;
34// Import proof types from our own crate's rust module
35use crate::error::WalletError;
36pub use crate::types::{coin_records_to_states, SuccessResponse, XchServerCoin};
37use crate::types::{EveProof, LineageProof, Proof};
38use crate::xch_server_coin::{urls_from_conditions, MirrorArgs, MirrorSolution, NewXchServerCoin};
39use crate::{morph_store_launcher_id, NetworkType, UnspentCoinStates};
40use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature, SignerError};
41use chia_wallet_sdk::types::{
42    announcement_id,
43    conditions::{CreateCoin, MeltSingleton, Memos, UpdateDataStoreMerkleRoot},
44    Condition, Conditions, MAINNET_CONSTANTS, TESTNET11_CONSTANTS,
45};
46use chia_wallet_sdk::utils::{self, CoinSelectionError};
47use clvmr::Allocator;
48use hex_literal::hex;
49
50/* echo -n 'datastore' | sha256sum */
51pub const DATASTORE_LAUNCHER_HINT: Bytes32 = Bytes32::new(hex!(
52    "
53    aa7e5b234e1d55967bf0a316395a2eab6cb3370332c0f251f0e44a5afb84fc68
54    "
55));
56
57pub const DIG_ASSET_ID: Bytes32 = Bytes32::new(hex!(
58    "a406d3a9de984d03c9591c10d917593b434d5263cabe2b42f6b367df16832f81"
59));
60
61pub const MAX_CLVM_COST: u64 = 11_000_000_000;
62
63pub async fn get_unspent_coin_states_by_hint(
64    peer: &Peer,
65    hint: Bytes32,
66    network_type: NetworkType,
67) -> Result<UnspentCoinStates, WalletError> {
68    let header_hash = match network_type {
69        NetworkType::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
70        NetworkType::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
71    };
72    get_unspent_coin_states(peer, hint, None, header_hash, true).await
73}
74
75/// Instantiates a $DIG collateral coin
76/// Verifies that coin is unspent and locked by the $DIG P2Parent puzzle
77pub async fn fetch_dig_collateral_coin(
78    peer: &Peer,
79    coin_state: CoinState,
80) -> Result<(P2ParentCoin, Memos), WalletError> {
81    let coin = coin_state.coin;
82
83    // verify coin is unspent
84    if matches!(coin_state.spent_height, Some(x) if x != 0) {
85        return Err(WalletError::CoinIsAlreadySpent);
86    }
87
88    // verify that the coin is $DIG p2 parent
89    let p2_parent_hash = P2ParentCoin::puzzle_hash(Some(DIG_ASSET_ID));
90    if coin.puzzle_hash != p2_parent_hash.into() {
91        return Err(WalletError::PuzzleHashMismatch(format!(
92            "Coin {} is not locked by the $DIG collateral puzzle",
93            coin.coin_id()
94        )));
95    }
96
97    let Some(created_height) = coin_state.created_height else {
98        return Err(WalletError::UnknownCoin);
99    };
100
101    // 1) Request parent coin state
102    let parent_state = peer
103        .request_coin_state(
104            vec![coin.parent_coin_info],
105            None,
106            MAINNET_CONSTANTS.genesis_challenge,
107            false,
108        )
109        .await?
110        .map_err(|_| WalletError::RejectCoinState)?
111        .coin_states
112        .first()
113        .copied()
114        .ok_or(WalletError::UnknownCoin)?;
115
116    let parent_puzzle_and_solution_response = peer
117        .request_puzzle_and_solution(coin.parent_coin_info, created_height)
118        .await?
119        .map_err(|_| WalletError::RejectPuzzleSolution)?;
120
121    let mut allocator = Allocator::new();
122    let parent_puzzle_ptr = parent_puzzle_and_solution_response
123        .puzzle
124        .to_clvm(&mut allocator)?;
125    let parent_solution_ptr = parent_puzzle_and_solution_response
126        .solution
127        .to_clvm(&mut allocator)?;
128    let parent_puzzle = Puzzle::parse(&allocator, parent_puzzle_ptr);
129
130    P2ParentCoin::parse_child(
131        &mut allocator,
132        parent_state.coin,
133        parent_puzzle,
134        parent_solution_ptr,
135    )?
136    .ok_or(WalletError::Parse)
137}
138
139pub async fn get_unspent_coin_states(
140    peer: &Peer,
141    puzzle_hash: Bytes32,
142    previous_height: Option<u32>,
143    previous_header_hash: Bytes32,
144    allow_hints: bool,
145) -> Result<UnspentCoinStates, WalletError> {
146    let mut coin_states = Vec::new();
147    let mut last_height = previous_height.unwrap_or_default();
148
149    let mut last_header_hash = previous_header_hash;
150
151    loop {
152        let response = peer
153            .request_puzzle_state(
154                vec![puzzle_hash],
155                if last_height == 0 {
156                    None
157                } else {
158                    Some(last_height)
159                },
160                last_header_hash,
161                CoinStateFilters {
162                    include_spent: false,
163                    include_unspent: true,
164                    include_hinted: allow_hints,
165                    min_amount: 1,
166                },
167                false,
168            )
169            .await
170            .map_err(WalletError::Client)?
171            .map_err(|_| WalletError::RejectPuzzleState)?;
172
173        last_height = response.height;
174        last_header_hash = response.header_hash;
175        coin_states.extend(
176            response
177                .coin_states
178                .into_iter()
179                .filter(|cs| cs.spent_height.is_none()),
180        );
181
182        if response.is_finished {
183            break;
184        }
185    }
186
187    Ok(UnspentCoinStates {
188        coin_states,
189        last_height,
190        last_header_hash,
191    })
192}
193
194pub fn select_coins(coins: Vec<Coin>, total_amount: u64) -> Result<Vec<Coin>, CoinSelectionError> {
195    utils::select_coins(coins.into_iter().collect(), total_amount)
196}
197
198fn spend_coins_together(
199    ctx: &mut SpendContext,
200    synthetic_key: PublicKey,
201    coins: &[Coin],
202    extra_conditions: Conditions,
203    output: i64,
204    change_puzzle_hash: Bytes32,
205) -> Result<(), WalletError> {
206    let p2 = StandardLayer::new(synthetic_key);
207
208    let change = i64::try_from(coins.iter().map(|coin| coin.amount).sum::<u64>()).unwrap() - output;
209    assert!(change >= 0);
210    let change = change as u64;
211
212    let first_coin_id = coins[0].coin_id();
213
214    for (i, &coin) in coins.iter().enumerate() {
215        if i == 0 {
216            let mut conditions = extra_conditions.clone();
217
218            if change > 0 {
219                conditions = conditions.create_coin(change_puzzle_hash, change, Memos::None);
220            }
221
222            p2.spend(ctx, coin, conditions)?;
223        } else {
224            p2.spend(
225                ctx,
226                coin,
227                Conditions::new().assert_concurrent_spend(first_coin_id),
228            )?;
229        }
230    }
231    Ok(())
232}
233
234pub fn send_xch(
235    synthetic_key: PublicKey,
236    coins: &[Coin],
237    outputs: &[(Bytes32, u64, Vec<Bytes>)],
238    fee: u64,
239) -> Result<Vec<CoinSpend>, WalletError> {
240    let mut ctx = SpendContext::new();
241
242    let mut conditions = Conditions::new().reserve_fee(fee);
243    let mut total_amount = fee;
244
245    for output in outputs {
246        let memos = ctx.alloc(&output.2)?;
247        conditions = conditions.create_coin(output.0, output.1, Memos::Some(memos));
248        total_amount += output.1;
249    }
250
251    spend_coins_together(
252        &mut ctx,
253        synthetic_key,
254        coins,
255        conditions,
256        total_amount.try_into().unwrap(),
257        StandardArgs::curry_tree_hash(synthetic_key).into(),
258    )?;
259
260    Ok(ctx.take())
261}
262
263/// Uses the specified $DIG to create a collateral coin for the provided DIG store ID (launcher ID)
264pub fn create_dig_collateral_coin(
265    dig_cats: Vec<Cat>,
266    collateral_amount: u64,
267    store_id: Bytes32,
268    synthetic_key: PublicKey,
269    fee_coins: Vec<Coin>,
270    fee: u64,
271) -> Result<Vec<CoinSpend>, WalletError> {
272    let p2_parent_inner_hash = P2ParentCoin::inner_puzzle_hash(Some(DIG_ASSET_ID));
273
274    let mut ctx = SpendContext::new();
275
276    let morphed_store_id = morph_store_launcher_id(store_id);
277    let hint = ctx.hint(morphed_store_id)?;
278
279    let actions = [
280        Action::fee(fee),
281        Action::send(
282            Id::Existing(DIG_ASSET_ID),
283            p2_parent_inner_hash.into(),
284            collateral_amount,
285            hint,
286        ),
287    ];
288
289    let p2_layer = StandardLayer::new(synthetic_key);
290    let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into();
291    let mut spends = Spends::new(p2_puzzle_hash);
292
293    // add collateral coins to spends
294    for cat in dig_cats {
295        spends.add(cat);
296    }
297
298    // add fee coins to spends
299    for fee_xch_coin in fee_coins {
300        spends.add(fee_xch_coin);
301    }
302
303    let deltas = spends.apply(&mut ctx, &actions)?;
304    let index_map = indexmap! {p2_puzzle_hash => synthetic_key};
305
306    let _outputs =
307        spends.finish_with_keys(&mut ctx, &deltas, Relation::AssertConcurrent, &index_map)?;
308
309    Ok(ctx.take())
310}
311
312pub fn create_server_coin(
313    synthetic_key: PublicKey,
314    selected_coins: Vec<Coin>,
315    hint: Bytes32,
316    uris: Vec<String>,
317    amount: u64,
318    fee: u64,
319) -> Result<NewXchServerCoin, WalletError> {
320    let puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into();
321
322    let mut memos = Vec::with_capacity(uris.len() + 1);
323    memos.push(hint.to_vec());
324
325    for url in &uris {
326        memos.push(url.as_bytes().to_vec());
327    }
328
329    let mut ctx = SpendContext::new();
330
331    let memos = ctx.alloc(&memos)?;
332
333    let conditions = Conditions::new()
334        .create_coin(
335            MirrorArgs::curry_tree_hash().into(),
336            amount,
337            Memos::Some(memos),
338        )
339        .reserve_fee(fee);
340
341    spend_coins_together(
342        &mut ctx,
343        synthetic_key,
344        &selected_coins,
345        conditions,
346        (amount + fee).try_into().unwrap(),
347        puzzle_hash,
348    )?;
349
350    let server_coin = XchServerCoin {
351        coin: Coin::new(
352            selected_coins[0].coin_id(),
353            MirrorArgs::curry_tree_hash().into(),
354            amount,
355        ),
356        p2_puzzle_hash: puzzle_hash,
357        memo_urls: uris,
358    };
359
360    Ok(NewXchServerCoin {
361        coin_spends: ctx.take(),
362        server_coin,
363    })
364}
365
366/// Spends the specified $DIG collateral coin to de-collateralize the store and return spendable
367/// $DIG to the wallet that created the collateral coin.
368pub fn spend_dig_collateral_coin(
369    synthetic_key: PublicKey,
370    fee_coins: Vec<Coin>,
371    selected_collateral_coin: P2ParentCoin,
372    fee: u64,
373) -> Result<Vec<CoinSpend>, WalletError> {
374    let mut ctx = SpendContext::new();
375    let p2_layer = StandardLayer::new(synthetic_key);
376    let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into();
377
378    let collateral_spend_conditions = Conditions::new().create_coin(
379        p2_puzzle_hash,
380        selected_collateral_coin.coin.amount,
381        Memos::None,
382    );
383
384    // add the collateral p2 parent spend to the spend context
385    let p2_delegated_spend =
386        p2_layer.spend_with_conditions(&mut ctx, collateral_spend_conditions)?;
387
388    selected_collateral_coin.spend(&mut ctx, p2_delegated_spend, ())?;
389
390    // use actions and spends to attach fee to transaction and generate change
391    let actions = [Action::fee(fee)];
392    let mut fee_spends = Spends::new(p2_puzzle_hash);
393    fee_spends
394        .conditions
395        .required
396        .push(AssertConcurrentSpend::new(
397            selected_collateral_coin.coin.coin_id(),
398        ));
399
400    // add fee coins to spends
401    for fee_xch_coin in fee_coins {
402        fee_spends.add(fee_xch_coin);
403    }
404
405    let deltas = fee_spends.apply(&mut ctx, &actions)?;
406    let index_map = indexmap! {p2_puzzle_hash => synthetic_key};
407
408    let _outputs =
409        fee_spends.finish_with_keys(&mut ctx, &deltas, Relation::AssertConcurrent, &index_map)?;
410
411    Ok(ctx.take())
412}
413
414pub async fn spend_xch_server_coins(
415    peer: &Peer,
416    synthetic_key: PublicKey,
417    selected_coins: Vec<Coin>,
418    total_fee: u64,
419    network: TargetNetwork,
420) -> Result<Vec<CoinSpend>, WalletError> {
421    let puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into();
422
423    let mut fee_coins = Vec::new();
424    let mut server_coins = Vec::new();
425
426    for coin in selected_coins {
427        if coin.puzzle_hash == puzzle_hash {
428            fee_coins.push(coin);
429        } else {
430            server_coins.push(coin);
431        }
432    }
433
434    if server_coins.is_empty() {
435        return Ok(Vec::new());
436    }
437
438    assert!(!fee_coins.is_empty());
439
440    let parent_coins = peer
441        .request_coin_state(
442            server_coins.iter().map(|sc| sc.parent_coin_info).collect(),
443            None,
444            match network {
445                TargetNetwork::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
446                TargetNetwork::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
447            },
448            false,
449        )
450        .await?
451        .map_err(|_| WalletError::RejectCoinState)?
452        .coin_states;
453
454    let mut ctx = SpendContext::new();
455
456    let puzzle_reveal = ctx.curry(MirrorArgs::default())?;
457
458    let mut conditions = Conditions::new().reserve_fee(total_fee);
459    let mut total_fee: i64 = total_fee.try_into().unwrap();
460
461    for server_coin in server_coins {
462        let parent_coin = parent_coins
463            .iter()
464            .find(|cs| cs.coin.coin_id() == server_coin.parent_coin_info)
465            .copied()
466            .ok_or(WalletError::UnknownCoin)?;
467
468        if parent_coin.coin.puzzle_hash != puzzle_hash {
469            return Err(WalletError::Permission);
470        }
471
472        let parent_inner_puzzle = ctx.curry(StandardArgs::new(synthetic_key))?;
473
474        let puzzle_reveal = ctx.serialize(&puzzle_reveal)?;
475
476        let solution = ctx.serialize(&MirrorSolution {
477            parent_parent_id: parent_coin.coin.parent_coin_info,
478            parent_inner_puzzle,
479            parent_amount: parent_coin.coin.amount,
480            parent_solution: StandardSolution {
481                original_public_key: None,
482                delegated_puzzle: (),
483                solution: (),
484            },
485        })?;
486
487        total_fee -= i64::try_from(server_coin.amount).unwrap();
488        ctx.insert(CoinSpend::new(server_coin, puzzle_reveal, solution));
489
490        conditions = conditions.assert_concurrent_spend(server_coin.coin_id());
491    }
492
493    spend_coins_together(
494        &mut ctx,
495        synthetic_key,
496        &fee_coins,
497        conditions,
498        total_fee,
499        puzzle_hash,
500    )?;
501
502    Ok(ctx.take())
503}
504
505pub async fn fetch_xch_server_coin(
506    peer: &Peer,
507    coin_state: CoinState,
508    max_cost: u64,
509) -> Result<XchServerCoin, WalletError> {
510    let Some(created_height) = coin_state.created_height else {
511        return Err(WalletError::UnknownCoin);
512    };
513
514    let spend = peer
515        .request_puzzle_and_solution(coin_state.coin.parent_coin_info, created_height)
516        .await?
517        .map_err(|_| WalletError::RejectPuzzleSolution)?;
518
519    let mut allocator = Allocator::new();
520
521    let Ok(output) = spend
522        .puzzle
523        .run(&mut allocator, 0, max_cost, &spend.solution)
524    else {
525        return Err(WalletError::Clvm);
526    };
527
528    let Ok(conditions) = Vec::<Condition>::from_clvm(&allocator, output.1) else {
529        return Err(WalletError::Parse);
530    };
531
532    let Some(urls) = urls_from_conditions(&allocator, &coin_state.coin, &conditions) else {
533        return Err(WalletError::Parse);
534    };
535
536    let puzzle = spend
537        .puzzle
538        .to_clvm(&mut allocator)
539        .map_err(DriverError::ToClvm)?;
540
541    Ok(XchServerCoin {
542        coin: coin_state.coin,
543        p2_puzzle_hash: tree_hash(&allocator, puzzle).into(),
544        memo_urls: urls,
545    })
546}
547
548#[allow(clippy::too_many_arguments)]
549pub fn mint_store(
550    minter_synthetic_key: PublicKey,
551    selected_coins: Vec<Coin>,
552    root_hash: Bytes32,
553    label: Option<String>,
554    description: Option<String>,
555    bytes: Option<u64>,
556    size_proof: Option<String>,
557    owner_puzzle_hash: Bytes32,
558    delegated_puzzles: Vec<DelegatedPuzzle>,
559    fee: u64,
560) -> Result<SuccessResponse, WalletError> {
561    let minter_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(minter_synthetic_key).into();
562    let total_amount_from_coins = selected_coins.iter().map(|c| c.amount).sum::<u64>();
563
564    let total_amount = fee + 1;
565
566    let mut ctx = SpendContext::new();
567
568    let p2 = StandardLayer::new(minter_synthetic_key);
569
570    let lead_coin = selected_coins[0];
571    let lead_coin_name = lead_coin.coin_id();
572
573    for coin in selected_coins.into_iter().skip(1) {
574        p2.spend(
575            &mut ctx,
576            coin,
577            Conditions::new().assert_concurrent_spend(lead_coin_name),
578        )?;
579    }
580
581    let (launch_singleton, datastore) = Launcher::new(lead_coin_name, 1).mint_datastore(
582        &mut ctx,
583        DataStoreMetadata {
584            root_hash,
585            label,
586            description,
587            bytes,
588            size_proof,
589        },
590        owner_puzzle_hash.into(),
591        delegated_puzzles,
592    )?;
593
594    let launch_singleton = Conditions::new().extend(
595        launch_singleton
596            .into_iter()
597            .map(|cond| {
598                if let Condition::CreateCoin(cc) = cond {
599                    if cc.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() {
600                        let hint = ctx.hint(DATASTORE_LAUNCHER_HINT)?;
601
602                        return Ok(Condition::CreateCoin(CreateCoin {
603                            puzzle_hash: cc.puzzle_hash,
604                            amount: cc.amount,
605                            memos: hint,
606                        }));
607                    }
608
609                    return Ok(Condition::CreateCoin(cc));
610                }
611
612                Ok(cond)
613            })
614            .collect::<Result<Vec<_>, WalletError>>()?,
615    );
616
617    let lead_coin_conditions = if total_amount_from_coins > total_amount {
618        let hint = ctx.hint(minter_puzzle_hash)?;
619
620        launch_singleton.create_coin(
621            minter_puzzle_hash,
622            total_amount_from_coins - total_amount,
623            hint,
624        )
625    } else {
626        launch_singleton
627    };
628    p2.spend(&mut ctx, lead_coin, lead_coin_conditions)?;
629
630    Ok(SuccessResponse {
631        coin_spends: ctx.take(),
632        new_datastore: datastore,
633    })
634}
635
636pub struct SyncStoreResponse {
637    pub latest_store: DataStore,
638    pub latest_height: u32,
639    pub root_hash_history: Option<Vec<(Bytes32, u64)>>,
640}
641
642pub async fn sync_store(
643    peer: &Peer,
644    store: &DataStore,
645    last_height: Option<u32>,
646    last_header_hash: Bytes32,
647    with_history: bool,
648) -> Result<SyncStoreResponse, WalletError> {
649    let mut latest_store = store.clone();
650    let mut history = vec![];
651
652    let response = peer
653        .request_coin_state(
654            vec![store.coin.coin_id()],
655            last_height,
656            last_header_hash,
657            false,
658        )
659        .await
660        .map_err(WalletError::Client)?
661        .map_err(|_| WalletError::RejectCoinState)?;
662    let mut last_coin_record = response
663        .coin_states
664        .into_iter()
665        .next()
666        .ok_or(WalletError::UnknownCoin)?;
667
668    let mut ctx = SpendContext::new(); // just to run puzzles more easily
669
670    while last_coin_record.spent_height.is_some() {
671        let puzzle_and_solution_req = peer
672            .request_puzzle_and_solution(
673                last_coin_record.coin.coin_id(),
674                last_coin_record.spent_height.unwrap(),
675            )
676            .await
677            .map_err(WalletError::Client)?
678            .map_err(|_| WalletError::RejectPuzzleSolution)?;
679
680        let cs = CoinSpend {
681            coin: last_coin_record.coin,
682            puzzle_reveal: puzzle_and_solution_req.puzzle,
683            solution: puzzle_and_solution_req.solution,
684        };
685
686        let new_store = DataStore::<DataStoreMetadata>::from_spend(
687            &mut ctx,
688            &cs,
689            &latest_store.info.delegated_puzzles,
690        )
691        .map_err(|_| WalletError::Parse)?
692        .ok_or(WalletError::Parse)?;
693
694        if with_history {
695            let resp: Result<RespondBlockHeader, RejectHeaderRequest> = peer
696                .request_fallible(RequestBlockHeader {
697                    height: last_coin_record.spent_height.unwrap(),
698                })
699                .await
700                .map_err(WalletError::Client)?;
701            let block_header = resp.map_err(|_| WalletError::RejectHeaderRequest)?;
702
703            history.push((
704                new_store.info.metadata.root_hash,
705                block_header
706                    .header_block
707                    .foliage_transaction_block
708                    .unwrap()
709                    .timestamp,
710            ));
711        }
712
713        let response = peer
714            .request_coin_state(
715                vec![new_store.coin.coin_id()],
716                last_height,
717                last_header_hash,
718                false,
719            )
720            .await
721            .map_err(WalletError::Client)?
722            .map_err(|_| WalletError::RejectCoinState)?;
723
724        last_coin_record = response
725            .coin_states
726            .into_iter()
727            .next()
728            .ok_or(WalletError::UnknownCoin)?;
729        latest_store = new_store;
730    }
731
732    Ok(SyncStoreResponse {
733        latest_store,
734        latest_height: last_coin_record
735            .created_height
736            .ok_or(WalletError::UnknownCoin)?,
737        root_hash_history: if with_history { Some(history) } else { None },
738    })
739}
740
741pub async fn sync_store_using_launcher_id(
742    peer: &Peer,
743    launcher_id: Bytes32,
744    last_height: Option<u32>,
745    last_header_hash: Bytes32,
746    with_history: bool,
747) -> Result<SyncStoreResponse, WalletError> {
748    let response = peer
749        .request_coin_state(vec![launcher_id], last_height, last_header_hash, false)
750        .await
751        .map_err(WalletError::Client)?
752        .map_err(|_| WalletError::RejectCoinState)?;
753    let last_coin_record = response
754        .coin_states
755        .into_iter()
756        .next()
757        .ok_or(WalletError::UnknownCoin)?;
758
759    let mut ctx = SpendContext::new(); // just to run puzzles more easily
760
761    let puzzle_and_solution_req = peer
762        .request_puzzle_and_solution(
763            last_coin_record.coin.coin_id(),
764            last_coin_record
765                .spent_height
766                .ok_or(WalletError::UnknownCoin)?,
767        )
768        .await
769        .map_err(WalletError::Client)?
770        .map_err(|_| WalletError::RejectPuzzleSolution)?;
771
772    let cs = CoinSpend {
773        coin: last_coin_record.coin,
774        puzzle_reveal: puzzle_and_solution_req.puzzle,
775        solution: puzzle_and_solution_req.solution,
776    };
777
778    let first_store = DataStore::<DataStoreMetadata>::from_spend(&mut ctx, &cs, &[])
779        .map_err(|_| WalletError::Parse)?
780        .ok_or(WalletError::Parse)?;
781
782    let res = sync_store(
783        peer,
784        &first_store,
785        last_height,
786        last_header_hash,
787        with_history,
788    )
789    .await?;
790
791    // prepend root hash from launch
792    let root_hash_history = if let Some(mut res_root_hash_history) = res.root_hash_history {
793        let spent_timestamp = if let Some(spent_height) = last_coin_record.spent_height {
794            let resp: Result<RespondBlockHeader, RejectHeaderRequest> = peer
795                .request_fallible(RequestBlockHeader {
796                    height: spent_height,
797                })
798                .await
799                .map_err(WalletError::Client)?;
800            let resp = resp.map_err(|_| WalletError::RejectHeaderRequest)?;
801
802            resp.header_block
803                .foliage_transaction_block
804                .unwrap()
805                .timestamp
806        } else {
807            0
808        };
809
810        res_root_hash_history.insert(0, (first_store.info.metadata.root_hash, spent_timestamp));
811        Some(res_root_hash_history)
812    } else {
813        None
814    };
815
816    Ok(SyncStoreResponse {
817        latest_store: res.latest_store,
818        latest_height: res.latest_height,
819        root_hash_history,
820    })
821}
822
823pub async fn get_store_creation_height(
824    peer: &Peer,
825    launcher_id: Bytes32,
826    last_height: Option<u32>,
827    last_header_hash: Bytes32,
828) -> Result<u32, WalletError> {
829    let response = peer
830        .request_coin_state(vec![launcher_id], last_height, last_header_hash, false)
831        .await
832        .map_err(WalletError::Client)?
833        .map_err(|_| WalletError::RejectCoinState)?;
834    let last_coin_record = response
835        .coin_states
836        .into_iter()
837        .next()
838        .ok_or(WalletError::UnknownCoin)?;
839
840    last_coin_record
841        .created_height
842        .ok_or(WalletError::UnknownCoin)
843}
844
845#[derive(Clone, Debug)]
846pub enum DataStoreInnerSpend {
847    Owner(PublicKey),
848    Admin(PublicKey),
849    Writer(PublicKey),
850    // does not include oracle since it can't change metadata/owners :(
851}
852
853fn update_store_with_conditions(
854    ctx: &mut SpendContext,
855    conditions: Conditions,
856    datastore: DataStore,
857    inner_spend_info: DataStoreInnerSpend,
858    allow_admin: bool,
859    allow_writer: bool,
860) -> Result<SuccessResponse, WalletError> {
861    let inner_datastore_spend = match inner_spend_info {
862        DataStoreInnerSpend::Owner(pk) => {
863            StandardLayer::new(pk).spend_with_conditions(ctx, conditions)?
864        }
865        DataStoreInnerSpend::Admin(pk) => {
866            if !allow_admin {
867                return Err(WalletError::Permission);
868            }
869
870            StandardLayer::new(pk).spend_with_conditions(ctx, conditions)?
871        }
872        DataStoreInnerSpend::Writer(pk) => {
873            if !allow_writer {
874                return Err(WalletError::Permission);
875            }
876
877            WriterLayer::new(StandardLayer::new(pk)).spend(ctx, conditions)?
878        }
879    };
880
881    let parent_delegated_puzzles = datastore.info.delegated_puzzles.clone();
882    let new_spend = datastore.spend(ctx, inner_datastore_spend)?;
883
884    let new_datastore =
885        DataStore::<DataStoreMetadata>::from_spend(ctx, &new_spend, &parent_delegated_puzzles)?
886            .ok_or(WalletError::Parse)?;
887
888    Ok(SuccessResponse {
889        coin_spends: vec![new_spend],
890        new_datastore,
891    })
892}
893
894pub fn update_store_ownership(
895    datastore: DataStore,
896    new_owner_puzzle_hash: Bytes32,
897    new_delegated_puzzles: Vec<DelegatedPuzzle>,
898    inner_spend_info: DataStoreInnerSpend,
899) -> Result<SuccessResponse, WalletError> {
900    let ctx = &mut SpendContext::new();
901
902    let update_condition: Condition = match inner_spend_info {
903        DataStoreInnerSpend::Owner(_) => {
904            DataStore::<DataStoreMetadata>::owner_create_coin_condition(
905                ctx,
906                datastore.info.launcher_id,
907                new_owner_puzzle_hash,
908                new_delegated_puzzles,
909                true,
910            )?
911        }
912        DataStoreInnerSpend::Admin(_) => {
913            let merkle_tree = get_merkle_tree(ctx, new_delegated_puzzles.clone())?;
914
915            let new_merkle_root_condition = UpdateDataStoreMerkleRoot {
916                new_merkle_root: merkle_tree.root(),
917                memos: DataStore::<DataStoreMetadata>::get_recreation_memos(
918                    datastore.info.launcher_id,
919                    new_owner_puzzle_hash.into(),
920                    new_delegated_puzzles,
921                ),
922            }
923            .to_clvm(&mut **ctx)
924            .map_err(DriverError::ToClvm)?;
925
926            Condition::Other(new_merkle_root_condition)
927        }
928        _ => return Err(WalletError::Permission),
929    };
930
931    let update_conditions = Conditions::new().with(update_condition);
932
933    update_store_with_conditions(
934        ctx,
935        update_conditions,
936        datastore,
937        inner_spend_info,
938        true,
939        false,
940    )
941}
942
943pub fn update_store_metadata(
944    datastore: DataStore,
945    new_root_hash: Bytes32,
946    new_label: Option<String>,
947    new_description: Option<String>,
948    new_bytes: Option<u64>,
949    new_size_proof: Option<String>,
950    inner_spend_info: DataStoreInnerSpend,
951) -> Result<SuccessResponse, WalletError> {
952    let ctx = &mut SpendContext::new();
953
954    let new_metadata = DataStoreMetadata {
955        root_hash: new_root_hash,
956        label: new_label,
957        description: new_description,
958        bytes: new_bytes,
959        size_proof: new_size_proof,
960    };
961    let mut new_metadata_condition = Conditions::new().with(
962        DataStore::<DataStoreMetadata>::new_metadata_condition(ctx, new_metadata)?,
963    );
964
965    if let DataStoreInnerSpend::Owner(_) = inner_spend_info {
966        new_metadata_condition = new_metadata_condition.with(
967            DataStore::<DataStoreMetadata>::owner_create_coin_condition(
968                ctx,
969                datastore.info.launcher_id,
970                datastore.info.owner_puzzle_hash,
971                datastore.info.delegated_puzzles.clone(),
972                false,
973            )?,
974        );
975    }
976
977    update_store_with_conditions(
978        ctx,
979        new_metadata_condition,
980        datastore,
981        inner_spend_info,
982        true,
983        true,
984    )
985}
986
987pub fn melt_store(
988    datastore: DataStore,
989    owner_pk: PublicKey,
990) -> Result<Vec<CoinSpend>, WalletError> {
991    let ctx = &mut SpendContext::new();
992
993    let melt_conditions = Conditions::new()
994        .with(Condition::reserve_fee(1))
995        .with(Condition::Other(
996            MeltSingleton {}
997                .to_clvm(&mut **ctx)
998                .map_err(DriverError::ToClvm)?,
999        ));
1000
1001    let inner_datastore_spend =
1002        StandardLayer::new(owner_pk).spend_with_conditions(ctx, melt_conditions)?;
1003
1004    let new_spend = datastore.spend(ctx, inner_datastore_spend)?;
1005
1006    Ok(vec![new_spend])
1007}
1008
1009pub fn oracle_spend(
1010    spender_synthetic_key: PublicKey,
1011    selected_coins: Vec<Coin>,
1012    datastore: DataStore,
1013    fee: u64,
1014) -> Result<SuccessResponse, WalletError> {
1015    let Some(DelegatedPuzzle::Oracle(oracle_ph, oracle_fee)) = datastore
1016        .info
1017        .delegated_puzzles
1018        .iter()
1019        .find(|dp| matches!(dp, DelegatedPuzzle::Oracle(_, _)))
1020    else {
1021        return Err(WalletError::Permission);
1022    };
1023
1024    let spender_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(spender_synthetic_key).into();
1025
1026    let total_amount = oracle_fee + fee;
1027
1028    let ctx = &mut SpendContext::new();
1029
1030    let p2 = StandardLayer::new(spender_synthetic_key);
1031
1032    let lead_coin = selected_coins[0];
1033    let lead_coin_name = lead_coin.coin_id();
1034
1035    let total_amount_from_coins = selected_coins.iter().map(|c| c.amount).sum::<u64>();
1036    for coin in selected_coins.into_iter().skip(1) {
1037        p2.spend(
1038            ctx,
1039            coin,
1040            Conditions::new().assert_concurrent_spend(lead_coin_name),
1041        )?;
1042    }
1043
1044    let assert_oracle_conds = Conditions::new().assert_puzzle_announcement(announcement_id(
1045        datastore.coin.puzzle_hash,
1046        Bytes::new("$".into()),
1047    ));
1048
1049    let mut lead_coin_conditions = assert_oracle_conds;
1050    if total_amount_from_coins > total_amount {
1051        let hint = ctx.hint(spender_puzzle_hash)?;
1052
1053        lead_coin_conditions = lead_coin_conditions.create_coin(
1054            spender_puzzle_hash,
1055            total_amount_from_coins - total_amount,
1056            hint,
1057        );
1058    }
1059    if fee > 0 {
1060        lead_coin_conditions = lead_coin_conditions.reserve_fee(fee);
1061    }
1062    p2.spend(ctx, lead_coin, lead_coin_conditions)?;
1063
1064    let inner_datastore_spend = OracleLayer::new(*oracle_ph, *oracle_fee)
1065        .ok_or(DriverError::OddOracleFee)?
1066        .construct_spend(ctx, ())?;
1067
1068    let parent_delegated_puzzles = datastore.info.delegated_puzzles.clone();
1069    let new_spend = datastore.spend(ctx, inner_datastore_spend)?;
1070
1071    let new_datastore = DataStore::from_spend(ctx, &new_spend, &parent_delegated_puzzles)?
1072        .ok_or(WalletError::Parse)?;
1073    ctx.insert(new_spend.clone());
1074
1075    Ok(SuccessResponse {
1076        coin_spends: ctx.take(),
1077        new_datastore,
1078    })
1079}
1080
1081pub fn add_fee(
1082    spender_synthetic_key: PublicKey,
1083    selected_coins: Vec<Coin>,
1084    coin_ids: Vec<Bytes32>,
1085    fee: u64,
1086) -> Result<Vec<CoinSpend>, WalletError> {
1087    let spender_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(spender_synthetic_key).into();
1088    let total_amount_from_coins = selected_coins.iter().map(|c| c.amount).sum::<u64>();
1089
1090    let mut ctx = SpendContext::new();
1091
1092    let p2 = StandardLayer::new(spender_synthetic_key);
1093
1094    let lead_coin = selected_coins[0];
1095    let lead_coin_name = lead_coin.coin_id();
1096
1097    for coin in selected_coins.into_iter().skip(1) {
1098        p2.spend(
1099            &mut ctx,
1100            coin,
1101            Conditions::new().assert_concurrent_spend(lead_coin_name),
1102        )?;
1103    }
1104
1105    let mut lead_coin_conditions = Conditions::new().reserve_fee(fee);
1106    if total_amount_from_coins > fee {
1107        let hint = ctx.hint(spender_puzzle_hash)?;
1108
1109        lead_coin_conditions = lead_coin_conditions.create_coin(
1110            spender_puzzle_hash,
1111            total_amount_from_coins - fee,
1112            hint,
1113        );
1114    }
1115    for coin_id in coin_ids {
1116        lead_coin_conditions = lead_coin_conditions.assert_concurrent_spend(coin_id);
1117    }
1118
1119    p2.spend(&mut ctx, lead_coin, lead_coin_conditions)?;
1120
1121    Ok(ctx.take())
1122}
1123
1124pub fn public_key_to_synthetic_key(pk: PublicKey) -> PublicKey {
1125    pk.derive_synthetic()
1126}
1127
1128pub fn secret_key_to_synthetic_key(sk: SecretKey) -> SecretKey {
1129    sk.derive_synthetic()
1130}
1131
1132#[derive(Debug, Clone, Copy)]
1133pub enum TargetNetwork {
1134    Mainnet,
1135    Testnet11,
1136}
1137
1138impl TargetNetwork {
1139    fn get_constants(&self) -> &ConsensusConstants {
1140        match self {
1141            TargetNetwork::Mainnet => &MAINNET_CONSTANTS,
1142            TargetNetwork::Testnet11 => &TESTNET11_CONSTANTS,
1143        }
1144    }
1145}
1146
1147pub fn sign_coin_spends(
1148    coin_spends: Vec<CoinSpend>,
1149    private_keys: Vec<SecretKey>,
1150    network: TargetNetwork,
1151) -> Result<Signature, SignerError> {
1152    let mut allocator = Allocator::new();
1153
1154    let required_signatures = RequiredSignature::from_coin_spends(
1155        &mut allocator,
1156        &coin_spends,
1157        &AggSigConstants::new(network.get_constants().agg_sig_me_additional_data),
1158    )?;
1159
1160    let key_pairs = private_keys
1161        .iter()
1162        .map(|sk| {
1163            (
1164                sk.public_key(),
1165                sk.clone(),
1166                sk.public_key().derive_synthetic(),
1167                sk.derive_synthetic(),
1168            )
1169        })
1170        .flat_map(|(pk1, sk1, pk2, sk2)| vec![(pk1, sk1), (pk2, sk2)])
1171        .collect::<HashMap<PublicKey, SecretKey>>();
1172
1173    let mut sig = Signature::default();
1174
1175    for required in required_signatures {
1176        let RequiredSignature::Bls(required) = required else {
1177            continue;
1178        };
1179
1180        let sk = key_pairs.get(&required.public_key);
1181
1182        if let Some(sk) = sk {
1183            sig += &sign(sk, required.message());
1184        }
1185    }
1186
1187    Ok(sig)
1188}
1189
1190pub async fn broadcast_spend_bundle(
1191    peer: &Peer,
1192    spend_bundle: SpendBundle,
1193) -> Result<TransactionAck, WalletError> {
1194    peer.send_transaction(spend_bundle)
1195        .await
1196        .map_err(WalletError::Client)
1197}
1198
1199pub async fn get_header_hash(peer: &Peer, height: u32) -> Result<Bytes32, WalletError> {
1200    let resp: Result<RespondBlockHeader, RejectHeaderRequest> = peer
1201        .request_fallible(RequestBlockHeader { height })
1202        .await
1203        .map_err(WalletError::Client)?;
1204
1205    resp.map_err(|_| WalletError::RejectHeaderRequest)
1206        .map(|resp| resp.header_block.header_hash())
1207}
1208
1209pub async fn get_fee_estimate(peer: &Peer, target_time_seconds: u64) -> Result<u64, WalletError> {
1210    let target_time_seconds = target_time_seconds
1211        + SystemTime::now()
1212            .duration_since(UNIX_EPOCH)
1213            .expect("Time went backwards")
1214            .as_secs();
1215
1216    let resp: RespondFeeEstimates = peer
1217        .request_infallible(RequestFeeEstimates {
1218            time_targets: vec![target_time_seconds],
1219        })
1220        .await
1221        .map_err(WalletError::Client)?;
1222    let fee_estimate_group = resp.estimates;
1223
1224    if let Some(error_message) = fee_estimate_group.error {
1225        return Err(WalletError::FeeEstimateRejection(error_message));
1226    }
1227
1228    if let Some(first_estimate) = fee_estimate_group.estimates.first() {
1229        if let Some(error_message) = &first_estimate.error {
1230            return Err(WalletError::FeeEstimateRejection(error_message.clone()));
1231        }
1232
1233        return Ok(first_estimate.estimated_fee_rate.mojos_per_clvm_cost);
1234    }
1235
1236    Err(WalletError::FeeEstimateRejection(
1237        "No fee estimates available".to_string(),
1238    ))
1239}
1240
1241pub async fn is_coin_spent(
1242    peer: &Peer,
1243    coin_id: Bytes32,
1244    last_height: Option<u32>,
1245    last_header_hash: Bytes32,
1246) -> Result<bool, WalletError> {
1247    let response = peer
1248        .request_coin_state(vec![coin_id], last_height, last_header_hash, false)
1249        .await
1250        .map_err(WalletError::Client)?
1251        .map_err(|_| WalletError::RejectCoinState)?;
1252
1253    if let Some(coin_state) = response.coin_states.first() {
1254        return Ok(coin_state.spent_height.is_some());
1255    }
1256
1257    Ok(false)
1258}
1259
1260// https://github.com/Chia-Network/chips/blob/main/CHIPs/chip-0002.md#signmessage
1261pub fn make_message(msg: Bytes) -> Result<Bytes32, WalletError> {
1262    let mut alloc = Allocator::new();
1263    let thing_ptr = clvm_tuple!("Chia Signed Message", msg)
1264        .to_clvm(&mut alloc)
1265        .map_err(DriverError::ToClvm)?;
1266
1267    Ok(tree_hash(&alloc, thing_ptr).into())
1268}
1269
1270pub fn sign_message(message: Bytes, sk: SecretKey) -> Result<Signature, WalletError> {
1271    Ok(sign(&sk, make_message(message)?))
1272}
1273
1274pub fn verify_signature(
1275    message: Bytes,
1276    pk: PublicKey,
1277    sig: Signature,
1278) -> Result<bool, WalletError> {
1279    Ok(verify(&sig, &pk, make_message(message)?))
1280}
1281
1282pub fn get_cost(coin_spends: Vec<CoinSpend>) -> Result<u64, WalletError> {
1283    let mut alloc = Allocator::new();
1284
1285    let generator = solution_generator(
1286        coin_spends
1287            .into_iter()
1288            .map(|cs| (cs.coin, cs.puzzle_reveal, cs.solution)),
1289    )
1290    .map_err(WalletError::Io)?;
1291
1292    let conds = run_block_generator::<&[u8], _>(
1293        &mut alloc,
1294        &generator,
1295        [],
1296        u64::MAX,
1297        MEMPOOL_MODE | DONT_VALIDATE_SIGNATURE,
1298        &Signature::default(),
1299        None,
1300        TargetNetwork::Mainnet.get_constants(),
1301    )?;
1302
1303    let conds = OwnedSpendBundleConditions::from(&alloc, conds);
1304
1305    Ok(conds.cost)
1306}
1307
1308pub struct PossibleLaunchersResponse {
1309    pub launcher_ids: Vec<Bytes32>,
1310    pub last_height: u32,
1311    pub last_header_hash: Bytes32,
1312}
1313
1314pub async fn look_up_possible_launchers(
1315    peer: &Peer,
1316    previous_height: Option<u32>,
1317    previous_header_hash: Bytes32,
1318) -> Result<PossibleLaunchersResponse, WalletError> {
1319    let resp = get_unspent_coin_states(
1320        peer,
1321        DATASTORE_LAUNCHER_HINT,
1322        previous_height,
1323        previous_header_hash,
1324        true,
1325    )
1326    .await?;
1327
1328    Ok(PossibleLaunchersResponse {
1329        last_header_hash: resp.last_header_hash,
1330        last_height: resp.last_height,
1331        launcher_ids: resp
1332            .coin_states
1333            .into_iter()
1334            .filter_map(|coin_state| {
1335                if coin_state.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() {
1336                    Some(coin_state.coin.coin_id())
1337                } else {
1338                    None
1339                }
1340            })
1341            .collect(),
1342    })
1343}
1344
1345/// Utility function to validate that a coin is a $DIG CAT coin. Returns an instantiated Cat
1346/// utility for the coin if it's a valid $DIG CAT
1347pub async fn prove_dig_cat_coin(
1348    peer: &Peer,
1349    coin: &Coin,
1350    coin_created_height: u32,
1351) -> Result<Cat, WalletError> {
1352    let mut ctx = SpendContext::new();
1353
1354    // 1) Request parent coin state
1355    let parent_state_response = peer
1356        .request_coin_state(
1357            vec![coin.parent_coin_info],
1358            None,
1359            MAINNET_CONSTANTS.genesis_challenge,
1360            false,
1361        )
1362        .await?;
1363
1364    let parent_state = parent_state_response.map_err(|_| WalletError::RejectCoinState)?;
1365
1366    // 2) Request parent puzzle and solution
1367    let parent_puzzle_and_solution_response = peer
1368        .request_puzzle_and_solution(parent_state.coin_ids[0], coin_created_height)
1369        .await?;
1370
1371    let parent_puzzle_and_solution =
1372        parent_puzzle_and_solution_response.map_err(|_| WalletError::RejectPuzzleSolution)?;
1373
1374    // 3) Convert puzzle to CLVM
1375    let parent_puzzle_ptr = ctx.alloc(&parent_puzzle_and_solution.puzzle)?;
1376    let parent_puzzle = Puzzle::parse(&ctx, parent_puzzle_ptr);
1377
1378    // 4) Convert solution to CLVM
1379    let parent_solution = ctx.alloc(&parent_puzzle_and_solution.solution)?;
1380
1381    // 5) Parse CAT
1382    let parsed_children = Cat::parse_children(
1383        &mut ctx,
1384        parent_state.coin_states[0].coin,
1385        parent_puzzle,
1386        parent_solution,
1387    )?
1388    .ok_or(WalletError::UnknownCoin)?;
1389
1390    let proved_cat = parsed_children
1391        .into_iter()
1392        .find(|parsed_child| {
1393            parsed_child.coin_id() == coin.coin_id()
1394                && parsed_child.lineage_proof.is_some()
1395                && parsed_child.info.asset_id == DIG_ASSET_ID
1396        })
1397        .ok_or_else(|| WalletError::UnknownCoin)?;
1398    Ok(proved_cat)
1399}
1400
1401pub async fn subscribe_to_coin_states(
1402    peer: &Peer,
1403    coin_id: Bytes32,
1404    previous_height: Option<u32>,
1405    previous_header_hash: Bytes32,
1406) -> Result<Option<u32>, WalletError> {
1407    let response = peer
1408        .request_coin_state(vec![coin_id], previous_height, previous_header_hash, true)
1409        .await
1410        .map_err(WalletError::Client)?
1411        .map_err(|_| WalletError::RejectCoinState)?;
1412
1413    if let Some(coin_state) = response.coin_states.first() {
1414        return Ok(coin_state.spent_height);
1415    }
1416
1417    Err(WalletError::UnknownCoin)
1418}
1419
1420pub async fn unsubscribe_from_coin_states(
1421    peer: &Peer,
1422    coin_id: Bytes32,
1423) -> Result<(), WalletError> {
1424    peer.remove_coin_subscriptions(Some(vec![coin_id]))
1425        .await
1426        .map_err(WalletError::Client)?;
1427
1428    Ok(())
1429}
1430
1431/// Mints a new NFT using a DID string.
1432///
1433/// # Arguments
1434/// * `peer` - The peer to query blockchain data
1435/// * `synthetic_key` - The synthetic key of the wallet
1436/// * `selected_coins` - Coins to spend for the transaction
1437/// * `did_string` - The DID string (e.g., "did:chia:1s8j4pquxfu5mhlldzu357qfqkwa9r35mdx5a0p0ehn76dr4ut4tqs0n6kv")
1438/// * `recipient_puzzle_hash` - The puzzle hash to send the NFT to
1439/// * `metadata` - The NFT metadata
1440/// * `royalty_puzzle_hash` - Optional royalty puzzle hash (defaults to recipient if None)
1441/// * `royalty_basis_points` - Royalty percentage in basis points (e.g., 300 = 3%)
1442/// * `fee` - Transaction fee
1443/// * `network` - The target network (mainnet/testnet)
1444///
1445/// # Returns
1446/// A vector of coin spends that mint the NFT
1447#[allow(clippy::too_many_arguments)]
1448pub async fn mint_nft(
1449    peer: &Peer,
1450    synthetic_key: PublicKey,
1451    selected_coins: Vec<Coin>,
1452    did_string: &str,
1453    recipient_puzzle_hash: Bytes32,
1454    metadata: NftMetadata,
1455    _royalty_puzzle_hash: Option<Bytes32>,
1456    royalty_basis_points: u16,
1457    fee: u64,
1458    network: TargetNetwork,
1459) -> Result<Vec<CoinSpend>, WalletError> {
1460    // Resolve the DID string to get the current coin and proof
1461    let (did_proof, did_coin) =
1462        resolve_did_string_and_generate_proof(peer, did_string, network).await?;
1463    let mut ctx = SpendContext::new();
1464
1465    // Convert DID proof
1466    let did_proof = match did_proof {
1467        chia::puzzles::Proof::Eve(eve) => Proof::Eve(EveProof {
1468            parent_parent_coin_info: eve.parent_parent_coin_info,
1469            parent_amount: eve.parent_amount,
1470        }),
1471        chia::puzzles::Proof::Lineage(lineage) => Proof::Lineage(LineageProof {
1472            parent_parent_coin_info: lineage.parent_parent_coin_info,
1473            parent_inner_puzzle_hash: lineage.parent_inner_puzzle_hash,
1474            parent_amount: lineage.parent_amount,
1475        }),
1476    };
1477
1478    // Create the DID singleton info (simplified DID structure)
1479    // Use the first 32 bytes of the public key (truncate from 48 to 32 bytes)
1480    let public_key_bytes = synthetic_key.derive_synthetic().to_bytes();
1481    let mut public_key_hash = [0u8; 32];
1482    public_key_hash.copy_from_slice(&public_key_bytes[..32]);
1483    let mut meta_data_allocator = Allocator::new();
1484    let node_metadata = metadata.to_clvm(&mut meta_data_allocator)?;
1485    let metadata_hashed_ptr = HashedPtr::from_ptr(&meta_data_allocator, node_metadata);
1486    let did_info: DidInfo = DidInfo::new(
1487        did_coin.coin_id(),
1488        None,
1489        1,
1490        metadata_hashed_ptr,
1491        public_key_hash.into(),
1492    );
1493
1494    let did = Did::new(did_coin, did_proof, did_info);
1495
1496    // Create StandardLayer for spending coins
1497    let p2 = StandardLayer::new(synthetic_key);
1498
1499    // Create the NFT mint configuration with metadata
1500    let nft_mint = NftMint::new(
1501        metadata_hashed_ptr,
1502        recipient_puzzle_hash,
1503        royalty_basis_points,
1504        None, // No DID owner for now - we'll set this up differently
1505    );
1506
1507    // Use IntermediateLauncher to mint the NFT
1508    let (mint_conditions, _nft) = IntermediateLauncher::new(did_coin.coin_id(), 0, 1)
1509        .create(&mut ctx)?
1510        .mint_nft(&mut ctx, &nft_mint)?;
1511
1512    // Update the DID with the mint conditions
1513    let _updated_did = did.update(&mut ctx, &p2, mint_conditions)?;
1514
1515    // Handle fee and change
1516    let total_input = selected_coins.iter().map(|coin| coin.amount).sum::<u64>();
1517    let total_needed = fee + 1; // 1 mojo for the NFT
1518
1519    if total_input < total_needed {
1520        return Err(WalletError::Parse); // Not enough coins
1521    }
1522
1523    let _change = total_input - total_needed;
1524    let change_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into();
1525
1526    // Spend the selected coins
1527    spend_coins_together(
1528        &mut ctx,
1529        synthetic_key,
1530        &selected_coins,
1531        Conditions::new().reserve_fee(fee),
1532        total_needed as i64,
1533        change_puzzle_hash,
1534    )?;
1535
1536    Ok(ctx.take())
1537}
1538
1539/// Generates a DID proof for a DID coin by analyzing its parent.
1540/// This is a simplified version that automatically determines the proof type.
1541///
1542/// # Arguments
1543/// * `peer` - The peer to query blockchain data
1544/// * `did_coin` - The DID coin to generate proof for
1545/// * `network` - The target network (mainnet/testnet)
1546///
1547/// # Returns
1548/// A tuple containing the DID proof and the DID coin
1549pub async fn generate_did_proof(
1550    peer: &Peer,
1551    did_coin: Coin,
1552    network: TargetNetwork,
1553) -> Result<(chia::puzzles::Proof, Coin), WalletError> {
1554    let proof = generate_did_proof_from_chain(peer, did_coin, network).await?;
1555    Ok((proof, did_coin))
1556}
1557
1558/// Generates a DID proof manually when you have the parent information.
1559///
1560/// # Arguments
1561/// * `did_coin` - The current DID coin
1562/// * `parent_coin` - The parent coin of the DID (None for eve proof)
1563/// * `parent_inner_puzzle_hash` - The parent's inner puzzle hash (for lineage proof)
1564///
1565/// # Returns
1566/// A DID proof that can be used to spend the DID coin
1567pub fn generate_did_proof_manual(
1568    did_coin: Coin,
1569    parent_coin: Option<Coin>,
1570    parent_inner_puzzle_hash: Option<Bytes32>,
1571) -> Result<chia::puzzles::Proof, WalletError> {
1572    match parent_coin {
1573        // Eve proof - first spend from launcher
1574        None => {
1575            // For eve proof, we need the launcher coin info
1576            // The parent_parent_coin_info is the coin that created the launcher
1577            // The parent_amount is the launcher coin amount (typically 1 mojo)
1578            Ok(chia::puzzles::Proof::Eve(chia::puzzles::EveProof {
1579                parent_parent_coin_info: did_coin.parent_coin_info,
1580                parent_amount: 1, // Launcher coins are typically 1 mojo
1581            }))
1582        }
1583        // Lineage proof - subsequent spends
1584        Some(parent) => {
1585            let parent_inner_puzzle_hash = parent_inner_puzzle_hash.ok_or(WalletError::Parse)?; // Need inner puzzle hash for lineage proof
1586
1587            Ok(chia::puzzles::Proof::Lineage(chia::puzzles::LineageProof {
1588                parent_parent_coin_info: parent.parent_coin_info,
1589                parent_inner_puzzle_hash,
1590                parent_amount: parent.amount,
1591            }))
1592        }
1593    }
1594}
1595
1596/// Generates a DID proof from a coin spend by analyzing the parent spend.
1597///
1598/// # Arguments
1599/// * `peer` - The peer to query blockchain data
1600/// * `did_coin` - The DID coin to generate proof for
1601/// * `network` - The target network (mainnet/testnet)
1602///
1603/// # Returns
1604/// A DID proof that can be used to spend the DID coin
1605pub async fn generate_did_proof_from_chain(
1606    peer: &Peer,
1607    did_coin: Coin,
1608    network: TargetNetwork,
1609) -> Result<chia::puzzles::Proof, WalletError> {
1610    // Get the parent coin state
1611    let parent_coin_states = peer
1612        .request_coin_state(
1613            vec![did_coin.parent_coin_info],
1614            None,
1615            match network {
1616                TargetNetwork::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
1617                TargetNetwork::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
1618            },
1619            false,
1620        )
1621        .await?
1622        .map_err(|_| WalletError::RejectCoinState)?
1623        .coin_states;
1624
1625    let parent_coin_state = parent_coin_states.first().ok_or(WalletError::UnknownCoin)?;
1626
1627    // Check if parent is a launcher (puzzle hash matches singleton launcher)
1628    if parent_coin_state.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() {
1629        // This is an eve proof - first spend from launcher
1630        return Ok(chia::puzzles::Proof::Eve(chia::puzzles::EveProof {
1631            parent_parent_coin_info: parent_coin_state.coin.parent_coin_info,
1632            parent_amount: parent_coin_state.coin.amount,
1633        }));
1634    }
1635
1636    // This is a lineage proof - need to get the parent's puzzle and solution
1637    let parent_spend_height = parent_coin_state
1638        .spent_height
1639        .ok_or(WalletError::UnknownCoin)?;
1640
1641    let _parent_spend = peer
1642        .request_puzzle_and_solution(parent_coin_state.coin.coin_id(), parent_spend_height)
1643        .await?
1644        .map_err(|_| WalletError::RejectPuzzleSolution)?;
1645
1646    let _allocator = Allocator::new();
1647
1648    // For now, create a basic lineage proof
1649    // This is a simplified approach - in production you'd want to properly parse the parent DID
1650    Ok(chia::puzzles::Proof::Lineage(chia::puzzles::LineageProof {
1651        parent_parent_coin_info: parent_coin_state.coin.parent_coin_info,
1652        parent_inner_puzzle_hash: Bytes32::default(), // Would need to parse from parent spend
1653        parent_amount: parent_coin_state.coin.amount,
1654    }))
1655}
1656
1657/// Creates a simple DID from a private key and selected coins.
1658///
1659/// # Arguments
1660/// * `synthetic_key` - The synthetic key that will control the DID
1661/// * `selected_coins` - Coins to spend for creating the DID
1662/// * `fee` - Transaction fee
1663///
1664/// # Returns
1665/// A tuple containing the coin spends and the created DID coin
1666pub fn create_simple_did(
1667    synthetic_key: PublicKey,
1668    selected_coins: Vec<Coin>,
1669    fee: u64,
1670) -> Result<(Vec<CoinSpend>, Coin), WalletError> {
1671    let mut ctx = SpendContext::new();
1672
1673    let p2 = StandardLayer::new(synthetic_key);
1674    let puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into();
1675
1676    // Calculate total input and needed amount
1677    let total_input = selected_coins.iter().map(|coin| coin.amount).sum::<u64>();
1678    let total_needed = fee + 1; // 1 mojo for the DID
1679
1680    if total_input < total_needed {
1681        return Err(WalletError::Parse); // Not enough coins
1682    }
1683
1684    let change = total_input - total_needed;
1685
1686    // Create the DID using the first coin as the parent for the launcher
1687    let first_coin = selected_coins[0];
1688    let launcher = Launcher::new(first_coin.coin_id(), 1);
1689
1690    // Create the DID
1691    let (create_did_conditions, did) = launcher.create_simple_did(&mut ctx, &p2)?;
1692
1693    // Spend all selected coins together
1694    let first_coin_id = first_coin.coin_id();
1695
1696    for (i, &coin) in selected_coins.iter().enumerate() {
1697        if i == 0 {
1698            // First coin creates the DID and handles change/fee
1699            let mut conditions = create_did_conditions.clone();
1700
1701            if change > 0 {
1702                let hint = ctx.hint(puzzle_hash)?;
1703                conditions = conditions.create_coin(puzzle_hash, change, hint);
1704            }
1705
1706            if fee > 0 {
1707                conditions = conditions.reserve_fee(fee);
1708            }
1709
1710            p2.spend(&mut ctx, coin, conditions)?;
1711        } else {
1712            // Other coins just assert concurrent spend
1713            p2.spend(
1714                &mut ctx,
1715                coin,
1716                Conditions::new().assert_concurrent_spend(first_coin_id),
1717            )?;
1718        }
1719    }
1720
1721    Ok((ctx.take(), did.coin))
1722}
1723
1724/// Resolves a DID string to find the current DID coin and generates its proof.
1725///
1726/// # Arguments
1727/// * `peer` - The peer to query blockchain data
1728/// * `did_string` - The DID string (e.g., "did:chia:1s8j4pquxfu5mhlldzu357qfqkwa9r35mdx5a0p0ehn76dr4ut4tqs0n6kv")
1729/// * `network` - The target network (mainnet/testnet)
1730///
1731/// # Returns
1732/// A tuple containing the DID proof and the current DID coin
1733pub async fn resolve_did_string_and_generate_proof(
1734    peer: &Peer,
1735    did_string: &str,
1736    network: TargetNetwork,
1737) -> Result<(chia::puzzles::Proof, Coin), WalletError> {
1738    // Parse DID string to extract launcher ID
1739    let parts: Vec<&str> = did_string.split(':').collect();
1740
1741    if parts.len() != 3 || parts[0] != "did" || parts[1] != "chia" {
1742        return Err(WalletError::Parse);
1743    }
1744
1745    let bech32_part = parts[2];
1746
1747    // Decode the bech32 address to get the launcher ID
1748    use chia_wallet_sdk::utils::Address;
1749    let address = Address::decode(bech32_part).map_err(|_| WalletError::Parse)?;
1750
1751    let did_id = address.puzzle_hash;
1752
1753    // First, get the launcher coin state to find the first DID coin
1754    let launcher_states = peer
1755        .request_coin_state(
1756            vec![did_id],
1757            None,
1758            match network {
1759                TargetNetwork::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
1760                TargetNetwork::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
1761            },
1762            false,
1763        )
1764        .await?
1765        .map_err(|_| WalletError::RejectCoinState)?
1766        .coin_states;
1767
1768    let launcher_state = launcher_states.first().ok_or(WalletError::UnknownCoin)?;
1769
1770    // Verify this is actually a launcher
1771    if launcher_state.coin.puzzle_hash != SINGLETON_LAUNCHER_HASH.into() {
1772        return Err(WalletError::Parse);
1773    }
1774
1775    // Get the spend of the launcher to find the first DID coin
1776    let launcher_spend_height = launcher_state
1777        .spent_height
1778        .ok_or(WalletError::UnknownCoin)?;
1779
1780    let launcher_spend = peer
1781        .request_puzzle_and_solution(launcher_state.coin.coin_id(), launcher_spend_height)
1782        .await?
1783        .map_err(|_| WalletError::RejectPuzzleSolution)?;
1784
1785    let mut allocator = Allocator::new();
1786
1787    // Run the launcher spend to find the created DID coin
1788    let launcher_puzzle = launcher_spend.puzzle.to_clvm(&mut allocator)?;
1789    let launcher_solution = launcher_spend.solution.to_clvm(&mut allocator)?;
1790
1791    let output = clvmr::run_program(
1792        &mut allocator,
1793        &clvmr::ChiaDialect::new(0),
1794        launcher_puzzle,
1795        launcher_solution,
1796        u64::MAX,
1797    )
1798    .map_err(|_| WalletError::Clvm)?;
1799
1800    let conditions =
1801        Vec::<Condition>::from_clvm(&allocator, output.1).map_err(|_| WalletError::Parse)?;
1802
1803    // Find the CREATE_COIN condition to get the first DID coin
1804    let mut first_did_coin: Option<Coin> = None;
1805    for condition in conditions {
1806        if let Some(create_coin) = condition.into_create_coin() {
1807            // DID coins have odd amounts (singleton property)
1808            if create_coin.amount % 2 == 1 {
1809                first_did_coin = Some(Coin::new(
1810                    launcher_state.coin.coin_id(),
1811                    create_coin.puzzle_hash,
1812                    create_coin.amount,
1813                ));
1814                break;
1815            }
1816        }
1817    }
1818
1819    let first_did_coin = first_did_coin.ok_or(WalletError::Parse)?;
1820
1821    // Now we need to trace the DID through all its spends to find the current coin
1822    let mut current_did_coin = first_did_coin;
1823
1824    loop {
1825        // Check if this coin is spent
1826        let coin_states = peer
1827            .request_coin_state(
1828                vec![current_did_coin.coin_id()],
1829                None,
1830                match network {
1831                    TargetNetwork::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
1832                    TargetNetwork::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
1833                },
1834                false,
1835            )
1836            .await?
1837            .map_err(|_| WalletError::RejectCoinState)?
1838            .coin_states;
1839
1840        let coin_state = coin_states.first().ok_or(WalletError::UnknownCoin)?;
1841
1842        // If not spent, this is our current DID coin
1843        if coin_state.spent_height.is_none() {
1844            break;
1845        }
1846
1847        // If spent, find the child DID coin
1848        let spend_height = coin_state.spent_height.unwrap();
1849        let spend = peer
1850            .request_puzzle_and_solution(current_did_coin.coin_id(), spend_height)
1851            .await?
1852            .map_err(|_| WalletError::RejectPuzzleSolution)?;
1853
1854        // Parse the spend to find the child DID coin
1855        let spend_puzzle = spend.puzzle.to_clvm(&mut allocator)?;
1856        let spend_solution = spend.solution.to_clvm(&mut allocator)?;
1857
1858        let spend_output = clvmr::run_program(
1859            &mut allocator,
1860            &clvmr::ChiaDialect::new(0),
1861            spend_puzzle,
1862            spend_solution,
1863            u64::MAX,
1864        )
1865        .map_err(|_| WalletError::Clvm)?;
1866
1867        let spend_conditions = Vec::<Condition>::from_clvm(&allocator, spend_output.1)
1868            .map_err(|_| WalletError::Parse)?;
1869
1870        // Find the CREATE_COIN condition for the child DID
1871        let mut child_did_coin: Option<Coin> = None;
1872        for condition in spend_conditions {
1873            if let Some(create_coin) = condition.into_create_coin() {
1874                // DID coins have odd amounts (singleton property)
1875                if create_coin.amount % 2 == 1 {
1876                    child_did_coin = Some(Coin::new(
1877                        current_did_coin.coin_id(),
1878                        create_coin.puzzle_hash,
1879                        create_coin.amount,
1880                    ));
1881                    break;
1882                }
1883            }
1884        }
1885
1886        current_did_coin = child_did_coin.ok_or(WalletError::Parse)?;
1887    }
1888
1889    // Now generate the proof for the current DID coin
1890    let proof = generate_did_proof_from_chain(peer, current_did_coin, network).await?;
1891
1892    Ok((proof, current_did_coin))
1893}