action_layer_driver/singleton/
driver.rs

1//! SingletonDriver - Core driver for Action Layer singletons
2
3use chia::protocol::{Bytes32, Coin, CoinSpend};
4use chia::puzzles::singleton::{SingletonArgs, SingletonSolution, SingletonStruct};
5use chia_wallet_sdk::driver::{Launcher, SpendContext};
6use clvm_traits::{FromClvm, ToClvm};
7use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash};
8use clvmr::{Allocator, NodePtr};
9
10use crate::action_layer::ActionLayerConfig;
11use crate::error::DriverError;
12use crate::singleton::types::{LaunchResult, SingletonCoin, SingletonLineage};
13
14/// Singleton launcher puzzle hash (standard)
15pub const SINGLETON_LAUNCHER_PUZZLE_HASH: [u8; 32] =
16    hex_literal::hex!("eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9");
17
18/// Core driver for Action Layer singletons.
19///
20/// Generic over the state type `S`. Handles common singleton operations:
21/// - Launching
22/// - Building action spends
23/// - State/lineage tracking
24/// - Proof generation
25///
26/// Specific singleton implementations (NetworkSingleton, CollateralSingleton, etc.)
27/// wrap this driver and expose typed action methods.
28pub struct SingletonDriver<S> {
29    /// On-chain singleton info (None if not yet launched)
30    singleton: Option<SingletonCoin>,
31
32    /// Current state
33    state: S,
34
35    /// Action layer configuration
36    action_config: ActionLayerConfig<S>,
37
38    /// Hint for the default finalizer
39    hint: Bytes32,
40}
41
42impl<S> SingletonDriver<S>
43where
44    S: Clone + ToClvm<Allocator> + FromClvm<Allocator> + ToTreeHash,
45{
46    // ========================================================================
47    // Construction
48    // ========================================================================
49
50    /// Create a driver for a new singleton (not yet launched)
51    pub fn new(action_hashes: Vec<Bytes32>, hint: Bytes32, initial_state: S) -> Self {
52        let action_config = ActionLayerConfig::new(action_hashes, hint);
53        Self {
54            singleton: None,
55            state: initial_state,
56            action_config,
57            hint,
58        }
59    }
60
61    /// Create a driver for an existing on-chain singleton
62    pub fn from_coin(
63        singleton: SingletonCoin,
64        state: S,
65        action_hashes: Vec<Bytes32>,
66        hint: Bytes32,
67    ) -> Self {
68        let action_config = ActionLayerConfig::new(action_hashes, hint);
69        Self {
70            singleton: Some(singleton),
71            state,
72            action_config,
73            hint,
74        }
75    }
76
77    // ========================================================================
78    // Accessors
79    // ========================================================================
80
81    /// Get the launcher ID (None if not launched)
82    pub fn launcher_id(&self) -> Option<Bytes32> {
83        self.singleton.as_ref().map(|s| s.launcher_id)
84    }
85
86    /// Get the current coin (None if not launched or melted)
87    pub fn current_coin(&self) -> Option<&Coin> {
88        self.singleton.as_ref().map(|s| &s.coin)
89    }
90
91    /// Get the current state
92    pub fn state(&self) -> &S {
93        &self.state
94    }
95
96    /// Get mutable reference to state (for direct updates)
97    pub fn state_mut(&mut self) -> &mut S {
98        &mut self.state
99    }
100
101    /// Check if the singleton has been launched
102    pub fn is_launched(&self) -> bool {
103        self.singleton.is_some()
104    }
105
106    /// Get the hint
107    pub fn hint(&self) -> Bytes32 {
108        self.hint
109    }
110
111    /// Get the action layer config
112    pub fn action_config(&self) -> &ActionLayerConfig<S> {
113        &self.action_config
114    }
115
116    /// Compute the inner puzzle hash for the current state
117    pub fn inner_puzzle_hash(&self) -> TreeHash {
118        self.action_config.inner_puzzle_hash(&self.state)
119    }
120
121    /// Compute the inner puzzle hash for a given state
122    pub fn inner_puzzle_hash_for_state(&self, state: &S) -> TreeHash {
123        self.action_config.inner_puzzle_hash(state)
124    }
125
126    /// Compute the full singleton puzzle hash (None if not launched)
127    pub fn singleton_puzzle_hash(&self) -> Option<TreeHash> {
128        self.launcher_id().map(|launcher_id| {
129            SingletonArgs::curry_tree_hash(launcher_id, self.inner_puzzle_hash())
130        })
131    }
132
133    /// Compute singleton puzzle hash for a given state
134    pub fn singleton_puzzle_hash_for_state(&self, state: &S) -> Option<TreeHash> {
135        self.launcher_id().map(|launcher_id| {
136            SingletonArgs::curry_tree_hash(launcher_id, self.inner_puzzle_hash_for_state(state))
137        })
138    }
139
140    /// Get the proof for the next spend (None if not launched)
141    pub fn proof(&self) -> Option<chia::puzzles::Proof> {
142        self.singleton.as_ref().map(|s| s.proof())
143    }
144
145    // ========================================================================
146    // Action Hash Management
147    // ========================================================================
148
149    /// Update action hashes (needed after launch when network_id becomes known)
150    pub fn update_action_hashes(&mut self, action_hashes: Vec<Bytes32>) {
151        self.action_config = ActionLayerConfig::new(action_hashes, self.hint);
152    }
153
154    // ========================================================================
155    // Lifecycle Operations
156    // ========================================================================
157
158    /// Launch the singleton
159    ///
160    /// Creates the launcher spend in the context. Returns the launcher ID and
161    /// conditions to be included in the funding coin spend.
162    pub fn launch(
163        &mut self,
164        ctx: &mut SpendContext,
165        funding_coin: &Coin,
166        amount: u64,
167    ) -> Result<LaunchResult, DriverError> {
168        if self.is_launched() {
169            return Err(DriverError::AlreadyLaunched);
170        }
171
172        let inner_hash: Bytes32 = self.inner_puzzle_hash().into();
173
174        let launcher = Launcher::new(funding_coin.coin_id(), amount);
175        let launcher_id = launcher.coin().coin_id();
176
177        let (launcher_conditions, singleton_coin) = launcher
178            .spend(ctx, inner_hash, ())
179            .map_err(|e| DriverError::Launcher(format!("{:?}", e)))?;
180
181        // Update internal state
182        let lineage = SingletonLineage::eve(funding_coin.coin_id(), amount);
183        self.singleton = Some(SingletonCoin::new(launcher_id, singleton_coin, lineage));
184
185        Ok(LaunchResult {
186            launcher_id,
187            coin: singleton_coin,
188            conditions: launcher_conditions,
189        })
190    }
191
192    /// Build an action spend.
193    ///
194    /// Adds the singleton spend to the context. Does NOT update internal state -
195    /// call `apply_spend()` after the transaction confirms.
196    ///
197    /// # Arguments
198    /// * `ctx` - Spend context
199    /// * `action_index` - Index of the action in the merkle tree
200    /// * `action_puzzle` - The curried action puzzle (NodePtr)
201    /// * `action_solution` - The action solution (NodePtr)
202    pub fn build_action_spend(
203        &self,
204        ctx: &mut SpendContext,
205        action_index: usize,
206        action_puzzle: NodePtr,
207        action_solution: NodePtr,
208    ) -> Result<(), DriverError> {
209        let singleton = self.singleton.as_ref().ok_or(DriverError::NotLaunched)?;
210
211        // Build action layer spend (inner puzzle + solution)
212        let (inner_puzzle, inner_solution) = self.action_config.build_action_spend(
213            ctx,
214            self.state.clone(),
215            action_index,
216            action_puzzle,
217            action_solution,
218        )?;
219
220        // Build singleton puzzle
221        let singleton_puzzle = self.build_singleton_puzzle(ctx, inner_puzzle)?;
222
223        // Build singleton solution
224        let singleton_solution = self.build_singleton_solution(
225            ctx,
226            singleton.proof(),
227            singleton.coin.amount,
228            inner_solution,
229        )?;
230
231        // Create and insert coin spend
232        let puzzle_reveal = ctx
233            .serialize(&singleton_puzzle)
234            .map_err(|e| DriverError::Serialize(format!("{:?}", e)))?;
235        let solution = ctx
236            .serialize(&singleton_solution)
237            .map_err(|e| DriverError::Serialize(format!("{:?}", e)))?;
238
239        let coin_spend = CoinSpend::new(singleton.coin, puzzle_reveal, solution);
240        ctx.insert(coin_spend);
241
242        Ok(())
243    }
244
245    /// Update internal state after a spend confirms.
246    ///
247    /// Call this after the transaction is confirmed on chain.
248    pub fn apply_spend(&mut self, new_state: S) {
249        if let Some(singleton) = &self.singleton {
250            let launcher_id = singleton.launcher_id;
251            let old_coin = singleton.coin;
252            let old_inner_hash = self.inner_puzzle_hash();
253
254            // Update state first (needed for new puzzle hash calculation)
255            self.state = new_state;
256
257            // Compute new coin
258            let new_puzzle_hash: Bytes32 = self
259                .singleton_puzzle_hash()
260                .expect("singleton should exist")
261                .into();
262            let new_coin = Coin::new(old_coin.coin_id(), new_puzzle_hash, old_coin.amount);
263
264            // Update lineage
265            let new_lineage = SingletonLineage::lineage(old_coin, old_inner_hash);
266
267            self.singleton = Some(SingletonCoin::new(launcher_id, new_coin, new_lineage));
268        }
269    }
270
271    /// Mark the singleton as melted (destroyed).
272    ///
273    /// Call this after a melt action (like withdraw) confirms.
274    pub fn mark_melted(&mut self) {
275        self.singleton = None;
276    }
277
278    // ========================================================================
279    // Helpers
280    // ========================================================================
281
282    /// Compute the expected new coin after a spend with given new state
283    pub fn expected_new_coin(&self, new_state: &S) -> Option<Coin> {
284        let singleton = self.singleton.as_ref()?;
285        let new_inner_hash = self.inner_puzzle_hash_for_state(new_state);
286        let new_puzzle_hash: Bytes32 =
287            SingletonArgs::curry_tree_hash(singleton.launcher_id, new_inner_hash).into();
288        Some(Coin::new(
289            singleton.coin.coin_id(),
290            new_puzzle_hash,
291            singleton.coin.amount,
292        ))
293    }
294
295    /// Compute the expected child launcher ID for a child singleton
296    /// emitted by the current singleton
297    pub fn expected_child_launcher_id(&self) -> Option<Bytes32> {
298        let singleton = self.singleton.as_ref()?;
299        let child_launcher_coin = Coin::new(
300            singleton.coin.coin_id(),
301            Bytes32::new(SINGLETON_LAUNCHER_PUZZLE_HASH),
302            0,
303        );
304        Some(child_launcher_coin.coin_id())
305    }
306
307    /// Build the singleton puzzle (internal helper)
308    fn build_singleton_puzzle(
309        &self,
310        ctx: &mut SpendContext,
311        inner_puzzle: NodePtr,
312    ) -> Result<NodePtr, DriverError> {
313        let launcher_id = self.launcher_id().ok_or(DriverError::NotLaunched)?;
314
315        let singleton_mod_hash = TreeHash::new(chia_puzzles::SINGLETON_TOP_LAYER_V1_1_HASH);
316        let singleton_ptr = ctx
317            .puzzle(singleton_mod_hash, &chia_puzzles::SINGLETON_TOP_LAYER_V1_1)
318            .map_err(|e| DriverError::PuzzleLoad(format!("singleton: {:?}", e)))?;
319
320        ctx.alloc(&CurriedProgram {
321            program: singleton_ptr,
322            args: SingletonArgs {
323                singleton_struct: SingletonStruct::new(launcher_id),
324                inner_puzzle,
325            },
326        })
327        .map_err(|e| DriverError::Alloc(format!("singleton curry: {:?}", e)))
328    }
329
330    /// Build the singleton solution (internal helper)
331    fn build_singleton_solution(
332        &self,
333        ctx: &mut SpendContext,
334        proof: chia::puzzles::Proof,
335        amount: u64,
336        inner_solution: NodePtr,
337    ) -> Result<NodePtr, DriverError> {
338        ctx.alloc(&SingletonSolution {
339            lineage_proof: proof,
340            amount,
341            inner_solution,
342        })
343        .map_err(|e| DriverError::Alloc(format!("singleton solution: {:?}", e)))
344    }
345}
346
347impl<S: std::fmt::Debug> std::fmt::Debug for SingletonDriver<S> {
348    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349        f.debug_struct("SingletonDriver")
350            .field(
351                "launcher_id",
352                &self.singleton.as_ref().map(|s| hex::encode(s.launcher_id)),
353            )
354            .field("is_launched", &self.singleton.is_some())
355            .field("state", &self.state)
356            .finish()
357    }
358}