chia_sdk_driver/primitives/did/
did_info.rs

1use chia_protocol::Bytes32;
2use chia_puzzle_types::{
3    did::DidArgs,
4    singleton::{SingletonArgs, SingletonStruct},
5};
6use chia_sdk_types::Mod;
7use clvm_traits::{FromClvm, ToClvm};
8use clvm_utils::{ToTreeHash, TreeHash};
9use clvmr::Allocator;
10
11use crate::{DidLayer, DriverError, Layer, Puzzle, SingletonLayer};
12
13pub type StandardDidLayers<M, I> = SingletonLayer<DidLayer<M, I>>;
14
15/// Information needed to construct the outer puzzle of a DID.
16/// It does not include the inner puzzle, which must be stored separately.
17///
18/// This type can be used on its own for parsing, or as part of the [`Did`](crate::Did) primitive.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct DidInfo<M> {
21    /// The coin id of the launcher coin that created this DID's singleton.
22    pub launcher_id: Bytes32,
23
24    /// The hash of the recovery list. This is a very infrequently used feature
25    /// and is not fully supported at this time.
26    ///
27    /// In the Chia reference wallet, the recovery list hash must be present
28    /// even if recovery is disabled. However, in some other wallets it's allowed
29    /// to be [`None`]. This is an on-chain cost optimization and simplification.
30    pub recovery_list_hash: Option<Bytes32>,
31
32    /// The number of verifications required to recover the DID.
33    pub num_verifications_required: u64,
34
35    /// The metadata stored in the [`DidLayer`]. This can be updated freely,
36    /// but must be confirmed by an additional update spend to ensure wallets
37    /// can sync it from the parent coin.
38    pub metadata: M,
39
40    /// The hash of the inner puzzle to this DID.
41    /// If you encode this puzzle hash as bech32m, it's the same as the current owner's address.
42    pub p2_puzzle_hash: Bytes32,
43}
44
45impl<M> DidInfo<M> {
46    pub fn new(
47        launcher_id: Bytes32,
48        recovery_list_hash: Option<Bytes32>,
49        num_verifications_required: u64,
50        metadata: M,
51        p2_puzzle_hash: Bytes32,
52    ) -> Self {
53        Self {
54            launcher_id,
55            recovery_list_hash,
56            num_verifications_required,
57            metadata,
58            p2_puzzle_hash,
59        }
60    }
61
62    pub fn with_metadata<N>(self, metadata: N) -> DidInfo<N> {
63        DidInfo {
64            launcher_id: self.launcher_id,
65            recovery_list_hash: self.recovery_list_hash,
66            num_verifications_required: self.num_verifications_required,
67            metadata,
68            p2_puzzle_hash: self.p2_puzzle_hash,
69        }
70    }
71
72    /// Parses a [`DidInfo`] from a [`Puzzle`] by extracting the [`DidLayer`].
73    ///
74    /// This will return a tuple of the [`DidInfo`] and its p2 puzzle.
75    ///
76    /// If the puzzle is not a DID, this will return [`None`] instead of an error.
77    /// However, if the puzzle should have been a DID but had a parsing error, this will return an error.
78    pub fn parse(
79        allocator: &Allocator,
80        puzzle: Puzzle,
81    ) -> Result<Option<(Self, Puzzle)>, DriverError>
82    where
83        M: ToClvm<Allocator> + FromClvm<Allocator>,
84    {
85        let Some(layers) = StandardDidLayers::<M, Puzzle>::parse_puzzle(allocator, puzzle)? else {
86            return Ok(None);
87        };
88
89        let p2_puzzle = layers.inner_puzzle.inner_puzzle;
90
91        Ok(Some((Self::from_layers(layers), p2_puzzle)))
92    }
93
94    pub fn from_layers<I>(layers: StandardDidLayers<M, I>) -> Self
95    where
96        I: ToTreeHash,
97    {
98        Self {
99            launcher_id: layers.launcher_id,
100            recovery_list_hash: layers.inner_puzzle.recovery_list_hash,
101            num_verifications_required: layers.inner_puzzle.num_verifications_required,
102            metadata: layers.inner_puzzle.metadata,
103            p2_puzzle_hash: layers.inner_puzzle.inner_puzzle.tree_hash().into(),
104        }
105    }
106
107    #[must_use]
108    pub fn into_layers<I>(self, p2_puzzle: I) -> StandardDidLayers<M, I> {
109        SingletonLayer::new(
110            self.launcher_id,
111            DidLayer::new(
112                self.launcher_id,
113                self.recovery_list_hash,
114                self.num_verifications_required,
115                self.metadata,
116                p2_puzzle,
117            ),
118        )
119    }
120
121    /// Calculates the inner puzzle hash of the DID singleton.
122    ///
123    /// This includes the [`DidLayer`], but not the [`SingletonLayer`].
124    pub fn inner_puzzle_hash(&self) -> TreeHash
125    where
126        M: ToTreeHash,
127    {
128        DidArgs::curry_tree_hash(
129            self.p2_puzzle_hash.into(),
130            self.recovery_list_hash,
131            self.num_verifications_required,
132            SingletonStruct::new(self.launcher_id),
133            self.metadata.tree_hash(),
134        )
135    }
136
137    /// Calculates the full puzzle hash of the DID, which is the hash of the outer [`SingletonLayer`].
138    pub fn puzzle_hash(&self) -> TreeHash
139    where
140        M: ToTreeHash,
141    {
142        SingletonArgs::new(self.launcher_id, self.inner_puzzle_hash()).curry_tree_hash()
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use chia_sdk_test::Simulator;
149    use chia_sdk_types::Conditions;
150
151    use crate::{Launcher, SpendContext, StandardLayer};
152
153    use super::*;
154
155    #[test]
156    fn test_parse_did_info() -> anyhow::Result<()> {
157        let mut sim = Simulator::new();
158        let ctx = &mut SpendContext::new();
159
160        let alice = sim.bls(1);
161        let alice_p2 = StandardLayer::new(alice.pk);
162
163        let custom_metadata = ["Metadata".to_string(), "Example".to_string()];
164        let (create_did, did) = Launcher::new(alice.coin.coin_id(), 1).create_did(
165            ctx,
166            None,
167            1,
168            custom_metadata,
169            &alice_p2,
170        )?;
171        alice_p2.spend(ctx, alice.coin, create_did)?;
172
173        let original_did = did.clone();
174        let _did = did.update(ctx, &alice_p2, Conditions::new())?;
175
176        sim.spend_coins(ctx.take(), &[alice.sk])?;
177
178        let puzzle_reveal = sim
179            .puzzle_reveal(original_did.coin.coin_id())
180            .expect("missing did puzzle");
181
182        let mut allocator = Allocator::new();
183        let ptr = puzzle_reveal.to_clvm(&mut allocator)?;
184        let puzzle = Puzzle::parse(&allocator, ptr);
185        let (did_info, p2_puzzle) =
186            DidInfo::<[String; 2]>::parse(&allocator, puzzle)?.expect("not a did");
187
188        assert_eq!(did_info, original_did.info);
189        assert_eq!(p2_puzzle.curried_puzzle_hash(), alice.puzzle_hash.into());
190
191        Ok(())
192    }
193}