datalayer_driver/
wallet.rs

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