Skip to main content

ckb_sdk/traits/
mod.rs

1//! The traits defined here is intent to describe the requirements of current
2//!  library code and only implemented the trait in upper level code.
3
4pub mod default_impls;
5pub mod dummy_impls;
6pub mod light_client_impls;
7pub mod offchain_impls;
8
9pub use default_impls::{
10    DefaultCellCollector, DefaultCellDepResolver, DefaultHeaderDepResolver,
11    DefaultTransactionDependencyProvider, SecpCkbRawKeySigner,
12};
13pub use light_client_impls::{
14    LightClientCellCollector, LightClientHeaderDepResolver,
15    LightClientTransactionDependencyProvider,
16};
17pub use offchain_impls::{
18    OffchainCellCollector, OffchainCellDepResolver, OffchainHeaderDepResolver,
19    OffchainTransactionDependencyProvider,
20};
21
22#[cfg(not(target_arch = "wasm32"))]
23use ckb_hash::blake2b_256;
24#[cfg(not(target_arch = "wasm32"))]
25use ckb_traits::{CellDataProvider, ExtensionProvider, HeaderProvider};
26#[cfg(not(target_arch = "wasm32"))]
27use ckb_types::core::{
28    cell::{CellMetaBuilder, CellProvider, CellStatus, HeaderChecker},
29    error::OutPointError,
30};
31use dyn_clone::DynClone;
32use thiserror::Error;
33
34use ckb_types::{
35    bytes::Bytes,
36    core::{HeaderView, TransactionView},
37    packed::{Byte32, CellDep, CellOutput, OutPoint, Script, Transaction},
38    prelude::*,
39};
40
41use crate::{rpc::ckb_indexer::SearchMode, util::is_mature};
42
43/// Signer errors
44#[derive(Error, Debug)]
45pub enum SignerError {
46    #[error("the id is not found in the signer")]
47    IdNotFound,
48
49    #[error("invalid message, reason: `{0}`")]
50    InvalidMessage(String),
51
52    #[error("invalid transaction, reason: `{0}`")]
53    InvalidTransaction(String),
54
55    // maybe hardware wallet error or io error
56    #[error(transparent)]
57    Other(#[from] anyhow::Error),
58}
59
60/// A signer abstraction, support signer type:
61///    * secp256k1 ckb signer
62///    * secp256k1 eth signer
63///    * RSA signer
64///    * Hardware wallet signer
65pub trait Signer: Send + Sync {
66    /// typecial id are blake160(pubkey) and keccak256(pubkey)[12..20]
67    fn match_id(&self, id: &[u8]) -> bool;
68
69    /// `message` type is variable length, because different algorithm have
70    /// different length of message:
71    ///   * secp256k1 => 256bits
72    ///   * RSA       => 512bits (when key size is 1024bits)
73    fn sign(
74        &self,
75        id: &[u8],
76        message: &[u8],
77        recoverable: bool,
78        tx: &TransactionView,
79    ) -> Result<Bytes, SignerError>;
80}
81
82/// Transaction dependency provider errors
83#[derive(Error, Debug)]
84pub enum TransactionDependencyError {
85    #[error("the resource is not found in the provider: `{0}`")]
86    NotFound(String),
87
88    #[error(transparent)]
89    Other(#[from] anyhow::Error),
90}
91
92/// Provider dependency information of a transaction:
93///   * inputs
94///   * cell_deps
95///   * header_deps
96#[async_trait::async_trait]
97pub trait TransactionDependencyProvider: Sync + Send {
98    async fn get_transaction_async(
99        &self,
100        tx_hash: &Byte32,
101    ) -> Result<TransactionView, TransactionDependencyError>;
102    /// For get the output information of inputs or cell_deps, those cell should be live cell
103    async fn get_cell_async(
104        &self,
105        out_point: &OutPoint,
106    ) -> Result<CellOutput, TransactionDependencyError>;
107    /// For get the output data information of inputs or cell_deps
108    async fn get_cell_data_async(
109        &self,
110        out_point: &OutPoint,
111    ) -> Result<Bytes, TransactionDependencyError>;
112    /// For get the header information of header_deps
113    async fn get_header_async(
114        &self,
115        block_hash: &Byte32,
116    ) -> Result<HeaderView, TransactionDependencyError>;
117
118    /// For get_block_extension
119    async fn get_block_extension_async(
120        &self,
121        block_hash: &Byte32,
122    ) -> Result<Option<ckb_types::packed::Bytes>, TransactionDependencyError>;
123    /// For verify certain cell belong to certain transaction
124    #[cfg(not(target_arch = "wasm32"))]
125    fn get_transaction(
126        &self,
127        tx_hash: &Byte32,
128    ) -> Result<TransactionView, TransactionDependencyError> {
129        crate::rpc::block_on(self.get_transaction_async(tx_hash))
130    }
131    #[cfg(not(target_arch = "wasm32"))]
132    /// For get the output information of inputs or cell_deps, those cell should be live cell
133    fn get_cell(&self, out_point: &OutPoint) -> Result<CellOutput, TransactionDependencyError> {
134        crate::rpc::block_on(self.get_cell_async(out_point))
135    }
136    #[cfg(not(target_arch = "wasm32"))]
137    /// For get the output data information of inputs or cell_deps
138    fn get_cell_data(&self, out_point: &OutPoint) -> Result<Bytes, TransactionDependencyError> {
139        crate::rpc::block_on(self.get_cell_data_async(out_point))
140    }
141    #[cfg(not(target_arch = "wasm32"))]
142    /// For get the header information of header_deps
143    fn get_header(&self, block_hash: &Byte32) -> Result<HeaderView, TransactionDependencyError> {
144        crate::rpc::block_on(self.get_header_async(block_hash))
145    }
146
147    /// For get_block_extension
148    #[cfg(not(target_arch = "wasm32"))]
149    fn get_block_extension(
150        &self,
151        block_hash: &Byte32,
152    ) -> Result<Option<ckb_types::packed::Bytes>, TransactionDependencyError> {
153        crate::rpc::block_on(self.get_block_extension_async(block_hash))
154    }
155}
156
157#[cfg(not(target_arch = "wasm32"))]
158// Implement CellDataProvider trait is currently for `DaoCalculator`
159impl CellDataProvider for &dyn TransactionDependencyProvider {
160    fn get_cell_data(&self, out_point: &OutPoint) -> Option<Bytes> {
161        TransactionDependencyProvider::get_cell_data(*self, out_point).ok()
162    }
163    fn get_cell_data_hash(&self, out_point: &OutPoint) -> Option<Byte32> {
164        TransactionDependencyProvider::get_cell_data(*self, out_point)
165            .ok()
166            .map(|data| blake2b_256(data.as_ref()).pack())
167    }
168}
169#[cfg(not(target_arch = "wasm32"))]
170// Implement CellDataProvider trait is currently for `DaoCalculator`
171impl HeaderProvider for &dyn TransactionDependencyProvider {
172    fn get_header(&self, hash: &Byte32) -> Option<HeaderView> {
173        TransactionDependencyProvider::get_header(*self, hash).ok()
174    }
175}
176#[cfg(not(target_arch = "wasm32"))]
177impl HeaderChecker for &dyn TransactionDependencyProvider {
178    fn check_valid(&self, block_hash: &Byte32) -> Result<(), OutPointError> {
179        TransactionDependencyProvider::get_header(*self, block_hash)
180            .map(|_| ())
181            .map_err(|_| OutPointError::InvalidHeader(block_hash.clone()))
182    }
183}
184#[cfg(not(target_arch = "wasm32"))]
185impl CellProvider for &dyn TransactionDependencyProvider {
186    fn cell(&self, out_point: &OutPoint, _eager_load: bool) -> CellStatus {
187        match self.get_transaction(&out_point.tx_hash()) {
188            Ok(tx) => tx
189                .outputs()
190                .get(out_point.index().unpack())
191                .map(|cell| {
192                    let data = tx
193                        .outputs_data()
194                        .get(out_point.index().unpack())
195                        .expect("output data");
196
197                    let cell_meta = CellMetaBuilder::from_cell_output(cell, data.unpack())
198                        .out_point(out_point.to_owned())
199                        .build();
200
201                    CellStatus::live_cell(cell_meta)
202                })
203                .unwrap_or(CellStatus::Unknown),
204            Err(_err) => CellStatus::Unknown,
205        }
206    }
207}
208#[cfg(not(target_arch = "wasm32"))]
209impl ExtensionProvider for &dyn TransactionDependencyProvider {
210    fn get_block_extension(&self, hash: &Byte32) -> Option<ckb_types::packed::Bytes> {
211        match TransactionDependencyProvider::get_block_extension(*self, hash).ok() {
212            Some(Some(bytes)) => Some(bytes),
213            _ => None,
214        }
215    }
216}
217
218/// Cell collector errors
219#[derive(Error, Debug)]
220pub enum CellCollectorError {
221    #[error(transparent)]
222    Internal(anyhow::Error),
223
224    #[error(transparent)]
225    Other(anyhow::Error),
226}
227
228#[derive(Debug, Clone)]
229pub struct LiveCell {
230    pub output: CellOutput,
231    pub output_data: Bytes,
232    pub out_point: OutPoint,
233    pub block_number: u64,
234    pub tx_index: u32,
235}
236
237/// The value range option: `start <= value < end`
238#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
239pub struct ValueRangeOption {
240    pub start: u64,
241    pub end: u64,
242}
243impl ValueRangeOption {
244    pub fn new(start: u64, end: u64) -> ValueRangeOption {
245        ValueRangeOption { start, end }
246    }
247    pub fn new_exact(value: u64) -> ValueRangeOption {
248        ValueRangeOption {
249            start: value,
250            end: value + 1,
251        }
252    }
253    pub fn new_min(start: u64) -> ValueRangeOption {
254        ValueRangeOption {
255            start,
256            end: u64::MAX,
257        }
258    }
259    pub fn match_value(&self, value: u64) -> bool {
260        self.start <= value && value < self.end
261    }
262}
263
264/// The primary serach script type
265///   * if primary script type is `lock` then secondary script type is `type`
266///   * if primary script type is `type` then secondary script type is `lock`
267#[derive(Debug, Clone, Eq, PartialEq, Hash)]
268pub enum PrimaryScriptType {
269    Lock,
270    Type,
271}
272
273#[derive(Debug, Clone, Eq, PartialEq, Hash)]
274pub enum MaturityOption {
275    Mature,
276    Immature,
277    Both,
278}
279#[derive(Debug, Clone, Eq, PartialEq, Hash)]
280pub enum QueryOrder {
281    Desc,
282    Asc,
283}
284
285#[derive(Debug, Clone, Eq, PartialEq, Hash)]
286pub struct CellQueryOptions {
287    pub primary_script: Script,
288    pub primary_type: PrimaryScriptType,
289    pub with_data: Option<bool>,
290
291    // Options for SearchKeyFilter
292    pub secondary_script: Option<Script>,
293    pub secondary_script_len_range: Option<ValueRangeOption>,
294    pub data_len_range: Option<ValueRangeOption>,
295    pub capacity_range: Option<ValueRangeOption>,
296    pub block_range: Option<ValueRangeOption>,
297
298    pub order: QueryOrder,
299    pub limit: Option<u32>,
300    /// Filter cell by its maturity
301    pub maturity: MaturityOption,
302    /// Try to collect at least `min_total_capacity` shannons of cells, if
303    /// satisfied will stop collecting. The default value is 1 shannon means
304    /// collect only one cell at most.
305    pub min_total_capacity: u64,
306    pub script_search_mode: Option<SearchMode>,
307}
308impl CellQueryOptions {
309    pub fn new(primary_script: Script, primary_type: PrimaryScriptType) -> CellQueryOptions {
310        CellQueryOptions {
311            primary_script,
312            primary_type,
313            secondary_script: None,
314            secondary_script_len_range: None,
315            data_len_range: None,
316            capacity_range: None,
317            block_range: None,
318            with_data: None,
319            order: QueryOrder::Asc,
320            limit: None,
321            maturity: MaturityOption::Mature,
322            min_total_capacity: 1,
323            script_search_mode: None,
324        }
325    }
326    pub fn new_lock(primary_script: Script) -> CellQueryOptions {
327        CellQueryOptions::new(primary_script, PrimaryScriptType::Lock)
328    }
329    pub fn new_type(primary_script: Script) -> CellQueryOptions {
330        CellQueryOptions::new(primary_script, PrimaryScriptType::Type)
331    }
332    pub fn match_cell(&self, cell: &LiveCell, max_mature_number: u64) -> bool {
333        fn extract_raw_data(script: &Script) -> Vec<u8> {
334            [
335                script.code_hash().as_slice(),
336                script.hash_type().as_slice(),
337                &script.args().raw_data(),
338            ]
339            .concat()
340        }
341        let filter_prefix = self.secondary_script.as_ref().map(|script| {
342            if script != &Script::default() {
343                extract_raw_data(script)
344            } else {
345                Vec::new()
346            }
347        });
348        match self.primary_type {
349            PrimaryScriptType::Lock => {
350                // check primary script
351                if cell.output.lock() != self.primary_script {
352                    return false;
353                }
354
355                // if primary is `lock`, secondary is `type`
356                if let Some(prefix) = filter_prefix {
357                    if prefix.is_empty() {
358                        if cell.output.type_().is_some() {
359                            return false;
360                        }
361                    } else if cell
362                        .output
363                        .type_()
364                        .to_opt()
365                        .as_ref()
366                        .map(extract_raw_data)
367                        .filter(|data| data.starts_with(&prefix))
368                        .is_none()
369                    {
370                        return false;
371                    }
372                }
373            }
374            PrimaryScriptType::Type => {
375                // check primary script
376                if cell.output.type_().to_opt().as_ref() != Some(&self.primary_script) {
377                    return false;
378                }
379
380                // if primary is `type`, secondary is `lock`
381                if let Some(prefix) = filter_prefix {
382                    if !extract_raw_data(&cell.output.lock()).starts_with(&prefix) {
383                        return false;
384                    }
385                }
386            }
387        }
388        if let Some(range) = self.secondary_script_len_range {
389            match self.primary_type {
390                PrimaryScriptType::Lock => {
391                    let script_len = cell
392                        .output
393                        .type_()
394                        .to_opt()
395                        .map(|script| extract_raw_data(&script).len())
396                        .unwrap_or_default();
397                    if !range.match_value(script_len as u64) {
398                        return false;
399                    }
400                }
401                PrimaryScriptType::Type => {
402                    let script_len = extract_raw_data(&cell.output.lock()).len();
403                    if !range.match_value(script_len as u64) {
404                        return false;
405                    }
406                }
407            }
408        }
409
410        if let Some(range) = self.data_len_range {
411            if !range.match_value(cell.output_data.len() as u64) {
412                return false;
413            }
414        }
415        if let Some(range) = self.capacity_range {
416            let capacity: u64 = cell.output.capacity().unpack();
417            if !range.match_value(capacity) {
418                return false;
419            }
420        }
421        if let Some(range) = self.block_range {
422            if !range.match_value(cell.block_number) {
423                return false;
424            }
425        }
426        let cell_is_mature = is_mature(cell, max_mature_number);
427        match self.maturity {
428            MaturityOption::Mature => cell_is_mature,
429            MaturityOption::Immature => !cell_is_mature,
430            MaturityOption::Both => true,
431        }
432    }
433}
434
435#[async_trait::async_trait]
436pub trait CellCollector: DynClone + Send + Sync {
437    /// Collect live cells by query options, if `apply_changes` is true will
438    /// mark all collected cells as dead cells.
439    async fn collect_live_cells_async(
440        &mut self,
441        query: &CellQueryOptions,
442        apply_changes: bool,
443    ) -> Result<(Vec<LiveCell>, u64), CellCollectorError>;
444    /// Collect live cells by query options, if `apply_changes` is true will
445    /// mark all collected cells as dead cells.
446    #[cfg(not(target_arch = "wasm32"))]
447    fn collect_live_cells(
448        &mut self,
449        query: &CellQueryOptions,
450        apply_changes: bool,
451    ) -> Result<(Vec<LiveCell>, u64), CellCollectorError> {
452        crate::rpc::block_on(self.collect_live_cells_async(query, apply_changes))
453    }
454
455    /// Mark this cell as dead cell
456    fn lock_cell(
457        &mut self,
458        out_point: OutPoint,
459        tip_block_number: u64,
460    ) -> Result<(), CellCollectorError>;
461    /// Mark all inputs as dead cells and outputs as live cells in the transaction.
462    fn apply_tx(
463        &mut self,
464        tx: Transaction,
465        tip_block_number: u64,
466    ) -> Result<(), CellCollectorError>;
467
468    /// Clear cache and locked cells
469    fn reset(&mut self);
470}
471
472pub trait CellDepResolver: Send + Sync {
473    /// Resolve cell dep by script.
474    ///
475    /// When a new script is added, transaction builders use CellDepResolver to find the corresponding cell deps and add them to the transaction.
476    fn resolve(&self, script: &Script) -> Option<CellDep>;
477}
478
479#[async_trait::async_trait]
480pub trait HeaderDepResolver: Send + Sync {
481    /// Resolve header dep by trancation hash
482    async fn resolve_by_tx_async(
483        &self,
484        tx_hash: &Byte32,
485    ) -> Result<Option<HeaderView>, anyhow::Error>;
486
487    /// Resolve header dep by block number
488    async fn resolve_by_number_async(
489        &self,
490        number: u64,
491    ) -> Result<Option<HeaderView>, anyhow::Error>;
492    #[cfg(not(target_arch = "wasm32"))]
493    /// Resolve header dep by trancation hash
494    fn resolve_by_tx(&self, tx_hash: &Byte32) -> Result<Option<HeaderView>, anyhow::Error> {
495        crate::rpc::block_on(self.resolve_by_tx_async(tx_hash))
496    }
497    #[cfg(not(target_arch = "wasm32"))]
498    /// Resolve header dep by block number
499    fn resolve_by_number(&self, number: u64) -> Result<Option<HeaderView>, anyhow::Error> {
500        crate::rpc::block_on(self.resolve_by_number_async(number))
501    }
502}
503
504// test cases make sure new added exception won't breadk `anyhow!(e_variable)` usage,
505#[cfg(test)]
506mod anyhow_tests {
507    use anyhow::anyhow;
508    #[test]
509    fn test_signer_error() {
510        use super::SignerError;
511        let error = anyhow!(SignerError::IdNotFound);
512        assert_eq!("the id is not found in the signer", error.to_string());
513        let error = anyhow!(SignerError::InvalidMessage("InvalidMessage".to_string()));
514        assert_eq!(
515            "invalid message, reason: `InvalidMessage`",
516            error.to_string()
517        );
518        let error = anyhow!(SignerError::InvalidTransaction(
519            "InvalidTransaction".to_string()
520        ));
521        assert_eq!(
522            "invalid transaction, reason: `InvalidTransaction`",
523            error.to_string()
524        );
525        let error = anyhow!(SignerError::Other(anyhow::anyhow!("Other")));
526        assert_eq!("Other", error.to_string());
527    }
528
529    #[test]
530    fn test_transaction_dependency_error() {
531        use super::TransactionDependencyError;
532        let error = TransactionDependencyError::NotFound("NotFound".to_string());
533        let error = anyhow!(error);
534
535        assert_eq!(
536            "the resource is not found in the provider: `NotFound`",
537            error.to_string()
538        );
539    }
540
541    #[test]
542    fn test_cell_collector_error() {
543        use super::CellCollectorError;
544        let error = CellCollectorError::Internal(anyhow!("Internel"));
545        let error = anyhow!(error);
546        assert_eq!("Internel", error.to_string());
547
548        let error = CellCollectorError::Other(anyhow!("Other"));
549        let error = anyhow!(error);
550        assert_eq!("Other", error.to_string());
551    }
552}