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