Skip to main content

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_consensus::consensus_constants::ConsensusConstants;
14use chia_consensus::flags::{DONT_VALIDATE_SIGNATURE, MEMPOOL_MODE};
15use chia_consensus::owned_conditions::OwnedSpendBundleConditions;
16use chia_consensus::run_block_generator::run_block_generator;
17use chia_consensus::solution_generator::solution_generator;
18use chia_protocol::{
19    Bytes, Bytes32, Coin, CoinSpend, CoinState, CoinStateFilters, RejectHeaderRequest,
20    RequestBlockHeader, RequestFeeEstimates, RespondBlockHeader, RespondFeeEstimates, SpendBundle,
21    TransactionAck,
22};
23use chia_puzzle_types::{
24    nft::NftMetadata,
25    standard::{StandardArgs, StandardSolution},
26    DeriveSynthetic,
27};
28use chia_puzzles::SINGLETON_LAUNCHER_HASH;
29use chia_wallet_sdk::client::Peer;
30use chia_wallet_sdk::driver::{
31    get_merkle_tree, Datastore, DatastoreMetadata, DelegatedPuzzle, Did, DidInfo, DriverError,
32    HashedPtr, IntermediateLauncher, Launcher, Layer, NftMint, OracleLayer, SpendContext,
33    SpendWithConditions, StandardLayer, WriterLayer,
34};
35use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature, SignerError};
36use chia_wallet_sdk::types::{
37    announcement_id,
38    conditions::{CreateCoin, MeltSingleton, Memos, UpdateDatastoreMerkleRoot},
39    Condition, Conditions, MAINNET_CONSTANTS, TESTNET11_CONSTANTS,
40};
41use chia_wallet_sdk::utils::{self, CoinSelectionError};
42use clvm_traits::{clvm_tuple, FromClvm, ToClvm};
43use clvm_utils::tree_hash;
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
1129    let conds = run_block_generator::<&[u8], _>(
1130        &mut alloc,
1131        &generator,
1132        [],
1133        u64::MAX,
1134        MEMPOOL_MODE | DONT_VALIDATE_SIGNATURE,
1135        &Signature::default(),
1136        None,
1137        TargetNetwork::Mainnet.get_constants(),
1138    )?;
1139
1140    let conds = OwnedSpendBundleConditions::from(&alloc, conds);
1141
1142    Ok(conds.cost)
1143}
1144
1145pub struct PossibleLaunchersResponse {
1146    pub launcher_ids: Vec<Bytes32>,
1147    pub last_height: u32,
1148    pub last_header_hash: Bytes32,
1149}
1150
1151pub async fn look_up_possible_launchers(
1152    peer: &Peer,
1153    previous_height: Option<u32>,
1154    previous_header_hash: Bytes32,
1155) -> Result<PossibleLaunchersResponse, WalletError> {
1156    let resp = get_unspent_coin_states(
1157        peer,
1158        DATASTORE_LAUNCHER_HINT,
1159        previous_height,
1160        previous_header_hash,
1161        true,
1162    )
1163    .await?;
1164
1165    Ok(PossibleLaunchersResponse {
1166        last_header_hash: resp.last_header_hash,
1167        last_height: resp.last_height,
1168        launcher_ids: resp
1169            .coin_states
1170            .into_iter()
1171            .filter_map(|coin_state| {
1172                if coin_state.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() {
1173                    Some(coin_state.coin.coin_id())
1174                } else {
1175                    None
1176                }
1177            })
1178            .collect(),
1179    })
1180}
1181
1182pub async fn subscribe_to_coin_states(
1183    peer: &Peer,
1184    coin_id: Bytes32,
1185    previous_height: Option<u32>,
1186    previous_header_hash: Bytes32,
1187) -> Result<Option<u32>, WalletError> {
1188    let response = peer
1189        .request_coin_state(vec![coin_id], previous_height, previous_header_hash, true)
1190        .await
1191        .map_err(WalletError::Client)?
1192        .map_err(|_| WalletError::RejectCoinState)?;
1193
1194    if let Some(coin_state) = response.coin_states.first() {
1195        return Ok(coin_state.spent_height);
1196    }
1197
1198    Err(WalletError::UnknownCoin)
1199}
1200
1201pub async fn unsubscribe_from_coin_states(
1202    peer: &Peer,
1203    coin_id: Bytes32,
1204) -> Result<(), WalletError> {
1205    peer.remove_coin_subscriptions(Some(vec![coin_id]))
1206        .await
1207        .map_err(WalletError::Client)?;
1208
1209    Ok(())
1210}
1211
1212/// Mints a new NFT using a DID string.
1213///
1214/// # Arguments
1215/// * `peer` - The peer to query blockchain data
1216/// * `synthetic_key` - The synthetic key of the wallet
1217/// * `selected_coins` - Coins to spend for the transaction
1218/// * `did_string` - The DID string (e.g., "did:chia:1s8j4pquxfu5mhlldzu357qfqkwa9r35mdx5a0p0ehn76dr4ut4tqs0n6kv")
1219/// * `recipient_puzzle_hash` - The puzzle hash to send the NFT to
1220/// * `metadata` - The NFT metadata
1221/// * `royalty_puzzle_hash` - Optional royalty puzzle hash (defaults to recipient if None)
1222/// * `royalty_basis_points` - Royalty percentage in basis points (e.g., 300 = 3%)
1223/// * `fee` - Transaction fee
1224/// * `network` - The target network (mainnet/testnet)
1225///
1226/// # Returns
1227/// A vector of coin spends that mint the NFT
1228#[allow(clippy::too_many_arguments)]
1229pub async fn mint_nft(
1230    peer: &Peer,
1231    synthetic_key: PublicKey,
1232    selected_coins: Vec<Coin>,
1233    did_string: &str,
1234    recipient_puzzle_hash: Bytes32,
1235    metadata: NftMetadata,
1236    _royalty_puzzle_hash: Option<Bytes32>,
1237    royalty_basis_points: u16,
1238    fee: u64,
1239    network: TargetNetwork,
1240) -> Result<Vec<CoinSpend>, WalletError> {
1241    // Resolve the DID string to get the current coin and proof
1242    let (did_proof, did_coin) =
1243        resolve_did_string_and_generate_proof(peer, did_string, network).await?;
1244    let mut ctx = SpendContext::new();
1245
1246    // Convert DID proof
1247    let did_proof = match did_proof {
1248        chia_puzzle_types::Proof::Eve(eve) => Proof::Eve(EveProof {
1249            parent_parent_coin_info: eve.parent_parent_coin_info,
1250            parent_amount: eve.parent_amount,
1251        }),
1252        chia_puzzle_types::Proof::Lineage(lineage) => Proof::Lineage(LineageProof {
1253            parent_parent_coin_info: lineage.parent_parent_coin_info,
1254            parent_inner_puzzle_hash: lineage.parent_inner_puzzle_hash,
1255            parent_amount: lineage.parent_amount,
1256        }),
1257    };
1258
1259    // Create the DID singleton info (simplified DID structure)
1260    // Use the first 32 bytes of the public key (truncate from 48 to 32 bytes)
1261    let public_key_bytes = synthetic_key.derive_synthetic().to_bytes();
1262    let mut public_key_hash = [0u8; 32];
1263    public_key_hash.copy_from_slice(&public_key_bytes[..32]);
1264    let mut meta_data_allocator = Allocator::new();
1265    let node_metadata = metadata.to_clvm(&mut meta_data_allocator)?;
1266    let metadata_hashed_ptr = HashedPtr::from_ptr(&meta_data_allocator, node_metadata);
1267    let did_info: DidInfo = DidInfo::new(
1268        did_coin.coin_id(),
1269        None,
1270        1,
1271        metadata_hashed_ptr,
1272        public_key_hash.into(),
1273    );
1274
1275    let did = Did::new(did_coin, did_proof, did_info);
1276
1277    // Create StandardLayer for spending coins
1278    let p2 = StandardLayer::new(synthetic_key);
1279
1280    // Create the NFT mint configuration with metadata
1281    let nft_mint = NftMint::new(
1282        metadata_hashed_ptr,
1283        recipient_puzzle_hash,
1284        royalty_basis_points,
1285        None, // No DID owner for now - we'll set this up differently
1286    );
1287
1288    // Use IntermediateLauncher to mint the NFT
1289    let (mint_conditions, _nft) = IntermediateLauncher::new(did_coin.coin_id(), 0, 1)
1290        .create(&mut ctx)?
1291        .mint_nft(&mut ctx, &nft_mint)?;
1292
1293    // Update the DID with the mint conditions
1294    let _updated_did = did.update(&mut ctx, &p2, mint_conditions)?;
1295
1296    // Handle fee and change
1297    let total_input = selected_coins.iter().map(|coin| coin.amount).sum::<u64>();
1298    let total_needed = fee + 1; // 1 mojo for the NFT
1299
1300    if total_input < total_needed {
1301        return Err(WalletError::InsufficientCoinAmount); // Not enough coins
1302    }
1303
1304    let _change = total_input - total_needed;
1305    let change_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into();
1306
1307    // Spend the selected coins
1308    spend_coins_together(
1309        &mut ctx,
1310        synthetic_key,
1311        &selected_coins,
1312        Conditions::new().reserve_fee(fee),
1313        total_needed as i64,
1314        change_puzzle_hash,
1315    )?;
1316
1317    Ok(ctx.take())
1318}
1319
1320/// Generates a DID proof for a DID coin by analyzing its parent.
1321/// This is a simplified version that automatically determines the proof type.
1322///
1323/// # Arguments
1324/// * `peer` - The peer to query blockchain data
1325/// * `did_coin` - The DID coin to generate proof for
1326/// * `network` - The target network (mainnet/testnet)
1327///
1328/// # Returns
1329/// A tuple containing the DID proof and the DID coin
1330pub async fn generate_did_proof(
1331    peer: &Peer,
1332    did_coin: Coin,
1333    network: TargetNetwork,
1334) -> Result<(chia_puzzle_types::Proof, Coin), WalletError> {
1335    let proof = generate_did_proof_from_chain(peer, did_coin, network).await?;
1336    Ok((proof, did_coin))
1337}
1338
1339/// Generates a DID proof manually when you have the parent information.
1340///
1341/// # Arguments
1342/// * `did_coin` - The current DID coin
1343/// * `parent_coin` - The parent coin of the DID (None for eve proof)
1344/// * `parent_inner_puzzle_hash` - The parent's inner puzzle hash (for lineage proof)
1345///
1346/// # Returns
1347/// A DID proof that can be used to spend the DID coin
1348pub fn generate_did_proof_manual(
1349    did_coin: Coin,
1350    parent_coin: Option<Coin>,
1351    parent_inner_puzzle_hash: Option<Bytes32>,
1352) -> Result<chia_puzzle_types::Proof, WalletError> {
1353    match parent_coin {
1354        // Eve proof - first spend from launcher
1355        None => {
1356            // For eve proof, we need the launcher coin info
1357            // The parent_parent_coin_info is the coin that created the launcher
1358            // The parent_amount is the launcher coin amount (typically 1 mojo)
1359            Ok(chia_puzzle_types::Proof::Eve(chia_puzzle_types::EveProof {
1360                parent_parent_coin_info: did_coin.parent_coin_info,
1361                parent_amount: 1, // Launcher coins are typically 1 mojo
1362            }))
1363        }
1364        // Lineage proof - subsequent spends
1365        Some(parent) => {
1366            let parent_inner_puzzle_hash = parent_inner_puzzle_hash.ok_or(WalletError::Parse(
1367                "Parent inner puzzle hash is required".to_string(),
1368            ))?; // Need inner puzzle hash for lineage proof
1369
1370            Ok(chia_puzzle_types::Proof::Lineage(
1371                chia_puzzle_types::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
1381/// Generates a DID proof from a coin spend by analyzing the parent spend.
1382///
1383/// # Arguments
1384/// * `peer` - The peer to query blockchain data
1385/// * `did_coin` - The DID coin to generate proof for
1386/// * `network` - The target network (mainnet/testnet)
1387///
1388/// # Returns
1389/// A DID proof that can be used to spend the DID coin
1390pub async fn generate_did_proof_from_chain(
1391    peer: &Peer,
1392    did_coin: Coin,
1393    network: TargetNetwork,
1394) -> Result<chia_puzzle_types::Proof, WalletError> {
1395    // Get the parent coin state
1396    let parent_coin_states = peer
1397        .request_coin_state(
1398            vec![did_coin.parent_coin_info],
1399            None,
1400            match network {
1401                TargetNetwork::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
1402                TargetNetwork::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
1403            },
1404            false,
1405        )
1406        .await?
1407        .map_err(|_| WalletError::RejectCoinState)?
1408        .coin_states;
1409
1410    let parent_coin_state = parent_coin_states.first().ok_or(WalletError::UnknownCoin)?;
1411
1412    // Check if parent is a launcher (puzzle hash matches singleton launcher)
1413    if parent_coin_state.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() {
1414        // This is an eve proof - first spend from launcher
1415        return Ok(chia_puzzle_types::Proof::Eve(chia_puzzle_types::EveProof {
1416            parent_parent_coin_info: parent_coin_state.coin.parent_coin_info,
1417            parent_amount: parent_coin_state.coin.amount,
1418        }));
1419    }
1420
1421    // This is a lineage proof - need to get the parent's puzzle and solution
1422    let parent_spend_height = parent_coin_state
1423        .spent_height
1424        .ok_or(WalletError::UnknownCoin)?;
1425
1426    let _parent_spend = peer
1427        .request_puzzle_and_solution(parent_coin_state.coin.coin_id(), parent_spend_height)
1428        .await?
1429        .map_err(|_| WalletError::RejectPuzzleSolution)?;
1430
1431    let _allocator = Allocator::new();
1432
1433    // For now, create a basic lineage proof
1434    // This is a simplified approach - in production you'd want to properly parse the parent DID
1435    Ok(chia_puzzle_types::Proof::Lineage(
1436        chia_puzzle_types::LineageProof {
1437            parent_parent_coin_info: parent_coin_state.coin.parent_coin_info,
1438            parent_inner_puzzle_hash: Bytes32::default(), // Would need to parse from parent spend
1439            parent_amount: parent_coin_state.coin.amount,
1440        },
1441    ))
1442}
1443
1444/// Creates a simple DID from a private key and selected coins.
1445///
1446/// # Arguments
1447/// * `synthetic_key` - The synthetic key that will control the DID
1448/// * `selected_coins` - Coins to spend for creating the DID
1449/// * `fee` - Transaction fee
1450///
1451/// # Returns
1452/// A tuple containing the coin spends and the created DID coin
1453pub fn create_simple_did(
1454    synthetic_key: PublicKey,
1455    selected_coins: Vec<Coin>,
1456    fee: u64,
1457) -> Result<(Vec<CoinSpend>, Coin), WalletError> {
1458    let mut ctx = SpendContext::new();
1459
1460    let p2 = StandardLayer::new(synthetic_key);
1461    let puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into();
1462
1463    // Calculate total input and needed amount
1464    let total_input = selected_coins.iter().map(|coin| coin.amount).sum::<u64>();
1465    let total_needed = fee + 1; // 1 mojo for the DID
1466
1467    if total_input < total_needed {
1468        return Err(WalletError::InsufficientCoinAmount); // Not enough coins
1469    }
1470
1471    let change = total_input - total_needed;
1472
1473    // Create the DID using the first coin as the parent for the launcher
1474    let first_coin = selected_coins[0];
1475    let launcher = Launcher::new(first_coin.coin_id(), 1);
1476
1477    // Create the DID
1478    let (create_did_conditions, did) = launcher.create_simple_did(&mut ctx, &p2)?;
1479
1480    // Spend all selected coins together
1481    let first_coin_id = first_coin.coin_id();
1482
1483    for (i, &coin) in selected_coins.iter().enumerate() {
1484        if i == 0 {
1485            // First coin creates the DID and handles change/fee
1486            let mut conditions = create_did_conditions.clone();
1487
1488            if change > 0 {
1489                let hint = ctx.hint(puzzle_hash)?;
1490                conditions = conditions.create_coin(puzzle_hash, change, hint);
1491            }
1492
1493            if fee > 0 {
1494                conditions = conditions.reserve_fee(fee);
1495            }
1496
1497            p2.spend(&mut ctx, coin, conditions)?;
1498        } else {
1499            // Other coins just assert concurrent spend
1500            p2.spend(
1501                &mut ctx,
1502                coin,
1503                Conditions::new().assert_concurrent_spend(first_coin_id),
1504            )?;
1505        }
1506    }
1507
1508    Ok((ctx.take(), did.coin))
1509}
1510
1511/// Resolves a DID string to find the current DID coin and generates its proof.
1512///
1513/// # Arguments
1514/// * `peer` - The peer to query blockchain data
1515/// * `did_string` - The DID string (e.g., "did:chia:1s8j4pquxfu5mhlldzu357qfqkwa9r35mdx5a0p0ehn76dr4ut4tqs0n6kv")
1516/// * `network` - The target network (mainnet/testnet)
1517///
1518/// # Returns
1519/// A tuple containing the DID proof and the current DID coin
1520pub async fn resolve_did_string_and_generate_proof(
1521    peer: &Peer,
1522    did_string: &str,
1523    network: TargetNetwork,
1524) -> Result<(chia_puzzle_types::Proof, Coin), WalletError> {
1525    // Parse DID string to extract launcher ID
1526    let parts: Vec<&str> = did_string.split(':').collect();
1527
1528    if parts.len() != 3 || parts[0] != "did" || parts[1] != "chia" {
1529        return Err(WalletError::Parse("Invalid DID string".to_string()));
1530    }
1531
1532    let bech32_part = parts[2];
1533
1534    // Decode the bech32 address to get the launcher ID
1535    use chia_wallet_sdk::utils::Address;
1536    let address = Address::decode(bech32_part)
1537        .map_err(|_| WalletError::Parse("Cannot decode address".to_string()))?;
1538
1539    let did_id = address.puzzle_hash;
1540
1541    // First, get the launcher coin state to find the first DID coin
1542    let launcher_states = peer
1543        .request_coin_state(
1544            vec![did_id],
1545            None,
1546            match network {
1547                TargetNetwork::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
1548                TargetNetwork::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
1549            },
1550            false,
1551        )
1552        .await?
1553        .map_err(|_| WalletError::RejectCoinState)?
1554        .coin_states;
1555
1556    let launcher_state = launcher_states.first().ok_or(WalletError::UnknownCoin)?;
1557
1558    // Verify this is actually a launcher
1559    if launcher_state.coin.puzzle_hash != SINGLETON_LAUNCHER_HASH.into() {
1560        return Err(WalletError::PuzzleHashMismatch(
1561            "Coin puzzle hash does not match datastore singleton launcher hash".to_string(),
1562        ));
1563    }
1564
1565    // Get the spend of the launcher to find the first DID coin
1566    let launcher_spend_height = launcher_state
1567        .spent_height
1568        .ok_or(WalletError::UnknownCoin)?;
1569
1570    let launcher_spend = peer
1571        .request_puzzle_and_solution(launcher_state.coin.coin_id(), launcher_spend_height)
1572        .await?
1573        .map_err(|_| WalletError::RejectPuzzleSolution)?;
1574
1575    let mut allocator = Allocator::new();
1576
1577    // Run the launcher spend to find the created DID coin
1578    let launcher_puzzle = launcher_spend.puzzle.to_clvm(&mut allocator)?;
1579    let launcher_solution = launcher_spend.solution.to_clvm(&mut allocator)?;
1580
1581    let output = clvmr::run_program(
1582        &mut allocator,
1583        &clvmr::ChiaDialect::new(0),
1584        launcher_puzzle,
1585        launcher_solution,
1586        u64::MAX,
1587    )
1588    .map_err(|_| WalletError::Clvm)?;
1589
1590    let conditions =
1591        Vec::<Condition>::from_clvm(&allocator, output.1).map_err(|_| WalletError::Clvm)?;
1592
1593    // Find the CREATE_COIN condition to get the first DID coin
1594    let mut first_did_coin: Option<Coin> = None;
1595    for condition in conditions {
1596        if let Some(create_coin) = condition.into_create_coin() {
1597            // DID coins have odd amounts (singleton property)
1598            if create_coin.amount % 2 == 1 {
1599                first_did_coin = Some(Coin::new(
1600                    launcher_state.coin.coin_id(),
1601                    create_coin.puzzle_hash,
1602                    create_coin.amount,
1603                ));
1604                break;
1605            }
1606        }
1607    }
1608
1609    let first_did_coin = first_did_coin.ok_or(WalletError::UnknownCoin)?;
1610
1611    // Now we need to trace the DID through all its spends to find the current coin
1612    let mut current_did_coin = first_did_coin;
1613
1614    loop {
1615        // Check if this coin is spent
1616        let coin_states = peer
1617            .request_coin_state(
1618                vec![current_did_coin.coin_id()],
1619                None,
1620                match network {
1621                    TargetNetwork::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
1622                    TargetNetwork::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
1623                },
1624                false,
1625            )
1626            .await?
1627            .map_err(|_| WalletError::RejectCoinState)?
1628            .coin_states;
1629
1630        let coin_state = coin_states.first().ok_or(WalletError::UnknownCoin)?;
1631
1632        // If not spent, this is our current DID coin
1633        if coin_state.spent_height.is_none() {
1634            break;
1635        }
1636
1637        // If spent, find the child DID coin
1638        let spend_height = coin_state.spent_height.unwrap();
1639        let spend = peer
1640            .request_puzzle_and_solution(current_did_coin.coin_id(), spend_height)
1641            .await?
1642            .map_err(|_| WalletError::RejectPuzzleSolution)?;
1643
1644        // Parse the spend to find the child DID coin
1645        let spend_puzzle = spend.puzzle.to_clvm(&mut allocator)?;
1646        let spend_solution = spend.solution.to_clvm(&mut allocator)?;
1647
1648        let spend_output = clvmr::run_program(
1649            &mut allocator,
1650            &clvmr::ChiaDialect::new(0),
1651            spend_puzzle,
1652            spend_solution,
1653            u64::MAX,
1654        )
1655        .map_err(|_| WalletError::Clvm)?;
1656
1657        let spend_conditions = Vec::<Condition>::from_clvm(&allocator, spend_output.1)
1658            .map_err(|_| WalletError::Clvm)?;
1659
1660        // Find the CREATE_COIN condition for the child DID
1661        let mut child_did_coin: Option<Coin> = None;
1662        for condition in spend_conditions {
1663            if let Some(create_coin) = condition.into_create_coin() {
1664                // DID coins have odd amounts (singleton property)
1665                if create_coin.amount % 2 == 1 {
1666                    child_did_coin = Some(Coin::new(
1667                        current_did_coin.coin_id(),
1668                        create_coin.puzzle_hash,
1669                        create_coin.amount,
1670                    ));
1671                    break;
1672                }
1673            }
1674        }
1675
1676        current_did_coin = child_did_coin.ok_or(WalletError::UnknownCoin)?;
1677    }
1678
1679    // Now generate the proof for the current DID coin
1680    let proof = generate_did_proof_from_chain(peer, current_did_coin, network).await?;
1681
1682    Ok((proof, current_did_coin))
1683}
1684
1685#[cfg(test)]
1686mod melt_kat {
1687    //! Custody KAT pinning `Datastore::from_spend`'s melt signal across the
1688    //! chia-wallet-sdk 0.34 -> 0.36 move (dig_ecosystem#2133, #3161).
1689    //!
1690    //! The just-merged digstore-chain #1981 melt classifier depends on the load-
1691    //! bearing fact that a childless datastore singleton spend (an owner melt)
1692    //! surfaces as `Err(DriverError::MissingChild)`, while a spend that recreates
1693    //! the datastore surfaces as `Ok(Some(_))`. This test drives a real
1694    //! peer-simulator mint -> melt and asserts both signals hold under 0.36.
1695    //!
1696    //! Its expected values are UNCHANGED by the 0.36 adoption: only the type's
1697    //! spelling moved (`DataStore` -> `Datastore`). Had either signal changed,
1698    //! this test would have gone red rather than needing an edit — which is the
1699    //! evidence that the melt classifier downstream is still safe.
1700    use super::*;
1701    use chia_wallet_sdk::test::{BlsPair, Simulator};
1702
1703    #[test]
1704    fn from_spend_reports_owner_melt_as_missing_child() -> anyhow::Result<()> {
1705        let mut sim = Simulator::new();
1706        let owner = BlsPair::default();
1707
1708        // In the simulator the standard puzzle is curried directly on the pair's
1709        // public key, so that key doubles as the "synthetic" key our wallet API
1710        // expects and the pair's secret key signs the spends.
1711        let owner_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
1712        let funding_coin = sim.new_coin(owner_puzzle_hash, 1);
1713
1714        // Mint a datastore (no delegation layers, zero fee) and land it on chain.
1715        let minted = mint_store(
1716            owner.pk,
1717            vec![funding_coin],
1718            Bytes32::new([1; 32]),
1719            None,
1720            None,
1721            None,
1722            None,
1723            owner_puzzle_hash,
1724            vec![],
1725            0,
1726        )?;
1727        let datastore = minted.new_datastore.clone();
1728        sim.spend_coins(minted.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
1729
1730        // Positive control: the launcher spend that CREATES the datastore (it
1731        // recreates the singleton with an odd-amount child) must be recognised as
1732        // a datastore, i.e. `Ok(Some(_))`. This proves `from_spend` genuinely
1733        // inspects the recreated child rather than returning the melt signal for
1734        // every datastore singleton spend.
1735        let mut ctx = SpendContext::new();
1736        let launcher_spend = minted
1737            .coin_spends
1738            .iter()
1739            .find(|cs| cs.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH.into())
1740            .expect("mint must contain the singleton launcher spend");
1741        let launched = Datastore::<DatastoreMetadata>::from_spend(&mut ctx, launcher_spend, &[])?;
1742        assert!(
1743            launched.is_some(),
1744            "from_spend must recognise the datastore-creating launcher spend as Ok(Some)"
1745        );
1746
1747        // Melt the datastore and land the melt on chain (proving it is a valid,
1748        // fully-executable datastore singleton spend, not a malformed one).
1749        let melt_spends = melt_store(datastore, owner.pk)?;
1750        assert_eq!(melt_spends.len(), 1, "melt produces exactly one spend");
1751        sim.spend_coins(melt_spends.clone(), std::slice::from_ref(&owner.sk))?;
1752
1753        // The pinned property: a valid datastore singleton spend that recreates no
1754        // odd-amount child (the owner melt) is reported as `Err(MissingChild)`.
1755        let mut ctx = SpendContext::new();
1756        let result = Datastore::<DatastoreMetadata>::from_spend(&mut ctx, &melt_spends[0], &[]);
1757        assert!(
1758            matches!(result, Err(DriverError::MissingChild)),
1759            "0.36 must still surface an owner melt as Err(DriverError::MissingChild), got {result:?}"
1760        );
1761
1762        Ok(())
1763    }
1764}