use chia_wallet_sdk::driver::{Did, Puzzle};
use chia_wallet_sdk::prelude::Allocator;
use clvm_traits::ToClvm;
use dig_chainsource_interface::ChainSource;
use crate::types::{Bytes32, CoinSpend};
use crate::{MerkleError, MerkleResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DidRef {
pub launcher_id: Bytes32,
}
pub fn did_ref_from_spend(spend: &CoinSpend) -> MerkleResult<Option<DidRef>> {
let mut allocator = Allocator::new();
let puzzle_ptr = spend
.puzzle_reveal
.to_clvm(&mut allocator)
.map_err(|error| MerkleError::Parse(format!("puzzle reveal: {error}")))?;
let solution_ptr = spend
.solution
.to_clvm(&mut allocator)
.map_err(|error| MerkleError::Parse(format!("solution: {error}")))?;
let puzzle = Puzzle::parse(&allocator, puzzle_ptr);
match Did::parse(&allocator, spend.coin, puzzle, solution_ptr)? {
Some((did, _p2_spend)) => Ok(Some(DidRef {
launcher_id: did.info.launcher_id,
})),
None => Ok(None),
}
}
pub fn resolve_owner_did<C: ChainSource>(
store_id: Bytes32,
chain: &C,
) -> MerkleResult<Option<DidRef>> {
let Some(launcher_spend) = read_coin_spend(chain, store_id)? else {
return Ok(None);
};
if launcher_spend.coin.coin_id() != store_id {
return Err(MerkleError::Chain(format!(
"launcher spend for {store_id} is coin {}, not the requested store's launcher",
launcher_spend.coin.coin_id()
)));
}
let parent_id = launcher_spend.coin.parent_coin_info;
let Some(creator_spend) = read_coin_spend(chain, parent_id)? else {
return Ok(None);
};
if creator_spend.coin.coin_id() != parent_id {
return Err(MerkleError::Chain(format!(
"creator spend for {parent_id} is coin {}, not the launcher's parent",
creator_spend.coin.coin_id()
)));
}
did_ref_from_spend(&creator_spend)
}
fn read_coin_spend<C: ChainSource>(chain: &C, coin_id: Bytes32) -> MerkleResult<Option<CoinSpend>> {
chain
.coin_spend(coin_id)
.map_err(|error| MerkleError::Chain(format!("chain read for {coin_id}: {error}")))
}
#[cfg(test)]
mod tests {
use super::*;
use chia_wallet_sdk::driver::{Launcher, SpendContext, StandardLayer};
use chia_wallet_sdk::test::Simulator;
#[test]
fn did_spend_is_recognised_with_its_launcher_id() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let alice = sim.bls(1);
let alice_p2 = StandardLayer::new(alice.pk);
let (create_did, did) =
Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
alice_p2.spend(ctx, alice.coin, create_did)?;
sim.spend_coins(ctx.take(), std::slice::from_ref(&alice.sk))?;
let did_coin = did.coin;
let _child = did.update(ctx, &alice_p2, chia_wallet_sdk::types::Conditions::new())?;
let coin_spends = ctx.take();
sim.spend_coins(coin_spends.clone(), std::slice::from_ref(&alice.sk))?;
let did_spend = coin_spends
.iter()
.find(|s| s.coin.coin_id() == did_coin.coin_id())
.expect("the DID coin spend is present");
let did_ref = did_ref_from_spend(did_spend)?.expect("a DID spend is recognised");
assert_eq!(
did_ref.launcher_id, did.info.launcher_id,
"the DidRef names the DID's own launcher id"
);
Ok(())
}
#[test]
fn plain_standard_spend_is_not_a_did() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let alice = sim.bls(1);
let alice_p2 = StandardLayer::new(alice.pk);
let memos = ctx.hint(alice.puzzle_hash)?;
alice_p2.spend(
ctx,
alice.coin,
chia_wallet_sdk::types::Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
)?;
let coin_spends = ctx.take();
let standard_spend = coin_spends
.iter()
.find(|s| s.coin.coin_id() == alice.coin.coin_id())
.expect("the standard coin spend is present");
assert_eq!(
did_ref_from_spend(standard_spend)?,
None,
"a plain standard spend is not a DID"
);
Ok(())
}
use crate::resolve_owner_did;
use crate::types::{Coin, CoinSpend};
use chia_wallet_sdk::types::Conditions;
use dig_chainsource_interface::{ChainSourceError, MockChainSource};
fn did_coin_and_spend(sim: &mut Simulator) -> anyhow::Result<(CoinSpend, Bytes32)> {
let ctx = &mut SpendContext::new();
let alice = sim.bls(1);
let alice_p2 = StandardLayer::new(alice.pk);
let (create_did, did) =
Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
alice_p2.spend(ctx, alice.coin, create_did)?;
sim.spend_coins(ctx.take(), std::slice::from_ref(&alice.sk))?;
let did_coin = did.coin;
let _child = did.update(ctx, &alice_p2, Conditions::new())?;
let coin_spends = ctx.take();
sim.spend_coins(coin_spends.clone(), std::slice::from_ref(&alice.sk))?;
let did_spend = coin_spends
.into_iter()
.find(|s| s.coin.coin_id() == did_coin.coin_id())
.expect("the DID coin spend is present");
Ok((did_spend, did.info.launcher_id))
}
#[test]
fn resolve_owner_did_returns_the_did_for_a_did_rooted_store() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (did_spend, did_launcher_id) = did_coin_and_spend(&mut sim)?;
let did_coin_id = did_spend.coin.coin_id();
let launcher_coin = Coin::new(did_coin_id, Bytes32::new([0xb2; 32]), 1);
let store_id = launcher_coin.coin_id();
let launcher_spend = CoinSpend::new(
launcher_coin,
did_spend.puzzle_reveal.clone(),
did_spend.solution.clone(),
);
let chain = MockChainSource::new()
.with_spend(store_id, launcher_spend)
.with_spend(did_coin_id, did_spend);
let did_ref = resolve_owner_did(store_id, &chain)?.expect("store is DID-owned");
assert_eq!(
did_ref.launcher_id, did_launcher_id,
"resolve names the owning DID's launcher id"
);
Ok(())
}
#[test]
fn resolve_owner_did_returns_none_for_a_plain_store() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let alice = sim.bls(1);
let alice_p2 = StandardLayer::new(alice.pk);
let memos = ctx.hint(alice.puzzle_hash)?;
alice_p2.spend(
ctx,
alice.coin,
Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
)?;
let creator_spend = ctx
.take()
.into_iter()
.find(|s| s.coin.coin_id() == alice.coin.coin_id())
.expect("standard creator spend present");
let launcher_coin = Coin::new(alice.coin.coin_id(), Bytes32::new([0xd4; 32]), 1);
let store_id = launcher_coin.coin_id();
let launcher_spend = CoinSpend::new(
launcher_coin,
creator_spend.puzzle_reveal.clone(),
creator_spend.solution.clone(),
);
let chain = MockChainSource::new()
.with_spend(store_id, launcher_spend)
.with_spend(alice.coin.coin_id(), creator_spend);
assert_eq!(
resolve_owner_did(store_id, &chain)?,
None,
"a plainly-minted store has no owning DID"
);
Ok(())
}
#[test]
fn resolve_owner_did_none_when_launcher_spend_missing() -> anyhow::Result<()> {
let chain = MockChainSource::new();
assert_eq!(
resolve_owner_did(Bytes32::new([0xee; 32]), &chain)?,
None,
"an unknown store id resolves to None"
);
Ok(())
}
#[test]
fn resolve_owner_did_none_when_creator_spend_missing() -> anyhow::Result<()> {
let parent_id = Bytes32::new([0x2b; 32]);
let mut sim = Simulator::new();
let (any_spend, _) = did_coin_and_spend(&mut sim)?;
let launcher_coin = Coin::new(parent_id, Bytes32::new([0x3c; 32]), 1);
let store_id = launcher_coin.coin_id();
let launcher_spend = CoinSpend::new(
launcher_coin,
any_spend.puzzle_reveal.clone(),
any_spend.solution.clone(),
);
let chain = MockChainSource::new().with_spend(store_id, launcher_spend);
assert_eq!(resolve_owner_did(store_id, &chain)?, None);
Ok(())
}
#[test]
fn resolve_owner_did_rejects_a_substituted_launcher() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (did_spend, _did_launcher_id) = did_coin_and_spend(&mut sim)?;
let did_coin_id = did_spend.coin.coin_id();
let launcher_coin = Coin::new(did_coin_id, Bytes32::new([0xb2; 32]), 1);
let store_b_id = launcher_coin.coin_id();
let launcher_spend = CoinSpend::new(
launcher_coin,
did_spend.puzzle_reveal.clone(),
did_spend.solution.clone(),
);
let store_a_id = Bytes32::new([0xa1; 32]);
assert_ne!(
store_a_id, store_b_id,
"the requested id differs from the answer's coin id"
);
let chain = MockChainSource::new()
.with_spend(store_a_id, launcher_spend)
.with_spend(did_coin_id, did_spend);
assert!(
matches!(
resolve_owner_did(store_a_id, &chain),
Err(MerkleError::Chain(_))
),
"a substituted launcher is rejected, not attributed to the other store's DID"
);
Ok(())
}
#[test]
fn resolve_owner_did_rejects_a_wrong_creator() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let (did_spend, _did_launcher_id) = did_coin_and_spend(&mut sim)?;
let did_coin_id = did_spend.coin.coin_id();
let fake_parent_id = Bytes32::new([0x7f; 32]);
assert_ne!(
fake_parent_id, did_coin_id,
"the parent id is not the DID coin's id"
);
let launcher_coin = Coin::new(fake_parent_id, Bytes32::new([0xb2; 32]), 1);
let store_id = launcher_coin.coin_id();
let launcher_spend = CoinSpend::new(
launcher_coin,
did_spend.puzzle_reveal.clone(),
did_spend.solution.clone(),
);
let chain = MockChainSource::new()
.with_spend(store_id, launcher_spend)
.with_spend(fake_parent_id, did_spend);
assert!(
matches!(
resolve_owner_did(store_id, &chain),
Err(MerkleError::Chain(_))
),
"a creator spend not bound to the launcher's parent is rejected"
);
Ok(())
}
#[test]
fn resolve_owner_did_maps_chain_error() {
let chain = MockChainSource::new().fail_with(ChainSourceError::Timeout);
let result = resolve_owner_did(Bytes32::new([0x44; 32]), &chain);
assert!(
matches!(result, Err(MerkleError::Chain(_))),
"a source read error is a Chain error, not None"
);
}
}