use chia_puzzle_types::standard::StandardArgs;
use chia_wallet_sdk::driver::{Datastore, SpendContext};
use chia_wallet_sdk::types::Conditions;
use crate::context::inner_spend;
use crate::metadata::DigDataStoreMetadata;
use crate::types::{MerkleCoinSpend, Owner};
use crate::{MerkleError, MerkleResult};
pub fn melt(
store: &Datastore<DigDataStoreMetadata>,
owner: Owner,
) -> MerkleResult<MerkleCoinSpend> {
if matches!(owner, Owner::Custom(_)) {
return Err(MerkleError::UnsupportedOwner(
"a melt's MELT_SINGLETON condition is built inside this call, so Owner::Custom cannot \
emit it — the bundle would melt nothing",
));
}
gate_owner_controls_store(store, owner)?;
let mut ctx = SpendContext::new();
let conditions = Conditions::new().melt_singleton();
let owner_spend = inner_spend(&mut ctx, owner, conditions)?;
let store_spend = store.clone().spend(&mut ctx, owner_spend)?;
Ok(MerkleCoinSpend::new(vec![store_spend], None))
}
fn gate_owner_controls_store<M>(store: &Datastore<M>, owner: Owner) -> MerkleResult<()> {
let Owner::Standard(public_key) = owner else {
return Err(MerkleError::UnsupportedOwner(
"melt requires Owner::Standard to prove control of the store",
));
};
if StandardArgs::curry_tree_hash(public_key) != store.info.owner_puzzle_hash.into() {
return Err(MerkleError::NotTheOwner);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mint::mint_datastore;
use crate::required_signatures;
use crate::types::{Bytes32, Datastore, DelegatedPuzzle};
use chia_puzzle_types::standard::StandardArgs;
use chia_wallet_sdk::clvm_utils::TreeHash;
use chia_wallet_sdk::prelude::MAINNET_CONSTANTS;
use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature};
use chia_wallet_sdk::test::Simulator;
fn minted_store(
sim: &mut Simulator,
) -> anyhow::Result<(
chia_wallet_sdk::test::BlsPairWithCoin,
Datastore<DigDataStoreMetadata>,
)> {
minted_store_with_delegation(sim, vec![])
}
fn minted_store_with_delegation(
sim: &mut Simulator,
delegated_puzzles: Vec<DelegatedPuzzle>,
) -> anyhow::Result<(
chia_wallet_sdk::test::BlsPairWithCoin,
Datastore<DigDataStoreMetadata>,
)> {
let owner = sim.bls(1_000_000);
let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
let built = mint_datastore(
owner.coin,
Owner::Standard(owner.pk),
Bytes32::new([0x5a; 32]),
None,
None,
None,
None,
None,
owner_ph,
delegated_puzzles,
0,
)?;
sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
Ok((owner, built.child.expect("mint yields a child")))
}
#[test]
fn melt_yields_no_child_and_validates() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (owner, store) = minted_store(&mut sim)?;
let built = melt(&store, Owner::Standard(owner.pk))?;
assert!(built.child.is_none(), "a melt leaves no successor");
assert_eq!(built.coin_spends.len(), 1, "melt is a single coin spend");
sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
Ok(())
}
#[test]
fn a_custom_owner_melt_is_refused() -> anyhow::Result<()> {
use chia_wallet_sdk::driver::{SpendContext, SpendWithConditions, StandardLayer};
let mut sim = Simulator::new();
let (owner, store) = minted_store(&mut sim)?;
let mut ctx = SpendContext::new();
let prebuilt = StandardLayer::new(owner.pk)
.spend_with_conditions(&mut ctx, chia_wallet_sdk::types::Conditions::new())?;
let result = melt(&store, Owner::Custom(prebuilt));
assert!(
matches!(result, Err(crate::MerkleError::UnsupportedOwner(_))),
"a custom-owner melt must refuse, not return a bundle that never melts, got: {result:?}"
);
Ok(())
}
#[test]
fn melt_requires_a_single_agg_sig_me() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (owner, store) = minted_store(&mut sim)?;
let built = melt(&store, Owner::Standard(owner.pk))?;
let constants = AggSigConstants::from(&*MAINNET_CONSTANTS);
let required = required_signatures(&built.coin_spends, &constants)?;
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"),
}
Ok(())
}
#[test]
fn a_melt_by_a_non_owner_is_refused() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (owner, store) = minted_store(&mut sim)?;
let stranger = sim.bls(0);
let _owner_can = melt(&store, Owner::Standard(owner.pk))
.expect("the real owner must still be able to melt this store");
let result = melt(&store, Owner::Standard(stranger.pk));
assert!(
matches!(result, Err(MerkleError::NotTheOwner)),
"a melt by a key that does not control the store must be refused, got: {result:?}"
);
Ok(())
}
#[test]
fn a_refused_melt_leaves_the_store_alive_while_the_owners_melt_ends_it() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (owner, store) = minted_store(&mut sim)?;
let stranger = sim.bls(0);
assert!(
sim.coin_state(store.coin.coin_id())
.is_some_and(|state| state.spent_height.is_none()),
"the store must be live before anyone tries to melt it"
);
let refused = melt(&store, Owner::Standard(stranger.pk));
assert!(matches!(refused, Err(MerkleError::NotTheOwner)));
assert!(
sim.coin_state(store.coin.coin_id())
.is_some_and(|state| state.spent_height.is_none()),
"a refused melt must leave the store singleton untouched on chain"
);
let built = melt(&store, Owner::Standard(owner.pk))?;
sim.spend_coins(built.coin_spends, std::slice::from_ref(&owner.sk))?;
assert!(
sim.coin_state(store.coin.coin_id())
.is_some_and(|state| state.spent_height.is_some()),
"the owner's melt must really spend the store coin, or the assertions above are vacuous"
);
Ok(())
}
#[test]
fn the_owner_of_a_store_with_delegated_puzzles_may_still_melt() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let delegated = vec![DelegatedPuzzle::Admin(TreeHash::new([0x11; 32]))];
let (owner, store) = minted_store_with_delegation(&mut sim, delegated)?;
assert!(
!store.info.delegated_puzzles.is_empty(),
"this control is meaningless unless the store really carries a delegation layer"
);
assert_ne!(
store.coin.puzzle_hash, store.info.owner_puzzle_hash,
"the delegation layer must make the coin's puzzle hash differ from the owner's, \
otherwise this shape cannot distinguish a gate keyed on the wrong field"
);
let built = melt(&store, Owner::Standard(owner.pk))
.expect("the owner of a delegated store must still be able to melt it");
sim.spend_coins(built.coin_spends, std::slice::from_ref(&owner.sk))?;
assert!(
sim.coin_state(store.coin.coin_id())
.is_some_and(|state| state.spent_height.is_some()),
"the owner's melt of a delegated store must confirm, not merely build"
);
Ok(())
}
#[test]
fn a_non_owner_melt_of_a_delegated_store_is_also_refused() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let delegated = vec![DelegatedPuzzle::Admin(TreeHash::new([0x11; 32]))];
let (_owner, store) = minted_store_with_delegation(&mut sim, delegated)?;
let stranger = sim.bls(0);
let result = melt(&store, Owner::Standard(stranger.pk));
assert!(
matches!(result, Err(MerkleError::NotTheOwner)),
"a delegation layer must not admit a melt by a non-owner, got: {result:?}"
);
Ok(())
}
}