use chia_protocol::{Bytes32, Coin};
use chia_puzzle_types::standard::StandardArgs;
use chia_wallet_sdk::driver::{Did, HashedPtr, Launcher, SpendContext};
use chia_wallet_sdk::types::Conditions;
use crate::amount::SingletonAmount;
use crate::context::{drain_coin_spends, inner_spend};
use crate::error::{DidError, DidResult};
use crate::types::{DidSpend, Owner};
pub fn create_did(
ctx: &mut SpendContext,
funding_coin: Coin,
owner: Owner,
recovery_list_hash: Option<Bytes32>,
num_verifications_required: u64,
metadata: HashedPtr,
) -> DidResult<DidSpend> {
let owner_puzzle_hash = standard_owner_puzzle_hash(owner, "create_did")?;
let (launch_conditions, eve) = singleton_launcher(funding_coin)?.create_eve_did(
ctx,
owner_puzzle_hash,
recovery_list_hash,
num_verifications_required,
metadata,
)?;
let settled = settle(ctx, eve, owner)?;
spend_funding_coin(ctx, funding_coin, owner, launch_conditions)?;
Ok(DidSpend::new(drain_coin_spends(ctx), Some(settled)))
}
pub fn create_simple_did(
ctx: &mut SpendContext,
funding_coin: Coin,
owner: Owner,
) -> DidResult<DidSpend> {
create_did(ctx, funding_coin, owner, None, 1, HashedPtr::NIL)
}
pub fn create_eve_did_only(
ctx: &mut SpendContext,
funding_coin: Coin,
owner: Owner,
recovery_list_hash: Option<Bytes32>,
num_verifications_required: u64,
metadata: HashedPtr,
) -> DidResult<DidSpend> {
let owner_puzzle_hash = standard_owner_puzzle_hash(owner, "create_eve_did_only")?;
let (launch_conditions, eve) = singleton_launcher(funding_coin)?.create_eve_did(
ctx,
owner_puzzle_hash,
recovery_list_hash,
num_verifications_required,
metadata,
)?;
spend_funding_coin(ctx, funding_coin, owner, launch_conditions)?;
Ok(DidSpend::new(drain_coin_spends(ctx), Some(eve)))
}
fn singleton_launcher(funding_coin: Coin) -> DidResult<Launcher> {
let amount = SingletonAmount::from_funding_coin(&funding_coin)?;
#[allow(clippy::disallowed_methods)]
Ok(Launcher::new(funding_coin.coin_id(), amount.get()))
}
fn standard_owner_puzzle_hash(owner: Owner, operation: &'static str) -> DidResult<Bytes32> {
match owner {
Owner::Standard(public_key) => Ok(StandardArgs::curry_tree_hash(public_key).into()),
Owner::Custom(_) => Err(DidError::UnsupportedOwner(match operation {
"create_eve_did_only" => {
"create_eve_did_only requires Owner::Standard; a pre-built custom inner spend \
cannot emit the launcher conditions, which are only computable inside this call — \
build the launch yourself with Launcher::create_eve_did"
}
_ => {
"create_did requires Owner::Standard; a pre-built custom inner spend cannot emit \
both the launcher conditions and the DID's recreation — build the launch yourself \
with Launcher::create_eve_did"
}
})),
}
}
fn settle(ctx: &mut SpendContext, did: Did, owner: Owner) -> DidResult<Did> {
crate::update::spend_did_with_conditions(ctx, did, owner, Conditions::new())
}
fn spend_funding_coin(
ctx: &mut SpendContext,
funding_coin: Coin,
owner: Owner,
launch_conditions: Conditions,
) -> DidResult<()> {
let spend = inner_spend(ctx, owner, launch_conditions)?;
ctx.spend(funding_coin, spend)?;
Ok(())
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use chia_wallet_sdk::prelude::MAINNET_CONSTANTS;
use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature};
use chia_wallet_sdk::test::Simulator;
#[test]
fn create_simple_did_produces_a_spendable_settled_did() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = sim.bls(1);
let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
let child = spend.child.expect("create always returns a child DID");
assert_eq!(child.info.recovery_list_hash, None);
assert_eq!(child.info.num_verifications_required, 1);
assert_eq!(child.info.p2_puzzle_hash, owner.puzzle_hash);
sim.spend_coins(spend.coin_spends, &[owner.sk])?;
Ok(())
}
#[test]
fn create_did_requires_two_agg_sig_mes_over_the_owner_key() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = sim.bls(1);
let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
let constants = AggSigConstants::from(&*MAINNET_CONSTANTS);
let required = crate::sign::required_signatures(&spend.coin_spends, &constants)
.expect("signature calculation must succeed for a well-formed create spend");
assert_eq!(
required.len(),
2,
"the funding-coin spend AND the settle spend each require one AGG_SIG_ME"
);
for signature in &required {
match signature {
RequiredSignature::Bls(bls) => assert_eq!(bls.public_key, owner.pk),
RequiredSignature::Secp(_) => panic!("a standard owner signs with BLS, not secp"),
}
}
Ok(())
}
#[test]
fn create_did_refuses_a_custom_owner() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = sim.bls(1);
let prebuilt = prebuilt_inner_spend(ctx, owner.pk)?;
assert!(matches!(
create_simple_did(ctx, owner.coin, Owner::Custom(prebuilt)),
Err(DidError::UnsupportedOwner(_))
));
assert!(matches!(
create_eve_did_only(
ctx,
owner.coin,
Owner::Custom(prebuilt),
None,
1,
HashedPtr::NIL
),
Err(DidError::UnsupportedOwner(_))
));
Ok(())
}
#[test]
fn a_custom_inner_spend_would_silently_drop_the_launcher_conditions() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = sim.bls(1);
let prebuilt = prebuilt_inner_spend(ctx, owner.pk)?;
let launcher = Launcher::new(owner.coin.coin_id(), owner.coin.amount);
let (_dropped_launch_conditions, _eve) =
launcher.create_eve_did(ctx, owner.puzzle_hash, None, 1, HashedPtr::NIL)?;
ctx.spend(owner.coin, prebuilt)?;
let coin_spends = drain_coin_spends(ctx);
assert!(
!crate::test_support::creates_coin_to(
ctx,
&coin_spends,
chia_puzzles::SINGLETON_LAUNCHER_HASH.into(),
)?,
"nothing in the bundle creates the launcher the eve DID was supposedly launched from"
);
Ok(())
}
fn prebuilt_inner_spend(
ctx: &mut SpendContext,
public_key: chia_wallet_sdk::prelude::PublicKey,
) -> DidResult<chia_wallet_sdk::driver::Spend> {
inner_spend(
ctx,
Owner::Standard(public_key),
Conditions::new().reserve_fee(0),
)
}
#[test]
fn every_creation_entry_point_refuses_an_even_funding_coin() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = sim.bls(2);
assert_eq!(owner.coin.amount, 2, "the fixture must be genuinely even");
assert!(matches!(
create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk)),
Err(DidError::EvenSingletonAmount(2))
));
assert!(matches!(
create_did(
ctx,
owner.coin,
Owner::Standard(owner.pk),
None,
1,
HashedPtr::NIL
),
Err(DidError::EvenSingletonAmount(2))
));
assert!(matches!(
create_eve_did_only(
ctx,
owner.coin,
Owner::Standard(owner.pk),
None,
1,
HashedPtr::NIL
),
Err(DidError::EvenSingletonAmount(2))
));
Ok(())
}
#[test]
fn every_creation_entry_point_accepts_an_odd_funding_coin() -> anyhow::Result<()> {
for build in [
(|ctx: &mut SpendContext, coin, owner| create_simple_did(ctx, coin, owner))
as fn(&mut SpendContext, Coin, Owner) -> DidResult<DidSpend>,
|ctx, coin, owner| create_did(ctx, coin, owner, None, 1, HashedPtr::NIL),
|ctx, coin, owner| create_eve_did_only(ctx, coin, owner, None, 1, HashedPtr::NIL),
] {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = sim.bls(3);
assert_eq!(owner.coin.amount, 3, "the fixture must be genuinely odd");
let spend = build(ctx, owner.coin, Owner::Standard(owner.pk))?;
let child = spend.child.expect("create always returns a child DID");
assert_eq!(
child.coin.amount, 3,
"the singleton carries the funding coin's ENTIRE amount"
);
sim.spend_coins(spend.coin_spends, &[owner.sk])?;
}
Ok(())
}
#[test]
fn create_did_preserves_a_custom_recovery_configuration() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = sim.bls(1);
let recovery_list_hash =
Some(clvm_utils::tree_hash_atom(b"dig-did::create::recovery-list").into());
let spend = create_did(
ctx,
owner.coin,
Owner::Standard(owner.pk),
recovery_list_hash,
2,
HashedPtr::NIL,
)?;
let child = spend.child.expect("create always returns a child DID");
assert_eq!(child.info.recovery_list_hash, recovery_list_hash);
assert_eq!(child.info.num_verifications_required, 2);
sim.spend_coins(spend.coin_spends, &[owner.sk])?;
Ok(())
}
#[test]
fn create_eve_did_only_skips_the_settle_spend() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = sim.bls(1);
let spend = create_eve_did_only(
ctx,
owner.coin,
Owner::Standard(owner.pk),
None,
1,
HashedPtr::NIL,
)?;
assert_eq!(spend.coin_spends.len(), 2);
let eve = spend.child.expect("create always returns a child DID");
assert_eq!(eve.info.p2_puzzle_hash, owner.puzzle_hash);
sim.spend_coins(spend.coin_spends, &[owner.sk])?;
Ok(())
}
}