use chia_protocol::Bytes32;
use chia_wallet_sdk::clvmr::{Allocator, NodePtr, SExp};
use chia_wallet_sdk::driver::{Did, SingletonInfo, SpendContext};
use chia_wallet_sdk::types::{Condition, Conditions};
use crate::context::inner_spend;
use crate::error::{DidError, DidResult};
use crate::types::Owner;
pub fn spend_did_with_conditions(
ctx: &mut SpendContext,
did: Did,
owner: Owner,
conditions: Conditions,
) -> DidResult<Did> {
if matches!(owner, Owner::Custom(_)) {
return Err(DidError::UnsupportedOwner(
"spend_did_with_conditions requires Owner::Standard; a pre-built custom inner spend \
cannot carry the DID's recreation condition — build the spend yourself and call \
Did::spend",
));
}
permit_only_conditions_a_did_may_carry(ctx, &conditions)?;
let unchanged_inner_puzzle_hash: Bytes32 = did.info.inner_puzzle_hash().into();
let memos = ctx.hint(did.info.p2_puzzle_hash)?;
let with_recreation = Conditions::new()
.create_coin(unchanged_inner_puzzle_hash, did.coin.amount, memos)
.extend(conditions);
let spend = inner_spend(ctx, owner, with_recreation)?;
did.spend(ctx, spend)?
.ok_or_else(|| DidError::Parse("DID spend produced no successor DID".into()))
}
fn permit_only_conditions_a_did_may_carry(
ctx: &mut SpendContext,
conditions: &Conditions,
) -> DidResult<()> {
let allocated = ctx.alloc(conditions)?;
let as_the_chain_sees_them: Vec<Condition> = ctx.extract(allocated)?;
let as_raw_clvm: Vec<NodePtr> = ctx.extract(allocated)?;
for (condition, raw) in as_the_chain_sees_them.iter().zip(as_raw_clvm) {
match condition {
Condition::CreateCoin(_) if !amount_is_canonically_encoded(ctx, raw) => {
return Err(DidError::NonCanonicalCreateCoinAmount(describe_amount(
ctx, raw,
)));
}
Condition::CreateCoin(create) if create.amount % 2 == 1 => {
return Err(DidError::OddAmountCreateCoin);
}
Condition::AggSigUnsafe(_) => return Err(DidError::AggSigUnsafeInConditions),
Condition::Remark(_)
| Condition::CreateCoin(_)
| Condition::ReserveFee(_)
| Condition::CreateCoinAnnouncement(_)
| Condition::AssertCoinAnnouncement(_)
| Condition::CreatePuzzleAnnouncement(_)
| Condition::AssertPuzzleAnnouncement(_)
| Condition::AssertConcurrentSpend(_)
| Condition::AssertConcurrentPuzzle(_)
| Condition::SendMessage(_)
| Condition::ReceiveMessage(_)
| Condition::AssertMyCoinId(_)
| Condition::AssertMyParentId(_)
| Condition::AssertMyPuzzleHash(_)
| Condition::AssertMyAmount(_)
| Condition::AssertMyBirthSeconds(_)
| Condition::AssertMyBirthHeight(_)
| Condition::AssertEphemeral(_)
| Condition::AssertSecondsRelative(_)
| Condition::AssertSecondsAbsolute(_)
| Condition::AssertHeightRelative(_)
| Condition::AssertHeightAbsolute(_)
| Condition::AssertBeforeSecondsRelative(_)
| Condition::AssertBeforeSecondsAbsolute(_)
| Condition::AssertBeforeHeightRelative(_)
| Condition::AssertBeforeHeightAbsolute(_)
| Condition::AggSigMe(_)
| Condition::AggSigParent(_)
| Condition::AggSigParentAmount(_)
| Condition::AggSigParentPuzzle(_) => {}
other => return Err(DidError::DisallowedCondition(describe(ctx, other, raw))),
}
}
Ok(())
}
fn create_coin_amount_node(allocator: &Allocator, condition: NodePtr) -> Option<NodePtr> {
let SExp::Pair(_opcode, after_opcode) = allocator.sexp(condition) else {
return None;
};
let SExp::Pair(_puzzle_hash, after_puzzle_hash) = allocator.sexp(after_opcode) else {
return None;
};
let SExp::Pair(amount, _rest) = allocator.sexp(after_puzzle_hash) else {
return None;
};
Some(amount)
}
fn amount_is_canonically_encoded(allocator: &Allocator, condition: NodePtr) -> bool {
let Some(amount) = create_coin_amount_node(allocator, condition) else {
return false;
};
let SExp::Atom = allocator.sexp(amount) else {
return false;
};
let atom = allocator.atom(amount);
let bytes = atom.as_ref();
if bytes.is_empty() {
return true;
}
if bytes[0] & 0x80 != 0 {
return false; }
if bytes == [0_u8] || (bytes.len() > 1 && bytes[0] == 0 && bytes[1] & 0x80 == 0) {
return false; }
let significant_bytes = if bytes[0] == 0 { 9 } else { 8 };
bytes.len() <= significant_bytes
}
fn describe_amount(allocator: &Allocator, condition: NodePtr) -> String {
let Some(amount) = create_coin_amount_node(allocator, condition) else {
return "the condition has no amount element".to_string();
};
match allocator.sexp(amount) {
SExp::Atom => {
let atom = allocator.atom(amount);
let hex: String = atom
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
format!("amount atom 0x{hex}")
}
SExp::Pair(..) => "the amount element is a pair, not an integer".to_string(),
}
}
fn describe(allocator: &Allocator, condition: &Condition, raw: NodePtr) -> String {
let Condition::Other(_) = condition else {
return format!("{condition:?}");
};
match clvm_opcode(allocator, raw) {
Some(opcode) => format!("a condition the SDK cannot name, CLVM opcode {opcode}"),
None => "a condition the SDK cannot name, with no atom opcode".to_string(),
}
}
fn clvm_opcode(allocator: &Allocator, condition: NodePtr) -> Option<i64> {
let SExp::Pair(opcode, _rest) = allocator.sexp(condition) else {
return None;
};
let SExp::Atom = allocator.sexp(opcode) else {
return None;
};
let atom = allocator.atom(opcode);
let bytes = atom.as_ref();
if bytes.len() > 8 {
return None;
}
let negative = bytes.first().is_some_and(|first| first & 0x80 != 0);
let mut value: i64 = if negative { -1 } else { 0 };
for byte in bytes {
value = (value << 8) | i64::from(*byte);
}
Some(value)
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use crate::create::create_simple_did;
use crate::test_support::{all_emitted_conditions, creates_coin_to};
use chia_bls::SecretKey;
use chia_protocol::Bytes;
use chia_puzzle_types::singleton::SingletonArgs;
use chia_puzzle_types::Memos;
use chia_wallet_sdk::clvm_traits::ToClvm;
use chia_wallet_sdk::clvmr::{self, Allocator};
use chia_wallet_sdk::driver::Launcher;
use chia_wallet_sdk::prelude::{PublicKey, MAINNET_CONSTANTS};
use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature};
use chia_wallet_sdk::test::Simulator;
use chia_wallet_sdk::types::conditions::{AggSig, AggSigKind, CreateCoin};
struct MintedDid {
did: Did,
pk: PublicKey,
sk: SecretKey,
puzzle_hash: Bytes32,
}
fn singleton_puzzle_hash(did: &Did) -> Bytes32 {
SingletonArgs::curry_tree_hash(did.info.launcher_id, did.info.inner_puzzle_hash()).into()
}
fn mint(sim: &mut Simulator, ctx: &mut SpendContext) -> anyhow::Result<MintedDid> {
let owner = sim.bls(1);
let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
let did = spend.child.expect("create always returns a child DID");
sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
Ok(MintedDid {
did,
pk: owner.pk,
sk: owner.sk,
puzzle_hash: owner.puzzle_hash,
})
}
#[test]
fn spend_did_with_conditions_recreates_the_did_and_emits_the_callers_conditions(
) -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let marker = Bytes::from(b"dig-did::update::marker".to_vec());
let child = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new().create_puzzle_announcement(marker.clone()),
)?;
let coin_spends = ctx.take();
let emitted = all_emitted_conditions(ctx, &coin_spends)?;
assert!(
emitted.iter().any(|condition| matches!(
condition.as_create_puzzle_announcement(),
Some(announcement) if announcement.message == marker
)),
"the caller's condition must reach the wire"
);
assert_eq!(child.info.launcher_id, owner.did.info.launcher_id);
assert_eq!(child.info.p2_puzzle_hash, owner.did.info.p2_puzzle_hash);
assert_eq!(child.coin.amount, owner.did.coin.amount);
sim.spend_coins(coin_spends, &[owner.sk])?;
Ok(())
}
#[test]
fn callers_conditions_do_not_displace_the_recreation() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let recreated_puzzle_hash = singleton_puzzle_hash(&owner.did);
let child = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new()
.create_puzzle_announcement(Bytes::from(b"noise".to_vec()))
.create_puzzle_announcement(Bytes::from(b"more noise".to_vec())),
)?;
assert_eq!(child.coin.puzzle_hash, recreated_puzzle_hash);
let coin_spends = ctx.take();
assert!(
creates_coin_to(ctx, &coin_spends, recreated_puzzle_hash)?,
"the recreation CREATE_COIN must survive the caller's conditions"
);
sim.spend_coins(coin_spends, &[owner.sk])?;
Ok(())
}
fn creates_coin_with_puzzle_hash(condition: &Condition, puzzle_hash: Bytes32) -> bool {
condition
.as_create_coin()
.is_some_and(|create| create.puzzle_hash == puzzle_hash)
}
fn announces(condition: &Condition, message: &Bytes) -> bool {
condition
.as_create_puzzle_announcement()
.is_some_and(|announcement| announcement.message == *message)
}
#[test]
fn the_recreation_is_emitted_before_the_callers_conditions() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let recreated_puzzle_hash = singleton_puzzle_hash(&owner.did);
let first = Bytes::from(b"dig-did::ordering::first".to_vec());
let second = Bytes::from(b"dig-did::ordering::second".to_vec());
let _child = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new()
.create_puzzle_announcement(first.clone())
.create_puzzle_announcement(second.clone()),
)?;
let coin_spends = ctx.take();
let emitted = all_emitted_conditions(ctx, &coin_spends)?;
let index_of = |what: &str, predicate: &dyn Fn(&Condition) -> bool| {
emitted
.iter()
.position(predicate)
.unwrap_or_else(|| panic!("{what} must reach the wire, got {emitted:?}"))
};
let recreation = index_of("the DID's recreation", &|condition| {
creates_coin_with_puzzle_hash(condition, recreated_puzzle_hash)
});
let first = index_of("the caller's first condition", &|condition| {
announces(condition, &first)
});
let second = index_of("the caller's second condition", &|condition| {
announces(condition, &second)
});
assert_eq!(
(first, second),
(recreation + 1, recreation + 2),
"the recreation must open the composed list, with the caller's conditions following it \
in order, got {emitted:?}"
);
sim.spend_coins(coin_spends, &[owner.sk])?;
Ok(())
}
#[test]
fn spend_did_with_conditions_requires_exactly_one_agg_sig_me_under_the_owner(
) -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let _child = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new().create_puzzle_announcement(Bytes::from(b"benign".to_vec())),
)?;
let coin_spends = ctx.take();
let constants = AggSigConstants::from(&*MAINNET_CONSTANTS);
let required = crate::sign::required_signatures(&coin_spends, &constants)
.expect("signature calculation must succeed for a well-formed DID spend");
assert_eq!(required.len(), 1, "one owner spend, one AGG_SIG_ME");
match &required[0] {
RequiredSignature::Bls(bls) => assert_eq!(bls.public_key, owner.pk),
RequiredSignature::Secp(_) => panic!("a standard owner signs with BLS, not secp"),
}
Ok(())
}
#[test]
fn spend_did_with_conditions_refuses_a_custom_owner() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let prebuilt = inner_spend(ctx, Owner::Standard(owner.pk), Conditions::new())?;
let result =
spend_did_with_conditions(ctx, owner.did, Owner::Custom(prebuilt), Conditions::new());
assert!(matches!(result, Err(DidError::UnsupportedOwner(_))));
Ok(())
}
#[test]
fn composes_with_caller_conditions_that_create_a_coin() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let funder = sim.bls(2);
let funder_spend = inner_spend(ctx, Owner::Standard(funder.pk), Conditions::new())?;
ctx.spend(funder.coin, funder_spend)?;
let recreated_puzzle_hash = singleton_puzzle_hash(&owner.did);
let child = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new().create_coin(owner.puzzle_hash, 2, Memos::None),
)?;
assert_eq!(child.coin.puzzle_hash, recreated_puzzle_hash);
let coin_spends = ctx.take();
assert!(
creates_coin_to(ctx, &coin_spends, recreated_puzzle_hash)?,
"the DID must still recreate itself"
);
assert!(
creates_coin_to(ctx, &coin_spends, owner.puzzle_hash)?,
"and the caller's coin must actually be created"
);
sim.spend_coins(coin_spends, &[owner.sk, funder.sk])?;
Ok(())
}
#[test]
fn a_foreign_singleton_launcher_cannot_be_parented_to_the_did_coin() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let launcher = Launcher::new(owner.did.coin.coin_id(), 1);
let (launch_conditions, _eve_coin) = launcher.spend(ctx, owner.puzzle_hash, ())?;
let result =
spend_did_with_conditions(ctx, owner.did, Owner::Standard(owner.pk), launch_conditions);
assert!(
matches!(result, Err(DidError::OddAmountCreateCoin)),
"an odd-amount CREATE_COIN must be refused at build time, got {result:?}"
);
Ok(())
}
#[test]
fn spend_did_with_conditions_refuses_an_agg_sig_unsafe_condition() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let result = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new()
.agg_sig_unsafe(owner.pk, Bytes::from(b"ATTACKER-CHOSEN-MESSAGE".to_vec())),
);
assert!(
matches!(result, Err(DidError::AggSigUnsafeInConditions)),
"an AGG_SIG_UNSAFE must be refused at build time, got {result:?}"
);
Ok(())
}
fn smuggled(
ctx: &mut SpendContext,
value: &impl ToClvm<Allocator>,
) -> anyhow::Result<Condition> {
Ok(Condition::Other(ctx.alloc(value)?))
}
#[test]
fn refuses_an_agg_sig_unsafe_smuggled_as_an_unrecognized_condition() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let unsafe_sig = AggSig::new(
AggSigKind::Unsafe,
owner.pk,
Bytes::from(b"I, the DID owner, authorize the transfer of everything".to_vec()),
);
let disguised = smuggled(ctx, &unsafe_sig)?;
let result = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new().with(disguised),
);
assert!(
matches!(result, Err(DidError::AggSigUnsafeInConditions)),
"an AGG_SIG_UNSAFE must be refused however the caller types it, got {result:?}"
);
Ok(())
}
#[test]
fn refuses_an_odd_amount_create_coin_smuggled_as_an_unrecognized_condition(
) -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let odd: CreateCoin<clvmr::NodePtr> = CreateCoin::new(owner.puzzle_hash, 1, Memos::None);
let disguised = smuggled(ctx, &odd)?;
let result = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new().with(disguised),
);
assert!(
matches!(result, Err(DidError::OddAmountCreateCoin)),
"an odd-amount CREATE_COIN must be refused however the caller types it, got {result:?}"
);
Ok(())
}
#[test]
fn refuses_a_condition_the_sdk_cannot_name() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let unknown = smuggled(ctx, &(12345_u32, (Bytes::from(b"payload".to_vec()), ())))?;
let result = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new().with(unknown),
);
assert!(
matches!(result, Err(DidError::DisallowedCondition(_))),
"a condition outside the allowlist must be refused, got {result:?}"
);
Ok(())
}
#[test]
fn refuses_agg_sig_kinds_bound_only_to_lifetime_constant_attributes() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
for kind in [
AggSigKind::Puzzle,
AggSigKind::Amount,
AggSigKind::PuzzleAmount,
] {
let mut replay_ctx = SpendContext::new();
let sig = AggSig::new(kind, owner.pk, Bytes::from(b"replayable".to_vec()));
let condition = smuggled(&mut replay_ctx, &sig)?;
let result = spend_did_with_conditions(
&mut replay_ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new().with(condition),
);
assert!(
matches!(result, Err(DidError::DisallowedCondition(_))),
"{kind:?} binds only to attributes constant across the DID's lifetime and must be \
refused, got {result:?}"
);
}
Ok(())
}
fn create_coin_with_raw_amount(
ctx: &mut SpendContext,
puzzle_hash: Bytes32,
amount_atom: &[u8],
) -> anyhow::Result<Condition> {
smuggled(
ctx,
&(
51_u8,
(puzzle_hash, (Bytes::from(amount_atom.to_vec()), ())),
),
)
}
fn spend_with_raw_amount(amount_atom: &[u8]) -> anyhow::Result<DidResult<Did>> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let condition = create_coin_with_raw_amount(ctx, owner.puzzle_hash, amount_atom)?;
Ok(spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new().with(condition),
))
}
#[test]
fn refuses_a_create_coin_amount_whose_leading_byte_sets_the_sign_bit() -> anyhow::Result<()> {
let result = spend_with_raw_amount(&[0x80])?;
assert!(
matches!(result, Err(DidError::NonCanonicalCreateCoinAmount(_))),
"a negative amount must be refused at build time, got {result:?}"
);
Ok(())
}
#[test]
fn refuses_a_create_coin_amount_with_a_redundant_leading_zero() -> anyhow::Result<()> {
let result = spend_with_raw_amount(&[0x00, 0x00, 0x02])?;
assert!(
matches!(result, Err(DidError::NonCanonicalCreateCoinAmount(_))),
"a non-canonically-encoded amount must be refused at build time, got {result:?}"
);
Ok(())
}
#[test]
fn refuses_a_create_coin_amount_encoded_as_a_single_zero_byte() -> anyhow::Result<()> {
let result = spend_with_raw_amount(&[0x00])?;
assert!(
matches!(result, Err(DidError::NonCanonicalCreateCoinAmount(_))),
"a redundant zero encoding must be refused at build time, got {result:?}"
);
Ok(())
}
#[test]
fn permits_a_canonically_encoded_even_create_coin_amount() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let funder = sim.bls(2);
let funder_spend = inner_spend(ctx, Owner::Standard(funder.pk), Conditions::new())?;
ctx.spend(funder.coin, funder_spend)?;
let condition = create_coin_with_raw_amount(ctx, owner.puzzle_hash, &[0x02])?;
let _child = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new().with(condition),
)?;
let coin_spends = ctx.take();
assert!(
creates_coin_to(ctx, &coin_spends, owner.puzzle_hash)?,
"the caller's canonically-encoded payment must actually be created"
);
sim.spend_coins(coin_spends, &[owner.sk, funder.sk])?;
Ok(())
}
fn three_conditions_with_create_coin_last(
ctx: &mut SpendContext,
puzzle_hash: Bytes32,
amount_atom: &[u8],
) -> anyhow::Result<Conditions> {
let create_coin = create_coin_with_raw_amount(ctx, puzzle_hash, amount_atom)?;
Ok(Conditions::new()
.remark(NodePtr::NIL)
.create_coin_announcement(Bytes::from(b"ahead-of-the-create-coin".to_vec()))
.with(create_coin))
}
#[test]
fn refuses_a_non_canonical_create_coin_amount_behind_two_permitted_conditions(
) -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let conditions = three_conditions_with_create_coin_last(ctx, owner.puzzle_hash, &[0x80])?;
let result =
spend_did_with_conditions(ctx, owner.did, Owner::Standard(owner.pk), conditions);
assert!(
matches!(result, Err(DidError::NonCanonicalCreateCoinAmount(_))),
"a negative amount must be refused wherever it sits in the list, got {result:?}"
);
Ok(())
}
#[test]
fn permits_a_canonical_create_coin_amount_behind_two_permitted_conditions() -> anyhow::Result<()>
{
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let funder = sim.bls(2);
let funder_spend = inner_spend(ctx, Owner::Standard(funder.pk), Conditions::new())?;
ctx.spend(funder.coin, funder_spend)?;
let conditions = three_conditions_with_create_coin_last(ctx, owner.puzzle_hash, &[0x02])?;
let _child =
spend_did_with_conditions(ctx, owner.did, Owner::Standard(owner.pk), conditions)?;
let coin_spends = ctx.take();
assert!(
creates_coin_to(ctx, &coin_spends, owner.puzzle_hash)?,
"the caller's canonically-encoded payment must actually be created"
);
sim.spend_coins(coin_spends, &[owner.sk, funder.sk])?;
Ok(())
}
#[test]
fn refuses_a_melt_singleton_condition() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let melt = smuggled(
ctx,
&(51_u8, (Bytes::default(), (Bytes::from(vec![0x8F_u8]), ()))),
)?;
let reparsed: Vec<Condition> = {
let allocated = ctx.alloc(&Conditions::new().with(melt.clone()))?;
ctx.extract(allocated)?
};
assert!(
matches!(reparsed.as_slice(), [Condition::MeltSingleton(_)]),
"the fixture must resolve to MELT_SINGLETON, got {reparsed:?}"
);
let result = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new().with(melt),
);
assert!(
matches!(result, Err(DidError::DisallowedCondition(_))),
"MELT_SINGLETON would burn the DID and must be refused, got {result:?}"
);
Ok(())
}
#[test]
fn names_the_clvm_opcode_of_a_condition_the_sdk_cannot_name() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let unknown = smuggled(ctx, &(12345_u32, (Bytes::from(b"payload".to_vec()), ())))?;
let result = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new().with(unknown),
);
let Err(DidError::DisallowedCondition(rendered)) = result else {
panic!("an unnameable condition must be refused, got {result:?}");
};
assert!(
rendered.contains("12345"),
"the refusal must name the CLVM opcode, got {rendered:?}"
);
assert!(
!rendered.contains("NodePtr"),
"an allocator index tells the caller nothing, got {rendered:?}"
);
Ok(())
}
#[test]
fn permits_the_conditions_a_did_spend_legitimately_carries() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = mint(&mut sim, ctx)?;
let _child = spend_did_with_conditions(
ctx,
owner.did,
Owner::Standard(owner.pk),
Conditions::new()
.create_puzzle_announcement(Bytes::from(b"bind-me".to_vec()))
.assert_my_amount(owner.did.coin.amount)
.assert_height_relative(0)
.agg_sig_me(owner.pk, Bytes::from(b"coin-bound".to_vec())),
)?;
let coin_spends = ctx.take();
let constants = AggSigConstants::from(&*MAINNET_CONSTANTS);
let required = crate::sign::required_signatures(&coin_spends, &constants)
.expect("signature calculation must succeed for a well-formed DID spend");
assert_eq!(
required.len(),
2,
"the DID's own AGG_SIG_ME plus the caller's coin-bound one"
);
sim.spend_coins(coin_spends, &[owner.sk])?;
Ok(())
}
}