use chia_puzzle_types::standard::StandardArgs;
use chia_wallet_sdk::driver::{Did, SpendContext};
use chia_wallet_sdk::types::Conditions;
use crate::context::{drain_coin_spends, inner_spend};
use crate::error::{DidError, DidResult};
use crate::types::{DidSpend, Owner};
pub fn melt(ctx: &mut SpendContext, did: Did, owner: Owner) -> DidResult<DidSpend> {
if matches!(owner, Owner::Custom(_)) {
return Err(DidError::UnsupportedOwner(
"melt requires Owner::Standard; a pre-built custom inner spend cannot carry the \
MELT_SINGLETON condition this call builds — the bundle would melt nothing",
));
}
gate_owner_controls_did(&did, owner)?;
let spend = inner_spend(ctx, owner, Conditions::new().melt_singleton())?;
let successor = did.spend(ctx, spend)?;
if successor.is_some() {
return Err(DidError::Parse(
"melt produced a successor DID: the singleton was recreated, not melted".into(),
));
}
Ok(DidSpend::new(drain_coin_spends(ctx), None))
}
fn gate_owner_controls_did(did: &Did, owner: Owner) -> DidResult<()> {
let Owner::Standard(public_key) = owner else {
return Err(DidError::UnsupportedOwner(
"melt requires Owner::Standard to prove control of the singleton",
));
};
let controlling_puzzle_hash = StandardArgs::curry_tree_hash(public_key);
if controlling_puzzle_hash != did.info.p2_puzzle_hash.into() {
return Err(DidError::NotTheOwner);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use chia_protocol::Coin;
use chia_puzzle_types::singleton::SingletonArgs;
use chia_wallet_sdk::driver::SingletonInfo;
use chia_wallet_sdk::prelude::MAINNET_CONSTANTS;
use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature};
use chia_wallet_sdk::test::Simulator;
use crate::create::create_simple_did;
use crate::required_signatures;
use crate::test_support::all_emitted_conditions;
fn settled_did(
sim: &mut Simulator,
) -> anyhow::Result<(chia_wallet_sdk::test::BlsPairWithCoin, Did)> {
let owner = sim.bls(1);
let ctx = &mut SpendContext::new();
let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
let did = spend.child.expect("create returns a child DID");
sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
Ok((owner, did))
}
fn would_be_successor(did: &Did) -> Coin {
let wrapped =
SingletonArgs::curry_tree_hash(did.info.launcher_id(), did.info.inner_puzzle_hash());
Coin::new(did.coin.coin_id(), wrapped.into(), did.coin.amount)
}
#[test]
fn a_melted_did_has_no_next_generation_on_chain() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (owner, did) = settled_did(&mut sim)?;
let successor = would_be_successor(&did);
assert!(
sim.coin_state(did.coin.coin_id())
.is_some_and(|state| state.spent_height.is_none()),
"the DID must be live before it is melted"
);
let ctx = &mut SpendContext::new();
let built = melt(ctx, did, Owner::Standard(owner.pk))?;
assert!(built.child.is_none(), "a melt reports no successor DID");
sim.spend_coins(built.coin_spends, std::slice::from_ref(&owner.sk))?;
assert!(
sim.coin_state(did.coin.coin_id())
.is_some_and(|state| state.spent_height.is_some()),
"the melt must actually have spent the DID coin"
);
assert!(
sim.coin_state(successor.coin_id()).is_none(),
"the lineage must terminate: no next-generation singleton may exist"
);
Ok(())
}
#[test]
fn a_non_melting_spend_does_create_the_next_generation() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (owner, did) = settled_did(&mut sim)?;
let successor = would_be_successor(&did);
let ctx = &mut SpendContext::new();
let _recreated = crate::spend_did_with_conditions(
ctx,
did,
Owner::Standard(owner.pk),
Conditions::new(),
)?;
let coin_spends = ctx.take();
sim.spend_coins(coin_spends, std::slice::from_ref(&owner.sk))?;
assert!(
sim.coin_state(successor.coin_id()).is_some(),
"the reconstructed successor id must be the one a live spend really creates"
);
Ok(())
}
#[test]
fn the_executed_melt_creates_no_coin_at_all() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (owner, did) = settled_did(&mut sim)?;
let ctx = &mut SpendContext::new();
let built = melt(ctx, did, Owner::Standard(owner.pk))?;
let inspector = &mut SpendContext::new();
let conditions = all_emitted_conditions(inspector, &built.coin_spends)?;
let created: Vec<_> = conditions
.iter()
.filter_map(|condition| condition.clone().into_create_coin())
.collect();
assert!(
created.is_empty(),
"a melt creates no coin: no successor, and the amount becomes an implicit fee, got: {created:?}"
);
Ok(())
}
#[test]
fn a_key_that_does_not_control_the_did_is_refused() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (_owner, did) = settled_did(&mut sim)?;
let stranger = sim.bls(1);
let ctx = &mut SpendContext::new();
let result = melt(ctx, did, Owner::Standard(stranger.pk));
assert!(
matches!(result, Err(DidError::NotTheOwner)),
"a stranger's melt must be refused, got: {result:?}"
);
assert!(
ctx.take().is_empty(),
"the refusal must happen before any spend is built"
);
Ok(())
}
#[test]
fn a_custom_owner_melt_is_refused() -> anyhow::Result<()> {
use chia_wallet_sdk::driver::{SpendWithConditions, StandardLayer};
let mut sim = Simulator::new();
let (owner, did) = settled_did(&mut sim)?;
let ctx = &mut SpendContext::new();
let prebuilt =
StandardLayer::new(owner.pk).spend_with_conditions(ctx, Conditions::new())?;
let result = melt(ctx, did, Owner::Custom(prebuilt));
assert!(
matches!(result, Err(DidError::UnsupportedOwner(_))),
"a custom-owner melt must refuse, got: {result:?}"
);
Ok(())
}
#[test]
fn the_melt_requires_a_single_agg_sig_me() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (owner, did) = settled_did(&mut sim)?;
let ctx = &mut SpendContext::new();
let built = melt(ctx, did, 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!("a standard owner signs with a BLS key"),
}
Ok(())
}
}