1use chia_bls::PublicKey;
9use chia_protocol::{Bytes, Bytes32, Coin, CoinSpend, SpendBundle};
10use chia_puzzle_types::standard::StandardArgs;
11use chia_puzzles::SINGLETON_LAUNCHER_HASH;
12use chia_sdk_driver::{
13 get_merkle_tree, Datastore as DataStore, DatastoreMetadata as DataStoreMetadata,
14 DelegatedPuzzle, DriverError, Launcher, Layer, OracleLayer, SpendContext, SpendWithConditions,
15 StandardLayer, WriterLayer,
16};
17use chia_sdk_types::{
18 announcement_id,
19 conditions::{CreateCoin, MeltSingleton, UpdateDatastoreMerkleRoot},
20 Condition, Conditions,
21};
22use clvm_traits::ToClvm;
23use hex_literal::hex;
24
25use crate::error::{Error, WalletError};
26use crate::types::SuccessResponse;
27
28pub const DATASTORE_LAUNCHER_HINT: Bytes32 = Bytes32::new(hex!(
30 "aa7e5b234e1d55967bf0a316395a2eab6cb3370332c0f251f0e44a5afb84fc68"
31));
32
33pub const DIGSTORE_OWNER_HINT_DOMAIN: &[u8] = b"dig:datastore:owner:v1";
49
50pub fn digstore_owner_hint(owner_puzzle_hash: Bytes32) -> Bytes32 {
65 let mut h = chia_sha2::Sha256::new();
66 h.update(DIGSTORE_OWNER_HINT_DOMAIN);
67 h.update(owner_puzzle_hash);
68 Bytes32::new(h.finalize())
69}
70
71pub fn admin_delegated_puzzle_from_key(synthetic_key: &PublicKey) -> DelegatedPuzzle {
97 DelegatedPuzzle::Admin(StandardArgs::curry_tree_hash(*synthetic_key))
98}
99
100pub fn writer_delegated_puzzle_from_key(synthetic_key: &PublicKey) -> DelegatedPuzzle {
112 DelegatedPuzzle::Writer(StandardArgs::curry_tree_hash(*synthetic_key))
113}
114
115pub fn oracle_delegated_puzzle(oracle_puzzle_hash: Bytes32, oracle_fee: u64) -> DelegatedPuzzle {
121 DelegatedPuzzle::Oracle(oracle_puzzle_hash, oracle_fee)
122}
123
124#[allow(clippy::too_many_arguments)]
140pub fn mint_store(
141 minter_synthetic_key: PublicKey,
142 selected_coins: Vec<Coin>,
143 root_hash: Bytes32,
144 label: Option<String>,
145 description: Option<String>,
146 bytes: Option<u64>,
147 program_hash: Option<String>,
148 owner_puzzle_hash: Bytes32,
149 delegated_puzzles: Vec<DelegatedPuzzle>,
150 fee: u64,
151) -> Result<SuccessResponse, WalletError> {
152 if selected_coins.is_empty() {
153 return Err(WalletError::Parse("selected_coins is empty".to_string()));
154 }
155
156 let minter_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(minter_synthetic_key).into();
157 let total_amount_from_coins = selected_coins.iter().map(|c| c.amount).sum::<u64>();
158
159 let total_amount = fee + 1;
160
161 let mut ctx = SpendContext::new();
162
163 let p2 = StandardLayer::new(minter_synthetic_key);
164
165 let lead_coin = selected_coins[0];
166 let lead_coin_name = lead_coin.coin_id();
167
168 for coin in selected_coins.into_iter().skip(1) {
169 p2.spend(
170 &mut ctx,
171 coin,
172 Conditions::new().assert_concurrent_spend(lead_coin_name),
173 )?;
174 }
175
176 let (launch_singleton, datastore) = Launcher::new(lead_coin_name, 1).mint_datastore(
177 &mut ctx,
178 DataStoreMetadata {
179 root_hash,
180 label,
181 description,
182 bytes,
183 size_proof: program_hash,
184 },
185 owner_puzzle_hash.into(),
186 delegated_puzzles,
187 )?;
188
189 let launch_singleton = Conditions::new().extend(
190 launch_singleton
191 .into_iter()
192 .map(|cond| {
193 if let Condition::CreateCoin(cc) = cond {
194 if cc.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() {
195 let hint = ctx.memos(&[
200 digstore_owner_hint(owner_puzzle_hash),
201 DATASTORE_LAUNCHER_HINT,
202 ])?;
203
204 return Ok(Condition::CreateCoin(CreateCoin {
205 puzzle_hash: cc.puzzle_hash,
206 amount: cc.amount,
207 memos: hint,
208 }));
209 }
210
211 return Ok(Condition::CreateCoin(cc));
212 }
213
214 Ok(cond)
215 })
216 .collect::<Result<Vec<_>, WalletError>>()?,
217 );
218
219 let lead_coin_conditions = if total_amount_from_coins > total_amount {
220 let hint = ctx.hint(minter_puzzle_hash)?;
221
222 launch_singleton.create_coin(
223 minter_puzzle_hash,
224 total_amount_from_coins - total_amount,
225 hint,
226 )
227 } else {
228 launch_singleton
229 };
230 p2.spend(&mut ctx, lead_coin, lead_coin_conditions)?;
231
232 Ok(SuccessResponse {
233 coin_spends: ctx.take(),
234 new_datastore: datastore,
235 })
236}
237
238pub fn datastore_from_spend(
245 parent_spend: CoinSpend,
246 prev_delegated: Vec<DelegatedPuzzle>,
247) -> Result<DataStore, WalletError> {
248 let ctx = &mut SpendContext::new();
249 DataStore::<DataStoreMetadata>::from_spend(ctx, &parent_spend, &prev_delegated)?
250 .ok_or_else(|| WalletError::Parse("coin spend is not a DataStore spend".to_string()))
251}
252
253#[derive(Clone, Debug)]
260pub enum DataStoreInnerSpend {
261 Owner(PublicKey),
263 Admin(PublicKey),
265 Writer(PublicKey),
267}
268
269fn update_store_with_conditions(
270 ctx: &mut SpendContext,
271 conditions: Conditions,
272 datastore: DataStore,
273 inner_spend_info: DataStoreInnerSpend,
274 allow_admin: bool,
275 allow_writer: bool,
276) -> Result<SuccessResponse, WalletError> {
277 let inner_datastore_spend = match inner_spend_info {
278 DataStoreInnerSpend::Owner(pk) => {
279 StandardLayer::new(pk).spend_with_conditions(ctx, conditions)?
280 }
281 DataStoreInnerSpend::Admin(pk) => {
282 if !allow_admin {
283 return Err(WalletError::Permission);
284 }
285
286 StandardLayer::new(pk).spend_with_conditions(ctx, conditions)?
287 }
288 DataStoreInnerSpend::Writer(pk) => {
289 if !allow_writer {
290 return Err(WalletError::Permission);
291 }
292
293 WriterLayer::new(StandardLayer::new(pk)).spend(ctx, conditions)?
294 }
295 };
296
297 let parent_delegated_puzzles = datastore.info.delegated_puzzles.clone();
298 let new_spend = datastore.spend(ctx, inner_datastore_spend)?;
299
300 let new_datastore =
301 DataStore::<DataStoreMetadata>::from_spend(ctx, &new_spend, &parent_delegated_puzzles)?
302 .ok_or(WalletError::Parse("Store from spend is None".to_string()))?;
303
304 Ok(SuccessResponse {
305 coin_spends: vec![new_spend],
306 new_datastore,
307 })
308}
309
310pub fn update_store_ownership(
323 datastore: DataStore,
324 new_owner_puzzle_hash: Bytes32,
325 new_delegated_puzzles: Vec<DelegatedPuzzle>,
326 inner_spend_info: DataStoreInnerSpend,
327) -> Result<SuccessResponse, WalletError> {
328 let ctx = &mut SpendContext::new();
329
330 let update_condition: Condition = match inner_spend_info {
331 DataStoreInnerSpend::Owner(_) => {
332 DataStore::<DataStoreMetadata>::owner_create_coin_condition(
333 ctx,
334 datastore.info.launcher_id,
335 new_owner_puzzle_hash,
336 new_delegated_puzzles,
337 true,
338 )?
339 }
340 DataStoreInnerSpend::Admin(_) => {
341 let merkle_tree = get_merkle_tree(ctx, new_delegated_puzzles.clone())?;
342
343 let new_merkle_root_condition = UpdateDatastoreMerkleRoot {
344 new_merkle_root: merkle_tree.root(),
345 memos: DataStore::<DataStoreMetadata>::get_recreation_memos(
346 datastore.info.launcher_id,
347 new_owner_puzzle_hash.into(),
348 new_delegated_puzzles,
349 ),
350 }
351 .to_clvm(&mut **ctx)
352 .map_err(DriverError::ToClvm)?;
353
354 Condition::Other(new_merkle_root_condition)
355 }
356 _ => return Err(WalletError::Permission),
357 };
358
359 let update_conditions = Conditions::new().with(update_condition);
360
361 update_store_with_conditions(
362 ctx,
363 update_conditions,
364 datastore,
365 inner_spend_info,
366 true,
367 false,
368 )
369}
370
371pub fn update_store_metadata(
383 datastore: DataStore,
384 new_root_hash: Bytes32,
385 new_label: Option<String>,
386 new_description: Option<String>,
387 new_bytes: Option<u64>,
388 new_program_hash: Option<String>,
389 inner_spend_info: DataStoreInnerSpend,
390) -> Result<SuccessResponse, WalletError> {
391 let ctx = &mut SpendContext::new();
392
393 let new_metadata = DataStoreMetadata {
394 root_hash: new_root_hash,
395 label: new_label,
396 description: new_description,
397 bytes: new_bytes,
398 size_proof: new_program_hash,
399 };
400 let mut new_metadata_condition = Conditions::new().with(
401 DataStore::<DataStoreMetadata>::new_metadata_condition(ctx, new_metadata)?,
402 );
403
404 if let DataStoreInnerSpend::Owner(_) = inner_spend_info {
405 new_metadata_condition = new_metadata_condition.with(
406 DataStore::<DataStoreMetadata>::owner_create_coin_condition(
407 ctx,
408 datastore.info.launcher_id,
409 datastore.info.owner_puzzle_hash,
410 datastore.info.delegated_puzzles.clone(),
411 false,
412 )?,
413 );
414 }
415
416 update_store_with_conditions(
417 ctx,
418 new_metadata_condition,
419 datastore,
420 inner_spend_info,
421 true,
422 true,
423 )
424}
425
426pub fn melt_store(
435 datastore: DataStore,
436 owner_pk: PublicKey,
437) -> Result<Vec<CoinSpend>, WalletError> {
438 let ctx = &mut SpendContext::new();
439
440 let melt_conditions = Conditions::new()
441 .with(Condition::reserve_fee(1))
442 .with(Condition::Other(
443 MeltSingleton {}
444 .to_clvm(&mut **ctx)
445 .map_err(DriverError::ToClvm)?,
446 ));
447
448 let inner_datastore_spend =
449 StandardLayer::new(owner_pk).spend_with_conditions(ctx, melt_conditions)?;
450
451 let new_spend = datastore.spend(ctx, inner_datastore_spend)?;
452
453 Ok(vec![new_spend])
454}
455
456pub fn oracle_spend(
469 spender_synthetic_key: PublicKey,
470 selected_coins: Vec<Coin>,
471 datastore: DataStore,
472 fee: u64,
473) -> Result<SuccessResponse, WalletError> {
474 if selected_coins.is_empty() {
475 return Err(WalletError::Parse("selected_coins is empty".to_string()));
476 }
477
478 let Some(DelegatedPuzzle::Oracle(oracle_ph, oracle_fee)) = datastore
479 .info
480 .delegated_puzzles
481 .iter()
482 .find(|dp| matches!(dp, DelegatedPuzzle::Oracle(_, _)))
483 else {
484 return Err(WalletError::Permission);
485 };
486
487 let spender_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(spender_synthetic_key).into();
488
489 let total_amount = oracle_fee + fee;
490
491 let ctx = &mut SpendContext::new();
492
493 let p2 = StandardLayer::new(spender_synthetic_key);
494
495 let lead_coin = selected_coins[0];
496 let lead_coin_name = lead_coin.coin_id();
497
498 let total_amount_from_coins = selected_coins.iter().map(|c| c.amount).sum::<u64>();
499 for coin in selected_coins.into_iter().skip(1) {
500 p2.spend(
501 ctx,
502 coin,
503 Conditions::new().assert_concurrent_spend(lead_coin_name),
504 )?;
505 }
506
507 let assert_oracle_conds = Conditions::new().assert_puzzle_announcement(announcement_id(
508 datastore.coin.puzzle_hash,
509 Bytes::new("$".into()),
510 ));
511
512 let mut lead_coin_conditions = assert_oracle_conds;
513 if total_amount_from_coins > total_amount {
514 let hint = ctx.hint(spender_puzzle_hash)?;
515
516 lead_coin_conditions = lead_coin_conditions.create_coin(
517 spender_puzzle_hash,
518 total_amount_from_coins - total_amount,
519 hint,
520 );
521 }
522 if fee > 0 {
523 lead_coin_conditions = lead_coin_conditions.reserve_fee(fee);
524 }
525 p2.spend(ctx, lead_coin, lead_coin_conditions)?;
526
527 let inner_datastore_spend = OracleLayer::new(*oracle_ph, *oracle_fee)
528 .ok_or(DriverError::OddOracleFee)?
529 .construct_spend(ctx, ())?;
530
531 let parent_delegated_puzzles = datastore.info.delegated_puzzles.clone();
532 let new_spend = datastore.spend(ctx, inner_datastore_spend)?;
533
534 let new_datastore = DataStore::from_spend(ctx, &new_spend, &parent_delegated_puzzles)?
535 .ok_or(WalletError::Parse("Store from spend is None".to_string()))?;
536 ctx.insert(new_spend.clone());
537
538 Ok(SuccessResponse {
539 coin_spends: ctx.take(),
540 new_datastore,
541 })
542}
543
544pub fn add_fee(
548 spender_synthetic_key: PublicKey,
549 selected_coins: Vec<Coin>,
550 coin_ids: Vec<Bytes32>,
551 fee: u64,
552) -> Result<Vec<CoinSpend>, WalletError> {
553 if selected_coins.is_empty() {
554 return Err(WalletError::Parse("selected_coins is empty".to_string()));
555 }
556 let spender_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(spender_synthetic_key).into();
557 let total_amount_from_coins = selected_coins.iter().map(|c| c.amount).sum::<u64>();
558
559 let mut ctx = SpendContext::new();
560 let p2 = StandardLayer::new(spender_synthetic_key);
561
562 let lead_coin = selected_coins[0];
563 let lead_coin_name = lead_coin.coin_id();
564
565 for coin in selected_coins.into_iter().skip(1) {
566 p2.spend(
567 &mut ctx,
568 coin,
569 Conditions::new().assert_concurrent_spend(lead_coin_name),
570 )?;
571 }
572
573 let mut lead_coin_conditions = Conditions::new().reserve_fee(fee);
574 if total_amount_from_coins > fee {
575 let hint = ctx.hint(spender_puzzle_hash)?;
576 lead_coin_conditions = lead_coin_conditions.create_coin(
577 spender_puzzle_hash,
578 total_amount_from_coins - fee,
579 hint,
580 );
581 }
582 for coin_id in coin_ids {
583 lead_coin_conditions = lead_coin_conditions.assert_concurrent_spend(coin_id);
584 }
585
586 p2.spend(&mut ctx, lead_coin, lead_coin_conditions)?;
587
588 Ok(ctx.take())
589}
590
591pub fn hex_spend_bundle_to_coin_spends(hex_str: &str) -> Result<Vec<CoinSpend>, Error> {
593 use chia_traits::Streamable;
594 let bytes = hex::decode(hex_str).map_err(|e| Error::Parse(format!("hex decode: {e}")))?;
595 let spend_bundle =
596 SpendBundle::from_bytes(&bytes).map_err(|e| Error::Parse(format!("bundle parse: {e}")))?;
597 Ok(spend_bundle.coin_spends)
598}
599
600pub fn spend_bundle_to_hex(spend_bundle: &SpendBundle) -> Result<String, Error> {
602 use chia_traits::Streamable;
603 let bytes = spend_bundle
604 .to_bytes()
605 .map_err(|e| Error::Parse(format!("bundle serialize: {e}")))?;
606 Ok(hex::encode(bytes))
607}