use chia_wallet_sdk::driver::{Launcher, SpendContext};
use chia_wallet_sdk::types::conditions::CreateCoin;
use chia_wallet_sdk::types::{Condition, Conditions};
use hex_literal::hex;
use crate::context::{drain_coin_spends, inner_spend};
use crate::hint::{digstore_owner_hint, launcher_hint_for, StoreKind};
use crate::metadata::DigDataStoreMetadata;
use crate::size::SizeBucket;
use crate::types::{Bytes32, Coin, DelegatedPuzzle, MerkleCoinSpend, Owner};
use crate::{MerkleError, MerkleResult};
const SINGLETON_LAUNCHER_HASH: Bytes32 = Bytes32::new(hex!(
"eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9"
));
#[allow(clippy::too_many_arguments)]
pub fn mint_datastore(
parent_coin: Coin,
owner: Owner,
root_hash: Bytes32,
label: Option<String>,
description: Option<String>,
size_proof: Option<String>,
program_hash: Option<Bytes32>,
size_bucket: Option<SizeBucket>,
owner_puzzle_hash: Bytes32,
delegated_puzzles: Vec<DelegatedPuzzle>,
fee: u64,
) -> MerkleResult<MerkleCoinSpend> {
mint_datastore_with_kind(
StoreKind::File,
parent_coin,
owner,
root_hash,
label,
description,
size_proof,
program_hash,
size_bucket,
owner_puzzle_hash,
delegated_puzzles,
fee,
)
}
#[allow(clippy::too_many_arguments)]
pub fn mint_datastore_with_kind(
kind: StoreKind,
parent_coin: Coin,
owner: Owner,
root_hash: Bytes32,
label: Option<String>,
description: Option<String>,
size_proof: Option<String>,
program_hash: Option<Bytes32>,
size_bucket: Option<SizeBucket>,
owner_puzzle_hash: Bytes32,
delegated_puzzles: Vec<DelegatedPuzzle>,
fee: u64,
) -> MerkleResult<MerkleCoinSpend> {
let mut ctx = SpendContext::new();
let (launch_conditions, datastore) = Launcher::new(parent_coin.coin_id(), 1).mint_datastore(
&mut ctx,
DigDataStoreMetadata {
root_hash,
label,
description,
size_proof,
program_hash,
size_bucket,
},
owner_puzzle_hash.into(),
delegated_puzzles,
)?;
let launch_conditions =
override_launcher_hint(&mut ctx, launch_conditions, owner_puzzle_hash, kind)?;
let reserved = fee
.checked_add(1)
.ok_or_else(|| MerkleError::Chain("fee overflow: fee + 1 exceeds u64::MAX".into()))?;
let owner_conditions = if parent_coin.amount > reserved {
let change_hint = ctx.hint(owner_puzzle_hash)?;
launch_conditions.create_coin(
owner_puzzle_hash,
parent_coin.amount - reserved,
change_hint,
)
} else {
launch_conditions
};
let owner_spend = inner_spend(&mut ctx, owner, owner_conditions)?;
ctx.spend(parent_coin, owner_spend)?;
Ok(MerkleCoinSpend::new(
drain_coin_spends(&mut ctx),
Some(datastore),
))
}
fn override_launcher_hint(
ctx: &mut SpendContext,
conditions: Conditions,
owner_puzzle_hash: Bytes32,
kind: StoreKind,
) -> MerkleResult<Conditions> {
let mut rewritten = Conditions::new();
for condition in conditions {
match condition {
Condition::CreateCoin(create_coin)
if create_coin.puzzle_hash == SINGLETON_LAUNCHER_HASH =>
{
let memos = ctx.memos(&[
digstore_owner_hint(owner_puzzle_hash),
launcher_hint_for(kind),
])?;
rewritten = rewritten.with(Condition::CreateCoin(CreateCoin {
puzzle_hash: create_coin.puzzle_hash,
amount: create_coin.amount,
memos,
}));
}
other => rewritten = rewritten.with(other),
}
}
Ok(rewritten)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::required_signatures;
use crate::types::DataStore;
use chia_puzzle_types::standard::StandardArgs;
use chia_puzzle_types::Memos;
use chia_wallet_sdk::driver::SpendContext;
use chia_wallet_sdk::prelude::{NodePtr, MAINNET_CONSTANTS};
use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature};
use chia_wallet_sdk::test::Simulator;
use clvm_traits::{FromClvm, ToClvm};
fn seeded_owner() -> (chia_wallet_sdk::prelude::PublicKey, Bytes32) {
let mut sim = Simulator::new();
let owner = sim.bls(0);
let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
(owner.pk, owner_ph)
}
fn conditions_of(spend: &crate::types::CoinSpend) -> Vec<Condition> {
let mut ctx = SpendContext::new();
let puzzle = ctx.alloc(&spend.puzzle_reveal).expect("alloc puzzle");
let solution = ctx.alloc(&spend.solution).expect("alloc solution");
let output = ctx.run(puzzle, solution).expect("run puzzle");
Vec::<Condition>::from_clvm(&*ctx, output).expect("parse conditions")
}
fn launcher_memos(coin_spends: &[crate::types::CoinSpend]) -> Vec<Bytes32> {
for spend in coin_spends {
let mut ctx = SpendContext::new();
let puzzle = ctx.alloc(&spend.puzzle_reveal).expect("alloc puzzle");
let solution = ctx.alloc(&spend.solution).expect("alloc solution");
let output = ctx.run(puzzle, solution).expect("run puzzle");
let conditions = Vec::<Condition>::from_clvm(&*ctx, output).expect("parse conditions");
for condition in conditions {
if let Condition::CreateCoin(cc) = condition {
if cc.puzzle_hash == SINGLETON_LAUNCHER_HASH {
let Memos::Some(ptr) = cc.memos else {
panic!("launcher CREATE_COIN must carry memos");
};
return Vec::<Bytes32>::from_clvm(&*ctx, ptr)
.expect("parse launcher memos");
}
}
}
}
panic!("no launcher CREATE_COIN found");
}
#[test]
fn launcher_carries_the_two_memo_owner_discovery_hint() {
let (owner_pk, owner_ph) = seeded_owner();
let parent = Coin::new(Bytes32::new([0x33; 32]), owner_ph, 1_000_000);
let root = Bytes32::new([0xab; 32]);
let spend = mint_datastore(
parent,
Owner::Standard(owner_pk),
root,
None,
None,
None,
None,
None,
owner_ph,
vec![],
1_000,
)
.expect("mint builds");
let memos = launcher_memos(&spend.coin_spends);
assert_eq!(
memos,
vec![
digstore_owner_hint(owner_ph),
launcher_hint_for(StoreKind::File)
],
"launcher memos must be [owner_hint, launcher_hint] byte-for-byte"
);
}
#[test]
fn did_profile_mint_carries_the_profile_discriminator() {
use crate::hint::DID_PROFILE_LAUNCHER_HINT;
use crate::mint::mint_datastore_with_kind;
let (owner_pk, owner_ph) = seeded_owner();
let parent = Coin::new(Bytes32::new([0x55; 32]), owner_ph, 1_000_000);
let spend = mint_datastore_with_kind(
StoreKind::DidProfile,
parent,
Owner::Standard(owner_pk),
Bytes32::new([0xab; 32]),
None,
None,
None,
None,
None,
owner_ph,
vec![],
1_000,
)
.expect("did-profile mint builds");
let memos = launcher_memos(&spend.coin_spends);
assert_eq!(
memos,
vec![digstore_owner_hint(owner_ph), DID_PROFILE_LAUNCHER_HINT],
"a DidProfile mint carries the profile discriminator as memo[1]"
);
}
#[test]
fn metadata_clvm_encodes_root_as_first_atom() {
let mut ctx = SpendContext::new();
let root = Bytes32::new([0xcd; 32]);
let metadata = DigDataStoreMetadata {
root_hash: root,
label: Some("site".into()),
description: Some("desc".into()),
size_proof: None,
program_hash: None,
size_bucket: None,
};
let node = metadata.to_clvm(&mut *ctx).expect("encode metadata");
let (car, _rest) = <(Bytes32, NodePtr)>::from_clvm(&*ctx, node)
.expect("metadata is a pair with a Bytes32 car");
assert_eq!(car, root, "root_hash must be the first metadata atom");
}
#[test]
fn mint_validates_and_hydrates_on_simulator() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let owner = sim.bls(1_000_000);
let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
let root = Bytes32::new([0x5a; 32]);
let built = mint_datastore(
owner.coin,
Owner::Standard(owner.pk),
root,
Some("site".into()),
None,
None,
None,
None,
owner_ph,
vec![],
0,
)?;
let datastore = built.child.clone().expect("mint yields a child datastore");
sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
let mut ctx = SpendContext::new();
let launcher_spend = built
.coin_spends
.iter()
.find(|s| s.coin.coin_id() == datastore.info.launcher_id)
.expect("launcher-coin spend present");
let hydrated =
DataStore::<DigDataStoreMetadata>::from_spend(&mut ctx, launcher_spend, &[])?
.expect("launcher spend hydrates a datastore");
assert_eq!(hydrated.info.metadata.root_hash, root);
assert_eq!(hydrated.info.owner_puzzle_hash, owner_ph);
assert_eq!(hydrated.info.launcher_id, datastore.info.launcher_id);
assert!(hydrated.info.delegated_puzzles.is_empty());
Ok(())
}
#[test]
fn mint_requires_a_single_agg_sig_me_for_the_owner() {
let (owner_pk, owner_ph) = seeded_owner();
let parent = Coin::new(Bytes32::new([0x77; 32]), owner_ph, 500_000);
let built = mint_datastore(
parent,
Owner::Standard(owner_pk),
Bytes32::new([0x01; 32]),
None,
None,
None,
None,
None,
owner_ph,
vec![],
1_000,
)
.expect("mint builds");
let constants = AggSigConstants::from(&*MAINNET_CONSTANTS);
let required =
required_signatures(&built.coin_spends, &constants).expect("signatures compute");
assert_eq!(required.len(), 1, "one AGG_SIG_ME expected");
match &required[0] {
RequiredSignature::Bls(bls) => assert_eq!(bls.public_key, owner_pk),
RequiredSignature::Secp(_) => panic!("standard owner uses a BLS key"),
}
}
#[test]
fn mint_without_change_omits_the_change_coin() {
let (owner_pk, owner_ph) = seeded_owner();
let parent = Coin::new(Bytes32::new([0x99; 32]), owner_ph, 1);
let built = mint_datastore(
parent,
Owner::Standard(owner_pk),
Bytes32::new([0x02; 32]),
None,
None,
None,
None,
None,
owner_ph,
vec![],
0,
)
.expect("mint builds with no change");
let parent_spend = built
.coin_spends
.iter()
.find(|s| s.coin.coin_id() == parent.coin_id())
.expect("parent spend present");
let create_coins: Vec<_> = conditions_of(parent_spend)
.into_iter()
.filter(|c| matches!(c, Condition::CreateCoin(_)))
.collect();
assert_eq!(
create_coins.len(),
1,
"only the launcher CREATE_COIN, no change"
);
}
#[allow(clippy::too_many_arguments)]
fn reference_sdk_mint(
parent_coin: Coin,
owner_pk: chia_wallet_sdk::prelude::PublicKey,
root: Bytes32,
label: Option<String>,
description: Option<String>,
size_proof: Option<String>,
owner_puzzle_hash: Bytes32,
fee: u64,
) -> Vec<crate::types::CoinSpend> {
use chia_wallet_sdk::driver::DataStoreMetadata;
let mut ctx = SpendContext::new();
let (launch_conditions, _datastore) = Launcher::new(parent_coin.coin_id(), 1)
.mint_datastore(
&mut ctx,
DataStoreMetadata {
root_hash: root,
label,
description,
bytes: None,
size_proof,
},
owner_puzzle_hash.into(),
vec![],
)
.expect("reference mint builds");
let launch_conditions = override_launcher_hint(
&mut ctx,
launch_conditions,
owner_puzzle_hash,
StoreKind::File,
)
.expect("reference hint override");
let reserved = fee + 1;
let owner_conditions = if parent_coin.amount > reserved {
let change_hint = ctx.hint(owner_puzzle_hash).expect("hint");
launch_conditions.create_coin(
owner_puzzle_hash,
parent_coin.amount - reserved,
change_hint,
)
} else {
launch_conditions
};
let owner_spend =
crate::context::inner_spend(&mut ctx, Owner::Standard(owner_pk), owner_conditions)
.expect("reference owner spend");
ctx.spend(parent_coin, owner_spend)
.expect("reference parent spend");
crate::context::drain_coin_spends(&mut ctx)
}
#[test]
fn mint_none_program_hash_is_byte_identical() {
let (owner_pk, owner_ph) = seeded_owner();
let parent = Coin::new(Bytes32::new([0x44; 32]), owner_ph, 1_000_000);
let root = Bytes32::new([0xba; 32]);
let dig = mint_datastore(
parent,
Owner::Standard(owner_pk),
root,
Some("store".into()),
None,
None,
None,
None,
owner_ph,
vec![],
1_000,
)
.expect("dig mint builds");
let reference = reference_sdk_mint(
parent,
owner_pk,
root,
Some("store".into()),
None,
None,
owner_ph,
1_000,
);
assert_eq!(
dig.coin_spends, reference,
"a None-extras mint must be byte-identical to an SDK-metadata mint"
);
}
#[test]
fn mint_with_program_hash_hydrates() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let owner = sim.bls(1_000_000);
let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
let root = Bytes32::new([0x5b; 32]);
let program_hash = Bytes32::new([0xcc; 32]);
let built = mint_datastore(
owner.coin,
Owner::Standard(owner.pk),
root,
None,
None,
None,
Some(program_hash),
None,
owner_ph,
vec![],
0,
)?;
let datastore = built.child.clone().expect("mint yields a child datastore");
sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
let mut ctx = SpendContext::new();
let launcher_spend = built
.coin_spends
.iter()
.find(|s| s.coin.coin_id() == datastore.info.launcher_id)
.expect("launcher-coin spend present");
let hydrated =
DataStore::<DigDataStoreMetadata>::from_spend(&mut ctx, launcher_spend, &[])?
.expect("launcher spend hydrates a datastore");
assert_eq!(hydrated.info.metadata.root_hash, root);
assert_eq!(
hydrated.info.metadata.program_hash,
Some(program_hash),
"the program_hash survives the on-chain roundtrip"
);
Ok(())
}
#[test]
fn mint_with_size_bucket_hydrates() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let owner = sim.bls(1_000_000);
let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
let root = Bytes32::new([0x5c; 32]);
let size_bucket = SizeBucket::from_exponent(6).expect("valid bucket");
let built = mint_datastore(
owner.coin,
Owner::Standard(owner.pk),
root,
None,
None,
None,
None,
Some(size_bucket),
owner_ph,
vec![],
0,
)?;
let datastore = built.child.clone().expect("mint yields a child datastore");
sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
let mut ctx = SpendContext::new();
let launcher_spend = built
.coin_spends
.iter()
.find(|s| s.coin.coin_id() == datastore.info.launcher_id)
.expect("launcher-coin spend present");
let hydrated =
DataStore::<DigDataStoreMetadata>::from_spend(&mut ctx, launcher_spend, &[])?
.expect("launcher spend hydrates a datastore");
assert_eq!(hydrated.info.metadata.root_hash, root);
assert_eq!(
hydrated.info.metadata.size_bucket,
Some(size_bucket),
"the size bucket survives the on-chain roundtrip"
);
Ok(())
}
#[test]
fn mint_fee_overflow_fails_closed() {
let (owner_pk, owner_ph) = seeded_owner();
let parent = Coin::new(Bytes32::new([0xfe; 32]), owner_ph, 1_000_000);
let result = mint_datastore(
parent,
Owner::Standard(owner_pk),
Bytes32::new([0x03; 32]),
None,
None,
None,
None,
None,
owner_ph,
vec![],
u64::MAX,
);
assert!(
matches!(result, Err(MerkleError::Chain(_))),
"fee == u64::MAX must error, not panic or wrap"
);
}
}