Skip to main content

ree_exchange_sdk/
lib.rs

1//! The REE Exchange SDK provides a set of types and interfaces for building REE exchanges.
2//!
3//! # Example
4//! ```rust
5//! use self::exchange::*;
6//! use candid::CandidType;
7//! use ic_cdk::{query, update};
8//! use ree_exchange_sdk::{prelude::*, types::*, error::*};
9//! use serde::{Deserialize, Serialize};
10//!
11//! #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
12//! pub struct DummyPoolState {
13//!     pub txid: Txid,
14//!     pub nonce: u64,
15//!     pub coin_reserved: Vec<CoinBalance>,
16//!     pub btc_reserved: u64,
17//!     pub utxos: Vec<Utxo>,
18//!     pub attributes: String,
19//! }
20//!
21//! impl StateView for DummyPoolState {
22//!     fn inspect_state(&self) -> StateInfo {
23//!         StateInfo {
24//!             txid: self.txid,
25//!             nonce: self.nonce,
26//!             coin_reserved: self.coin_reserved.clone(),
27//!             btc_reserved: self.btc_reserved,
28//!             utxos: self.utxos.clone(),
29//!             attributes: "{}".to_string(),
30//!         }
31//!     }
32//!
33//!     fn set_nonce(&mut nonce) {
34//!         self.nonce = nonce;
35//!     }
36//! }
37//!
38//! #[exchange]
39//! pub mod exchange {
40//!     use super::*;
41//!
42//!     #[pools]
43//!     pub struct DummyPools;
44//!
45//!     impl Pools for DummyPools {
46//!         type PoolState = DummyPoolState;
47//!
48//!         type BlockState = u32;
49//!
50//!         const POOL_STATE_MEMORY: u8 = 1;
51//!
52//!         const BLOCK_STATE_MEMORY: u8 = 2;
53//!
54//!         fn network() -> Network {
55//!             Network::Testnet4
56//!         }
57//!     }
58//!
59//!     // This is optional
60//!     #[hook]
61//!     impl Hook for DummyPools {}
62//!
63//!     // `swap` is the action function that will be called by the REE Orchestrator
64//!     // All actions should return an `ActionResult<S>` where `S` is the pool state of `Pools`.
65//!     // The SDK will automatically commit this state to the IC stable memory.
66//!     #[action(name = "swap")]
67//!     pub async fn execute_swap(
68//!         psbt: &bitcoin::Psbt,
69//!         args: ActionArgs,
70//!     ) -> ActionResult<DummyPoolState> {
71//!         let pool = DummyPools::get(&args.intention.pool_address)
72//!             .ok_or(Error::PoolNotFound)?;
73//!         let mut state = pool.last_state().cloned().unwrap_or_default();
74//!         // do some checks...
75//!         state.nonce = state.nonce + 1;
76//!         state.txid = args.txid.clone();
77//!         Ok(state)
78//!     }
79//! }
80//!
81//! #[update]
82//! pub async fn new_pool(name: String) {
83//!     let metadata = Metadata::new::<DummyPools>(name)
84//!         .await
85//!         .expect("Failed to call chain-key API");
86//!     let pool = Pool::new(metadata);
87//!     DummyPools::insert(pool);
88//! }
89//!
90//! #[query]
91//! pub fn pre_swap(addr: String) -> Option<StateInfo> {
92//!     DummyPools::get(&addr).and_then(|pool| pool.last_state().map(|s| s.inspect_state()))
93//! }
94//!
95//! ic_cdk::export_candid!();
96//!```
97
98#[doc(hidden)]
99pub mod schnorr;
100#[doc(hidden)]
101pub mod states;
102pub mod store;
103pub mod prelude {
104    pub use crate::*;
105    pub use ree_exchange_sdk_macro::*;
106}
107
108use crate::types::{
109    CoinBalance, Intention, IntentionSet, Pubkey, TxRecord, Txid, Utxo, exchange_interfaces::*,
110};
111use candid::CandidType;
112use ic_stable_structures::{
113    BTreeMap, DefaultMemoryImpl, Storable, memory_manager::VirtualMemory, storable::Bound,
114};
115use serde::{Deserialize, Serialize};
116
117/// essential types of REE
118pub use ree_types as types;
119
120pub mod error {
121    pub const POOL_NOT_FOUND: u16 = 101;
122    pub const NONCE_EXPIRED: u16 = 102;
123    pub const UNKNOWN_ACTION: u16 = 103;
124    pub const ILLEGAL_PSBT: u16 = 104;
125    pub const POOL_BEING_EXECUTED: u16 = 105;
126    pub const TXID_NOT_FOUND: u16 = 106;
127    pub const NONCE_NOT_FOUND: u16 = 107;
128
129    #[derive(Clone, Debug, PartialEq, Eq)]
130    pub enum Error {
131        PoolNotFound,
132        NonceExpired,
133        UnknownAction,
134        IllegalPsbt,
135        PoolBeingExecuted,
136        TxidNotFound,
137        NonceNotFound,
138        Custom(u16, String),
139    }
140
141    impl std::fmt::Display for Error {
142        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
143            match self {
144                Error::PoolNotFound => write!(f, "{}:Pool not found", POOL_NOT_FOUND),
145                Error::NonceExpired => write!(f, "{}:Nonce expired", NONCE_EXPIRED),
146                Error::UnknownAction => write!(f, "{}:Unknown action", UNKNOWN_ACTION),
147                Error::IllegalPsbt => write!(f, "{}:Illegal PSBT", ILLEGAL_PSBT),
148                Error::TxidNotFound => write!(f, "{}:Txid not found", TXID_NOT_FOUND),
149                Error::NonceNotFound => write!(f, "{}:Nonce not found", NONCE_NOT_FOUND),
150                Error::PoolBeingExecuted => {
151                    write!(f, "{}:Pool is being executed", POOL_BEING_EXECUTED)
152                }
153                Error::Custom(code, msg) => write!(f, "{}:{}", code % 100 + 200, msg),
154            }
155        }
156    }
157}
158
159#[doc(hidden)]
160pub type BlockStateStorage<S> = BTreeMap<u32, GlobalStateWrapper<S>, Memory>;
161#[doc(hidden)]
162pub type Memory = VirtualMemory<DefaultMemoryImpl>;
163#[doc(hidden)]
164pub type BlockStorage = BTreeMap<u32, Block, Memory>;
165#[doc(hidden)]
166pub type UnconfirmedTxStorage = BTreeMap<Txid, TxRecord, Memory>;
167#[doc(hidden)]
168pub type PoolStorage<S> = BTreeMap<String, Pool<S>, Memory>;
169
170/// The network enum defines the networks supported by the exchange.
171#[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Copy)]
172pub enum Network {
173    Bitcoin,
174    Testnet4,
175}
176
177impl Into<crate::types::bitcoin::Network> for Network {
178    fn into(self) -> crate::types::bitcoin::Network {
179        match self {
180            Network::Bitcoin => crate::types::bitcoin::Network::Bitcoin,
181            Network::Testnet4 => crate::types::bitcoin::Network::Testnet4,
182        }
183    }
184}
185
186#[doc(hidden)]
187pub fn ensure_access<P: Pools>() -> Result<(), String> {
188    match P::network() {
189        Network::Bitcoin => crate::types::orchestrator_interfaces::ensure_orchestrator(),
190        Network::Testnet4 => crate::types::orchestrator_interfaces::ensure_testnet4_orchestrator(),
191    }
192}
193
194/// The parameters for the hook `on_block_confirmed` and `on_block_finalized`
195#[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
196pub struct Block {
197    /// The height of the block just received
198    pub block_height: u32,
199    /// The block hash
200    pub block_hash: String,
201    /// The block timestamp in seconds since the Unix epoch.
202    pub block_timestamp: u64,
203    /// transactions confirmed in this block
204    pub txs: Vec<TxRecord>,
205}
206
207impl Storable for Block {
208    fn to_bytes(&self) -> std::borrow::Cow<'_, [u8]> {
209        let bytes = bincode::serialize(self).unwrap();
210        std::borrow::Cow::Owned(bytes)
211    }
212
213    fn into_bytes(self) -> Vec<u8> {
214        bincode::serialize(&self).unwrap()
215    }
216
217    fn from_bytes(bytes: std::borrow::Cow<'_, [u8]>) -> Self {
218        bincode::deserialize(bytes.as_ref()).unwrap()
219    }
220
221    const BOUND: Bound = Bound::Unbounded;
222}
223
224/// The metadata for the pool, which includes the key, name, and address.
225/// Typically, the key and address should be generated by the IC chain-key.
226#[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
227pub struct Metadata {
228    pub key: Pubkey,
229    pub key_derivation_path: Vec<Vec<u8>>,
230    pub name: String,
231    pub address: String,
232}
233
234impl Metadata {
235    /// Creates a new metadata instance with the given name. It will automatically generate the key and address.
236    pub async fn new<P: Pools>(name: String) -> Result<Self, String> {
237        let key_derivation_path: Vec<Vec<u8>> = vec![name.clone().into_bytes()];
238        let (key, _, address) =
239            crate::schnorr::request_p2tr_address(key_derivation_path.clone(), P::network())
240                .await
241                .map_err(|e| format!("Failed to generate pool address: {}", e))?;
242        Ok(Self {
243            key,
244            key_derivation_path,
245            name,
246            address: address.to_string(),
247        })
248    }
249}
250
251/// The essential information about the pool state.
252#[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
253pub struct StateInfo {
254    pub nonce: u64,
255    pub txid: Txid,
256    pub coin_reserved: Vec<CoinBalance>,
257    pub btc_reserved: u64,
258    pub utxos: Vec<Utxo>,
259    pub attributes: String,
260}
261
262/// The parameter for the action function, which is used to execute a transaction in the exchange.
263#[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
264pub struct ActionArgs {
265    pub txid: Txid,
266    pub initiator_address: String,
267    pub intention: Intention,
268    pub other_intentions: Vec<Intention>,
269    pub unconfirmed_tx_count: usize,
270    pub is_reapply: bool,
271}
272
273impl From<ExecuteTxArgs> for ActionArgs {
274    fn from(args: ExecuteTxArgs) -> Self {
275        let ExecuteTxArgs {
276            psbt_hex: _,
277            txid,
278            intention_set,
279            intention_index,
280            zero_confirmed_tx_queue_length,
281            is_reapply,
282        } = args;
283        let IntentionSet {
284            mut intentions,
285            initiator_address,
286            tx_fee_in_sats: _,
287        } = intention_set;
288        let intention = intentions.swap_remove(intention_index as usize);
289        Self {
290            txid,
291            initiator_address,
292            intention,
293            other_intentions: intentions,
294            unconfirmed_tx_count: zero_confirmed_tx_queue_length as usize,
295            is_reapply: is_reapply.unwrap_or(false),
296        }
297    }
298}
299
300/// The result type for actions in the exchange, which can either be successful with a state or an error message.
301pub type ActionResult<S> = Result<S, error::Error>;
302
303/// User must implement the `StateView` trait for customized state to provide this information.
304pub trait StateView {
305    fn inspect_state(&self) -> StateInfo;
306
307    fn set_nonce(&mut self, nonce: u64);
308}
309
310/// The concrete type stored in the IC stable memory.
311/// The SDK will automatically manage the pool state `S`.
312#[derive(Clone, Debug, Deserialize, Serialize)]
313pub struct Pool<S> {
314    metadata: Metadata,
315    states: Vec<S>,
316}
317
318impl<S> Storable for Pool<S>
319where
320    S: Serialize + for<'de> Deserialize<'de>,
321{
322    const BOUND: Bound = Bound::Unbounded;
323
324    fn to_bytes(&self) -> std::borrow::Cow<'_, [u8]> {
325        let bytes = bincode::serialize(self).unwrap();
326        std::borrow::Cow::Owned(bytes)
327    }
328
329    fn into_bytes(self) -> Vec<u8> {
330        bincode::serialize(&self).unwrap()
331    }
332
333    fn from_bytes(bytes: std::borrow::Cow<'_, [u8]>) -> Self {
334        bincode::deserialize(bytes.as_ref()).unwrap()
335    }
336}
337
338impl<S> Pool<S>
339where
340    S: StateView,
341{
342    /// Creates a new pool with the given metadata.
343    pub fn new(metadata: Metadata) -> Self {
344        Self {
345            metadata,
346            states: Vec::new(),
347        }
348    }
349
350    /// Returns the metadata of the pool.
351    pub fn metadata(&self) -> &Metadata {
352        &self.metadata
353    }
354
355    /// Returns a reference of the last state of the pool.
356    pub fn last_state(&self) -> Option<&S> {
357        self.states.last()
358    }
359
360    /// Returns the states of the pool.
361    pub fn states(&self) -> &Vec<S> {
362        &self.states
363    }
364
365    /// Return the state matches the given txid.
366    pub fn get(&self, txid: Txid) -> Option<&S> {
367        self.states
368            .iter()
369            .find(|state| state.inspect_state().txid == txid)
370    }
371
372    /// Returns a mutable reference to the states of the pool.
373    pub fn states_mut(&mut self) -> &mut Vec<S> {
374        &mut self.states
375    }
376}
377
378#[doc(hidden)]
379pub trait ReePool<S> {
380    fn get_pool_info(&self) -> PoolInfo;
381
382    fn get_pool_basic(&self) -> PoolBasic;
383
384    fn truncate(&mut self, nonce: u64) -> Result<(), String>;
385
386    fn rollback(&mut self, txid: Txid) -> Result<Vec<S>, String>;
387
388    fn finalize(&mut self, txid: Txid) -> Result<(), String>;
389}
390
391#[doc(hidden)]
392impl<S> ReePool<S> for Pool<S>
393where
394    S: StateView,
395{
396    fn get_pool_basic(&self) -> PoolBasic {
397        PoolBasic {
398            name: self.metadata.name.clone(),
399            address: self.metadata.address.clone(),
400        }
401    }
402
403    fn get_pool_info(&self) -> PoolInfo {
404        let metadata: Metadata = self.metadata.clone();
405        let Metadata {
406            key,
407            key_derivation_path,
408            name,
409            address,
410        } = metadata;
411        let state = self
412            .states
413            .last()
414            .map(|s| s.inspect_state())
415            .unwrap_or_default();
416        let StateInfo {
417            txid: _,
418            nonce,
419            coin_reserved,
420            btc_reserved,
421            utxos,
422            attributes,
423        } = state;
424        PoolInfo {
425            key,
426            key_derivation_path,
427            name,
428            address,
429            nonce,
430            coin_reserved,
431            btc_reserved,
432            utxos,
433            attributes,
434        }
435    }
436
437    fn truncate(&mut self, nonce: u64) -> Result<(), String> {
438        while let Some(state) = self.states.last() {
439            if state.inspect_state().nonce >= nonce {
440                self.states.pop();
441            } else {
442                break;
443            }
444        }
445        Ok(())
446    }
447
448    fn rollback(&mut self, txid: Txid) -> Result<Vec<S>, String> {
449        let idx = self
450            .states
451            .iter()
452            .position(|state| state.inspect_state().txid == txid)
453            .ok_or("txid not found".to_string())?;
454
455        let mut rollbacked_states = vec![];
456        while self.states.len() > idx {
457            rollbacked_states.push(self.states.pop().unwrap());
458        }
459
460        Ok(rollbacked_states)
461    }
462
463    fn finalize(&mut self, txid: Txid) -> Result<(), String> {
464        let idx = self
465            .states
466            .iter()
467            .position(|state| state.inspect_state().txid == txid)
468            .ok_or("txid not found".to_string())?;
469        if idx == 0 {
470            return Ok(());
471        }
472        self.states.rotate_left(idx);
473        self.states.truncate(self.states.len() - idx);
474        Ok(())
475    }
476}
477
478/// The Pools trait defines the interface for the exchange pools, must be marked as `#[ree_exchange_sdk::pools]`.
479pub trait Pools {
480    /// The concrete type of the pool state.
481    type PoolState: StateView + Serialize + for<'de> Deserialize<'de>;
482
483    /// The concret type of the block state.
484    type BlockState: Serialize + for<'de> Deserialize<'de>;
485
486    /// The memory ID for the block state storage.
487    const BLOCK_STATE_MEMORY: u8;
488
489    /// The memory ID for the pool state storage.
490    const POOL_STATE_MEMORY: u8;
491
492    /// useful for ensuring that the exchange is running on the correct network.
493    fn network() -> Network;
494
495    /// Returns the state finalize threshold, useful for determining when a transaction is considered finalized.
496    /// For `Testnet4`, it should be great than 60 while in `Bitcoin` it should be ~ 3-6.
497    fn finalize_threshold() -> u32 {
498        60
499    }
500}
501
502/// A hook that can be implemented to respond to block event in the exchange lifecycle.
503/// It must be implemented over the `BlockState` type and marked as `#[ree_exchange_sdk::hook]`.
504pub trait Hook: Pools {
505    /// This function is called when a transaction is rejected and never confirmed.
506    fn on_tx_rollbacked(
507        _address: String,
508        _txid: Txid,
509        _reason: String,
510        _rollbacked_states: Vec<Self::PoolState>,
511    ) {
512    }
513
514    /// This function is called when a transaction is placed in a new block, before the `on_block_confirmed`.
515    fn on_tx_confirmed(_address: String, _txid: Txid, _block: Block) {}
516
517    /// This function is called when a block is received.
518    fn on_block_confirmed(_block: Block) {}
519
520    /// This function is called when a block is received but before any other hooks.
521    fn pre_block_confirmed(_height: u32) {}
522}
523
524/// A trait for accessing the pool storage.
525/// The user-defined `Pools` type will automatically implement this trait.
526pub trait PoolStorageAccess<P: Pools> {
527    fn block_state() -> Option<P::BlockState>;
528
529    fn commit(height: u32, block_state: P::BlockState) -> Result<(), String>;
530
531    fn get(address: &String) -> Option<Pool<P::PoolState>>;
532
533    fn insert(pool: Pool<P::PoolState>);
534
535    fn remove(address: &String) -> Option<Pool<P::PoolState>>;
536
537    fn iter() -> iter::PoolIterator<P>;
538}
539
540#[doc(hidden)]
541#[derive(Clone, Debug, Deserialize, Serialize)]
542pub struct GlobalStateWrapper<S> {
543    pub inner: S,
544}
545
546#[doc(hidden)]
547impl<S> GlobalStateWrapper<S> {
548    pub fn new(s: S) -> Self {
549        Self { inner: s }
550    }
551}
552
553#[doc(hidden)]
554impl<S> Storable for GlobalStateWrapper<S>
555where
556    S: Serialize + for<'de> Deserialize<'de>,
557{
558    const BOUND: Bound = Bound::Unbounded;
559
560    fn to_bytes(&self) -> std::borrow::Cow<'_, [u8]> {
561        let bytes = bincode::serialize(self).unwrap();
562        std::borrow::Cow::Owned(bytes)
563    }
564
565    fn into_bytes(self) -> Vec<u8> {
566        bincode::serialize(&self).unwrap()
567    }
568
569    fn from_bytes(bytes: std::borrow::Cow<'_, [u8]>) -> Self {
570        bincode::deserialize(bytes.as_ref()).unwrap()
571    }
572}
573
574/// The Upgrade trait is used to handle state migrations when the state type of a Pools implementation changes.
575/// Assume `MyPools` originally has a pool state type `MyPoolState` and block state type `MyBlockState`.
576///
577/// ```rust
578/// #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
579/// pub struct MyPoolState {
580///     pub txid: Txid,
581///     pub nonce: u64,
582///     pub coin_reserved: Vec<CoinBalance>,
583///     pub btc_reserved: u64,
584///     pub utxos: Vec<Utxo>,
585///     pub attributes: String,
586/// }
587///
588/// #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
589/// pub struct MyBlockState {
590///     pub block_number: u32,
591/// }
592///
593/// impl Pools for MyPools {
594///     type PoolState = MyPoolState;
595///
596///     type BlockState = MyBlockState;
597///
598///     const POOL_STATE_MEMORY: u8 = 1;
599///
600///     const BLOCK_STATE_MEMORY: u8 = 2;
601/// }
602/// ```
603/// Now we would like to update the `MyPoolState` type.
604///
605/// The best practice is to rename the `MyPoolState` to `OldPoolState` and define a new state type `MyPoolState`
606///
607/// ```rust
608/// #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
609/// pub struct OldPoolState {
610///     pub txid: Txid,
611///     pub nonce: u64,
612///     pub coin_reserved: Vec<CoinBalance>,
613///     pub btc_reserved: u64,
614///     pub utxos: Vec<Utxo>,
615///     pub attributes: String,
616/// }
617///
618/// #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
619/// pub struct MyPoolState {
620///     pub txid: Txid,
621///     pub nonce: u64,
622///     pub coin_reserved: Vec<CoinBalance>,
623///     pub btc_reserved: u64,
624///     pub utxos: Vec<Utxo>,
625///     pub attributes: String,
626///     pub new_field: u32,
627/// }
628///
629/// impl Into<MyState> for OldState {
630///     fn into(self) -> MyState {
631///         // ...
632///     }
633/// }
634///
635/// #[upgrade]
636/// impl Upgrade<MyPools> for MyPools {
637///     type PoolState = OldState;
638///
639///     type BlockState = u32;
640///
641///     // there is where we store the pool data before upgrade
642///     const POOL_STATE_MEMORY: u8 = 1;
643///
644///     const BLOCK_STATE_MEMORY: u8 = 2;
645/// }
646///
647/// impl Pools for MyPools {
648///     type PoolState = MyPoolState;
649///
650///     type BlockState = u32;
651///
652///     // this is where we store the pool data after upgrade
653///     const POOL_STATE_MEMORY: u8 = 3;
654///
655///     const BLOCK_STATE_MEMORY: u8 = 4;
656/// }
657///
658/// ```
659/// Now you can call `MyPools::upgrade()` in the `post_upgrade` hook.
660pub trait Upgrade<P: Pools> {
661    /// The previous pool state type before the upgrade.
662    type PoolState: Into<P::PoolState> + for<'de> Deserialize<'de> + Clone;
663
664    /// The previous block state type before the upgrade.
665    type BlockState: Into<P::BlockState> + for<'de> Deserialize<'de> + Clone;
666
667    /// The memory ID for the pool state storage in the previous version.
668    const POOL_STATE_MEMORY: u8;
669
670    /// The memory ID for the block state storage in the previous version.
671    const BLOCK_STATE_MEMORY: u8;
672}
673
674#[doc(hidden)]
675pub fn iterator<P>(memory: Memory) -> iter::PoolIterator<P>
676where
677    P: Pools,
678{
679    let inner = PoolStorage::<P::PoolState>::init(memory);
680    let keys = inner.keys().collect::<Vec<_>>();
681    iter::PoolIterator {
682        inner,
683        cursor: 0,
684        keys,
685    }
686}
687
688#[doc(hidden)]
689pub mod iter {
690    pub struct PoolIterator<P: super::Pools> {
691        pub(crate) inner: super::PoolStorage<P::PoolState>,
692        pub(crate) cursor: usize,
693        pub(crate) keys: Vec<String>,
694    }
695
696    impl<P> std::iter::Iterator for PoolIterator<P>
697    where
698        P: super::Pools,
699    {
700        type Item = (String, super::Pool<P::PoolState>);
701
702        fn next(&mut self) -> Option<Self::Item> {
703            if self.cursor < self.keys.len() {
704                let key = self.keys[self.cursor].clone();
705                self.cursor += 1;
706                self.inner.get(&key).map(|v| (key.clone(), v))
707            } else {
708                None
709            }
710        }
711    }
712}
713
714#[cfg(test)]
715pub mod test {
716    use std::str::FromStr;
717
718    use super::*;
719
720    #[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
721    struct DummyPoolState {
722        nonce: u64,
723        txid: Txid,
724        coin_reserved: Vec<CoinBalance>,
725        btc_reserved: u64,
726        utxos: Vec<Utxo>,
727        attributes: String,
728    }
729
730    impl StateView for DummyPoolState {
731        fn inspect_state(&self) -> StateInfo {
732            StateInfo {
733                txid: self.txid.clone(),
734                nonce: self.nonce,
735                coin_reserved: self.coin_reserved.clone(),
736                btc_reserved: self.btc_reserved,
737                utxos: self.utxos.clone(),
738                attributes: self.attributes.clone(),
739            }
740        }
741
742        fn set_nonce(&mut self, nonce: u64) {
743            self.nonce = nonce;
744        }
745    }
746
747    #[test]
748    pub fn test_candid_and_bincode_serialize() {
749        let state = DummyPoolState {
750            nonce: 1,
751            txid: Txid::default(),
752            coin_reserved: vec![],
753            btc_reserved: 0,
754            utxos: vec![],
755            attributes: "{}".to_string(),
756        };
757        let pool = Pool::<DummyPoolState> {
758            metadata: Metadata {
759                key: Pubkey::from_raw(vec![2u8; 33]).unwrap(),
760                key_derivation_path: vec![vec![0; 32]],
761                name: "Test Pool".to_string(),
762                address: "test-address".to_string(),
763            },
764            states: vec![state.clone()],
765        };
766        let bincode_serialized = pool.to_bytes();
767        Pool::<DummyPoolState>::from_bytes(bincode_serialized);
768        assert_eq!(pool.metadata.name, "Test Pool");
769
770        let mut candid_ser = candid::ser::IDLBuilder::new();
771        candid_ser.arg(&state).unwrap();
772        let candid_serialized = candid_ser.serialize_to_vec();
773        assert!(candid_serialized.is_ok());
774        let candid_serialized = candid_serialized.unwrap();
775        let mut candid_de = candid::de::IDLDeserialize::new(&candid_serialized).unwrap();
776        let candid_deserialized = candid_de.get_value::<DummyPoolState>();
777        assert!(candid_deserialized.is_ok());
778    }
779
780    #[test]
781    fn test_pool_rollback() {
782        let mut pool = Pool::<DummyPoolState> {
783            metadata: Metadata {
784                key: Pubkey::from_raw(vec![2u8; 33]).unwrap(),
785                key_derivation_path: vec![vec![0; 32]],
786                name: "Test Pool".to_string(),
787                address: "test-address".to_string(),
788            },
789            states: vec![],
790        };
791        let push_random_state_by_txid = |txid: &str, pool: &mut Pool<DummyPoolState>| {
792            let txid = Txid::from_str(txid).unwrap();
793            let nonce = pool.states.len() as u64;
794            let state = DummyPoolState {
795                nonce,
796                txid,
797                coin_reserved: vec![],
798                btc_reserved: 0,
799                utxos: vec![],
800                attributes: "{}".to_string(),
801            };
802            pool.states.push(state);
803        };
804
805        let txs = [
806            "51230fe70deae44a92f8f44a600585e3e57b8c8720a0b67c4c422f579d9ace2a",
807            "51230fe70deae44a92f8f44a600585e3e57b8c8720a0b67c4c422f579d9ace2b",
808            "51230fe70deae44a92f8f44a600585e3e57b8c8720a0b67c4c422f579d9ace2c",
809        ];
810
811        let init_pool_state = |pool: &mut Pool<DummyPoolState>| {
812            pool.states.clear();
813            for txid in txs.iter() {
814                push_random_state_by_txid(txid, pool);
815            }
816        };
817
818        // test rollback first tx
819        init_pool_state(&mut pool);
820        assert_eq!(pool.states.len(), 3);
821        let before_rollback_states = pool.states.clone();
822        let rollbacked_states = pool.rollback(Txid::from_str(txs[0]).unwrap()).unwrap();
823        assert_eq!(rollbacked_states.len(), 3);
824        assert_eq!(pool.states.len(), 0);
825        assert_eq!(rollbacked_states[0], before_rollback_states[2]);
826        assert_eq!(rollbacked_states[1], before_rollback_states[1]);
827        assert_eq!(rollbacked_states[2], before_rollback_states[0]);
828
829        // test rollback mid tx
830        init_pool_state(&mut pool);
831        assert_eq!(pool.states.len(), 3);
832        let before_rollback_states = pool.states.clone();
833        let rollbacked_states = pool.rollback(Txid::from_str(txs[1]).unwrap()).unwrap();
834        assert_eq!(rollbacked_states.len(), 2);
835        assert_eq!(pool.states.len(), 1);
836        assert_eq!(pool.states[0], before_rollback_states[0]);
837        assert_eq!(rollbacked_states[0], before_rollback_states[2]);
838        assert_eq!(rollbacked_states[1], before_rollback_states[1]);
839
840        // test rollback last tx
841        init_pool_state(&mut pool);
842        assert_eq!(pool.states.len(), 3);
843        let before_rollback_states = pool.states.clone();
844        let rollbacked_states = pool.rollback(Txid::from_str(txs[2]).unwrap()).unwrap();
845        assert_eq!(rollbacked_states.len(), 1);
846        assert_eq!(pool.states.len(), 2);
847        assert_eq!(pool.states[0], before_rollback_states[0]);
848        assert_eq!(pool.states[1], before_rollback_states[1]);
849        assert_eq!(rollbacked_states[0], before_rollback_states[2]);
850    }
851}