diff --git a/.github/workflows/ci-wasm-build.yml b/.github/workflows/ci-wasm-build.yml new file mode 100644 index 00000000..313c4e17 --- /dev/null +++ b/.github/workflows/ci-wasm-build.yml @@ -0,0 +1,26 @@ +name: wasm ci + +on: + pull_request: + types: [ opened, synchronize, reopened ] + push: + branches: + - "develop" + - "master" + - "pkg/*" + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: full + +jobs: + build-test: + runs-on: 'ubuntu-latest' + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: | + rustup target add wasm32-unknown-unknown + - name: Build + run: | + cargo build --target wasm32-unknown-unknown --no-default-features diff --git a/Cargo.toml b/Cargo.toml index 069b56d5..1b0f2d4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ license = "MIT" description = "Rust SDK for CKB" homepage = "https://github.com/nervosnetwork/ckb-sdk-rust" repository = "https://github.com/nervosnetwork/ckb-sdk-rust" - +resolver = "2" [dependencies] serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0" @@ -23,7 +23,7 @@ log = "0.4.6" reqwest = { version = "0.12", default-features = false, features = ["json"] } secp256k1 = { version = "0.30.0", features = ["recovery"] } tokio-util = { version = "0.7.7", features = ["codec"] } -tokio = { version = "1" } +tokio = { version = "1", features = ["time"] } bytes = "1" futures = "0.3" jsonrpc-core = "18" @@ -50,14 +50,20 @@ rand = { version = "0.7.3", optional = true } ckb-mock-tx-types = { version = "0.200.0" } ckb-chain-spec = "0.200.0" -sparse-merkle-tree = { version = "0.6", optional = true} +sparse-merkle-tree = { version = "0.6", optional = true } +async-iterator = "2.3.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2.16", features = ["js"] } +web-time = "1.1.0" +tokio_with_wasm = { version = "0.8.2", features = ["time"] } [features] default = ["default-tls"] default-tls = ["reqwest/default-tls"] native-tls-vendored = ["reqwest/native-tls-vendored"] rustls-tls = ["reqwest/rustls-tls"] -test = [] +test = ["rce", "rand", "default-tls"] rce = ["sparse-merkle-tree"] [dev-dependencies] diff --git a/README.md b/README.md index 5149ae23..2cf79f8b 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,10 @@ cargo build --examples For more use cases of building transactions with CKB node, please refer to [these examples](./examples/) and [unit tests](./src/tests/). +## Wasm + +`ckb-sdk-rust` has limited support for `wasm32-unknown-unknown`. Some trait are not implemented on `wasm32`, such as `impl CellDataProvider for &dyn TransactionDependencyProvider`. To use on wasm, default features must be disabled + ## License The SDK is available as open source under the terms of the [MIT License](./LICENSE). diff --git a/examples/script_unlocker_example.rs b/examples/script_unlocker_example.rs index 28b73122..8ec59049 100644 --- a/examples/script_unlocker_example.rs +++ b/examples/script_unlocker_example.rs @@ -17,7 +17,8 @@ use std::collections::HashMap; /// [CapacityDiff]: https://github.com/doitian/ckb-sdk-examples-capacity-diff struct CapacityDiffUnlocker {} -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl ScriptUnlocker for CapacityDiffUnlocker { // This works for any args fn match_args(&self, _args: &[u8]) -> bool { diff --git a/rust-toolchain b/rust-toolchain index dbd41264..f288d111 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.81.0 +1.85.0 diff --git a/src/lib.rs b/src/lib.rs index 393a13df..9d2d0384 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,9 @@ pub mod test_util; #[cfg(test)] mod tests; -pub use rpc::{CkbRpcAsyncClient, CkbRpcClient, IndexerRpcAsyncClient, IndexerRpcClient, RpcError}; +pub use rpc::{CkbRpcAsyncClient, IndexerRpcAsyncClient, RpcError}; +#[cfg(not(target_arch = "wasm32"))] +pub use rpc::{CkbRpcClient, IndexerRpcClient}; pub use types::{ Address, AddressPayload, AddressType, CodeHashIndex, HumanCapacity, NetworkInfo, NetworkType, OldAddress, OldAddressFormat, ScriptGroup, ScriptGroupType, ScriptId, Since, SinceType, @@ -24,3 +26,15 @@ pub use types::{ }; pub use ckb_crypto::secp::SECP256K1; + +#[cfg(target_arch = "wasm32")] +mod target_specific { + pub trait MaybeSend {} + impl MaybeSend for T {} +} +#[cfg(not(target_arch = "wasm32"))] +mod target_specific { + pub trait MaybeSend: Send {} + impl MaybeSend for T where T: Send {} +} +pub use target_specific::MaybeSend; diff --git a/src/rpc/ckb.rs b/src/rpc/ckb.rs index 2cbb3f82..f7256602 100644 --- a/src/rpc/ckb.rs +++ b/src/rpc/ckb.rs @@ -13,7 +13,7 @@ use ckb_types::{core::Cycle, H256}; use super::{ckb_indexer::CellsCapacity, ResponseFormatGetter}; pub use super::ckb_indexer::{Cell, Order, Pagination, SearchKey, Tip, Tx}; - +#[cfg(not(target_arch = "wasm32"))] crate::jsonrpc!(pub struct CkbRpcClient { // Chain pub fn get_block(&self, hash: H256) -> Option; @@ -211,7 +211,7 @@ fn transform_cycles(cycles: Option>) -> Vec .map(|c| c.into_iter().map(Into::into).collect()) .unwrap_or_default() } - +#[cfg(not(target_arch = "wasm32"))] impl From<&CkbRpcClient> for CkbRpcAsyncClient { fn from(value: &CkbRpcClient) -> Self { Self { @@ -220,7 +220,7 @@ impl From<&CkbRpcClient> for CkbRpcAsyncClient { } } } - +#[cfg(not(target_arch = "wasm32"))] impl CkbRpcClient { pub fn get_packed_block(&self, hash: H256) -> Result, crate::RpcError> { self.post("get_block", (hash, Some(Uint32::from(0u32)))) diff --git a/src/rpc/ckb_indexer.rs b/src/rpc/ckb_indexer.rs index 79caa5fc..83e89080 100644 --- a/src/rpc/ckb_indexer.rs +++ b/src/rpc/ckb_indexer.rs @@ -188,7 +188,7 @@ pub struct Pagination { pub objects: Vec, pub last_cursor: JsonBytes, } - +#[cfg(not(target_arch = "wasm32"))] crate::jsonrpc!(pub struct IndexerRpcClient { pub fn get_indexer_tip(&self) -> Option; pub fn get_cells(&self, search_key: SearchKey, order: Order, limit: Uint32, after: Option) -> Pagination; @@ -202,7 +202,7 @@ crate::jsonrpc_async!(pub struct IndexerRpcAsyncClient { pub fn get_transactions(&self, search_key: SearchKey, order: Order, limit: Uint32, after: Option) -> Pagination; pub fn get_cells_capacity(&self, search_key: SearchKey) -> Option; }); - +#[cfg(not(target_arch = "wasm32"))] impl From<&IndexerRpcClient> for IndexerRpcAsyncClient { fn from(value: &IndexerRpcClient) -> Self { Self { diff --git a/src/rpc/ckb_light_client.rs b/src/rpc/ckb_light_client.rs index 4460a2c6..0e571299 100644 --- a/src/rpc/ckb_light_client.rs +++ b/src/rpc/ckb_light_client.rs @@ -135,7 +135,7 @@ pub struct PeerSyncState { /// Proved best known header of remote peer. pub proved_best_known_header: Option, } - +#[cfg(not(target_arch = "wasm32"))] crate::jsonrpc!(pub struct LightClientRpcClient { // BlockFilter pub fn set_scripts(&self, scripts: Vec, command: Option) -> (); @@ -199,7 +199,7 @@ crate::jsonrpc_async!(pub struct LightClientRpcAsyncClient { pub fn get_peers(&self) -> Vec; pub fn local_node_info(&self) -> LocalNode; }); - +#[cfg(not(target_arch = "wasm32"))] impl From<&LightClientRpcClient> for LightClientRpcAsyncClient { fn from(value: &LightClientRpcClient) -> Self { Self { diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 8c12e4c3..a342c291 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -3,14 +3,23 @@ pub mod ckb_indexer; pub mod ckb_light_client; use anyhow::anyhow; -pub use ckb::{CkbRpcAsyncClient, CkbRpcClient}; -pub use ckb_indexer::{IndexerRpcAsyncClient, IndexerRpcClient}; -use ckb_jsonrpc_types::{JsonBytes, ResponseFormat}; -pub use ckb_light_client::{LightClientRpcAsyncClient, LightClientRpcClient}; +#[cfg(not(target_arch = "wasm32"))] +pub use ckb::CkbRpcClient; +#[cfg(not(target_arch = "wasm32"))] +pub use ckb_indexer::IndexerRpcClient; +#[cfg(not(target_arch = "wasm32"))] +pub use ckb_light_client::LightClientRpcClient; + +pub use ckb::CkbRpcAsyncClient; +pub use ckb_indexer::IndexerRpcAsyncClient; +use ckb_jsonrpc_types::{JsonBytes, ResponseFormat}; +pub use ckb_light_client::LightClientRpcAsyncClient; +#[cfg(not(target_arch = "wasm32"))] use std::future::Future; use thiserror::Error; +#[cfg(not(target_arch = "wasm32"))] pub(crate) fn block_on(future: impl Future + Send) -> F { match tokio::runtime::Handle::try_current() { Ok(h) @@ -55,6 +64,7 @@ pub enum RpcError { Other(#[from] anyhow::Error), } +#[cfg(not(target_arch = "wasm32"))] #[macro_export] macro_rules! jsonrpc { ( @@ -159,7 +169,7 @@ macro_rules! jsonrpc_async { pub fn new(uri: &str) -> Self { $struct_name { id: 0.into(), client: $crate::rpc::RpcClient::new(uri), } } - + #[cfg(not(target_arch="wasm32"))] pub fn post(&self, method:&str, params: PARAM)->impl std::future::Future> + Send + 'static where PARAM:serde::ser::Serialize + Send + 'static, @@ -181,7 +191,28 @@ macro_rules! jsonrpc_async { self.client.post(params_fn) } + #[cfg(target_arch="wasm32")] + pub fn post(&self, method:&str, params: PARAM)->impl std::future::Future> + 'static + where + PARAM:serde::ser::Serialize + Send + 'static, + RET: serde::de::DeserializeOwned + Send + 'static, + { + let id = self.id.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let method = serde_json::json!(method); + let params_fn = move || -> Result<_,_> { + let params = serde_json::to_value(params)?; + let mut req_json = serde_json::Map::new(); + req_json.insert("id".to_owned(), serde_json::json!(id)); + req_json.insert("jsonrpc".to_owned(), serde_json::json!("2.0")); + req_json.insert("method".to_owned(), method); + req_json.insert("params".to_owned(), params); + Ok(req_json) + }; + + self.client.post(params_fn) + + } $( $(#[$attr])* pub fn $method(&$selff $(, $arg_name: $arg_ty)*) -> impl std::future::Future> { diff --git a/src/test_util.rs b/src/test_util.rs index ec8da58b..339d9037 100644 --- a/src/test_util.rs +++ b/src/test_util.rs @@ -391,7 +391,8 @@ impl Context { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TransactionDependencyProvider for Context { // For verify certain cell belong to certain transaction async fn get_transaction_async( @@ -451,7 +452,8 @@ impl TransactionDependencyProvider for Context { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl HeaderDepResolver for Context { async fn resolve_by_tx_async( &self, @@ -520,7 +522,8 @@ impl CellDepResolver for Context { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl CellCollector for LiveCellsContext { async fn collect_live_cells_async( &mut self, diff --git a/src/tests/cycle.rs b/src/tests/cycle.rs index dc047aab..355a3e94 100644 --- a/src/tests/cycle.rs +++ b/src/tests/cycle.rs @@ -27,7 +27,8 @@ const CYCLE_BIN: &[u8] = include_bytes!("../test-data/cycle"); pub struct CycleUnlocker { loops: u64, } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl ScriptUnlocker for CycleUnlocker { fn match_args(&self, _args: &[u8]) -> bool { true diff --git a/src/traits/default_impls.rs b/src/traits/default_impls.rs index 74543c95..be8ceddc 100644 --- a/src/traits/default_impls.rs +++ b/src/traits/default_impls.rs @@ -212,7 +212,8 @@ impl DefaultHeaderDepResolver { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl HeaderDepResolver for DefaultHeaderDepResolver { async fn resolve_by_tx_async( &self, @@ -277,7 +278,7 @@ impl DefaultCellCollector { pub fn set_acceptable_indexer_leftbehind(&mut self, value: u64) { self.acceptable_indexer_leftbehind = value; } - + #[cfg(not(target_arch = "wasm32"))] /// wrapper check_ckb_chain_async future pub fn check_ckb_chain(&mut self) -> Result<(), CellCollectorError> { crate::rpc::block_on(self.check_ckb_chain_async()) @@ -301,7 +302,10 @@ impl DefaultCellCollector { if tip_number.value() > block_number.value() + self.acceptable_indexer_leftbehind { + #[cfg(not(target_arch = "wasm32"))] tokio::time::sleep(Duration::from_millis(50)).await; + #[cfg(target_arch = "wasm32")] + tokio_with_wasm::time::sleep(Duration::from_millis(50)).await; } else { return Ok(()); } @@ -319,7 +323,8 @@ impl DefaultCellCollector { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl CellCollector for DefaultCellCollector { async fn collect_live_cells_async( &mut self, @@ -461,7 +466,7 @@ impl DefaultTransactionDependencyProvider { inner: Arc::new(Mutex::new(inner)), } } - + #[cfg(not(target_arch = "wasm32"))] pub fn apply_tx( &mut self, tx: Transaction, @@ -479,7 +484,7 @@ impl DefaultTransactionDependencyProvider { inner.offchain_cache.apply_tx(tx, tip_block_number)?; Ok(()) } - + #[cfg(not(target_arch = "wasm32"))] pub fn get_cell_with_data( &self, out_point: &OutPoint, @@ -517,7 +522,8 @@ impl DefaultTransactionDependencyProvider { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TransactionDependencyProvider for DefaultTransactionDependencyProvider { async fn get_transaction_async( &self, @@ -528,7 +534,7 @@ impl TransactionDependencyProvider for DefaultTransactionDependencyProvider { return Ok(tx.clone()); } let ret: Result = - inner.offchain_cache.get_transaction(tx_hash); + inner.offchain_cache.get_transaction_async(tx_hash).await; if ret.is_ok() { return ret; } @@ -559,7 +565,7 @@ impl TransactionDependencyProvider for DefaultTransactionDependencyProvider { ) -> Result { { let inner = self.inner.lock().await; - let ret = inner.offchain_cache.get_cell(out_point); + let ret = inner.offchain_cache.get_cell_async(out_point).await; if ret.is_ok() { return ret; } @@ -574,7 +580,7 @@ impl TransactionDependencyProvider for DefaultTransactionDependencyProvider { ) -> Result { { let inner = self.inner.lock().await; - let ret = inner.offchain_cache.get_cell_data(out_point); + let ret = inner.offchain_cache.get_cell_data_async(out_point).await; if ret.is_ok() { return ret; } diff --git a/src/traits/dummy_impls.rs b/src/traits/dummy_impls.rs index 5dd6d01e..74d94bc5 100644 --- a/src/traits/dummy_impls.rs +++ b/src/traits/dummy_impls.rs @@ -14,7 +14,8 @@ use anyhow::anyhow; #[derive(Clone, Default)] pub struct DummyCellCollector; -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl CellCollector for DummyCellCollector { async fn collect_live_cells_async( &mut self, @@ -48,7 +49,8 @@ impl CellCollector for DummyCellCollector { #[derive(Default)] pub struct DummyHeaderDepResolver; -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl HeaderDepResolver for DummyHeaderDepResolver { async fn resolve_by_tx_async( &self, @@ -68,7 +70,8 @@ impl HeaderDepResolver for DummyHeaderDepResolver { #[derive(Default)] pub struct DummyTransactionDependencyProvider; -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TransactionDependencyProvider for DummyTransactionDependencyProvider { // For verify certain cell belong to certain transaction async fn get_transaction_async( diff --git a/src/traits/light_client_impls.rs b/src/traits/light_client_impls.rs index d96e1329..e3aea8a9 100644 --- a/src/traits/light_client_impls.rs +++ b/src/traits/light_client_impls.rs @@ -42,7 +42,8 @@ impl LightClientHeaderDepResolver { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl HeaderDepResolver for LightClientHeaderDepResolver { async fn resolve_by_tx_async( &self, @@ -119,7 +120,8 @@ impl LightClientTransactionDependencyProvider { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TransactionDependencyProvider for LightClientTransactionDependencyProvider { async fn get_transaction_async( &self, @@ -270,7 +272,8 @@ impl LightClientCellCollector { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl CellCollector for LightClientCellCollector { async fn collect_live_cells_async( &mut self, diff --git a/src/traits/mod.rs b/src/traits/mod.rs index 0258f510..c15fff41 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -19,18 +19,21 @@ pub use offchain_impls::{ OffchainTransactionDependencyProvider, }; +#[cfg(not(target_arch = "wasm32"))] +use ckb_hash::blake2b_256; +#[cfg(not(target_arch = "wasm32"))] +use ckb_traits::{CellDataProvider, ExtensionProvider, HeaderProvider}; +#[cfg(not(target_arch = "wasm32"))] +use ckb_types::core::{ + cell::{CellMetaBuilder, CellProvider, CellStatus, HeaderChecker}, + error::OutPointError, +}; use dyn_clone::DynClone; use thiserror::Error; -use ckb_hash::blake2b_256; -use ckb_traits::{CellDataProvider, ExtensionProvider, HeaderProvider}; use ckb_types::{ bytes::Bytes, - core::{ - cell::{CellMetaBuilder, CellProvider, CellStatus, HeaderChecker}, - error::OutPointError, - HeaderView, TransactionView, - }, + core::{HeaderView, TransactionView}, packed::{Byte32, CellDep, CellOutput, OutPoint, Script, Transaction}, prelude::*, }; @@ -90,7 +93,8 @@ pub enum TransactionDependencyError { /// * inputs /// * cell_deps /// * header_deps -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] pub trait TransactionDependencyProvider: Sync + Send { async fn get_transaction_async( &self, @@ -118,26 +122,31 @@ pub trait TransactionDependencyProvider: Sync + Send { block_hash: &Byte32, ) -> Result, TransactionDependencyError>; /// For verify certain cell belong to certain transaction + #[cfg(not(target_arch = "wasm32"))] fn get_transaction( &self, tx_hash: &Byte32, ) -> Result { crate::rpc::block_on(self.get_transaction_async(tx_hash)) } + #[cfg(not(target_arch = "wasm32"))] /// For get the output information of inputs or cell_deps, those cell should be live cell fn get_cell(&self, out_point: &OutPoint) -> Result { crate::rpc::block_on(self.get_cell_async(out_point)) } + #[cfg(not(target_arch = "wasm32"))] /// For get the output data information of inputs or cell_deps fn get_cell_data(&self, out_point: &OutPoint) -> Result { crate::rpc::block_on(self.get_cell_data_async(out_point)) } + #[cfg(not(target_arch = "wasm32"))] /// For get the header information of header_deps fn get_header(&self, block_hash: &Byte32) -> Result { crate::rpc::block_on(self.get_header_async(block_hash)) } /// For get_block_extension + #[cfg(not(target_arch = "wasm32"))] fn get_block_extension( &self, block_hash: &Byte32, @@ -146,6 +155,7 @@ pub trait TransactionDependencyProvider: Sync + Send { } } +#[cfg(not(target_arch = "wasm32"))] // Implement CellDataProvider trait is currently for `DaoCalculator` impl CellDataProvider for &dyn TransactionDependencyProvider { fn get_cell_data(&self, out_point: &OutPoint) -> Option { @@ -157,13 +167,14 @@ impl CellDataProvider for &dyn TransactionDependencyProvider { .map(|data| blake2b_256(data.as_ref()).pack()) } } - +#[cfg(not(target_arch = "wasm32"))] // Implement CellDataProvider trait is currently for `DaoCalculator` impl HeaderProvider for &dyn TransactionDependencyProvider { fn get_header(&self, hash: &Byte32) -> Option { TransactionDependencyProvider::get_header(*self, hash).ok() } } +#[cfg(not(target_arch = "wasm32"))] impl HeaderChecker for &dyn TransactionDependencyProvider { fn check_valid(&self, block_hash: &Byte32) -> Result<(), OutPointError> { TransactionDependencyProvider::get_header(*self, block_hash) @@ -171,6 +182,7 @@ impl HeaderChecker for &dyn TransactionDependencyProvider { .map_err(|_| OutPointError::InvalidHeader(block_hash.clone())) } } +#[cfg(not(target_arch = "wasm32"))] impl CellProvider for &dyn TransactionDependencyProvider { fn cell(&self, out_point: &OutPoint, _eager_load: bool) -> CellStatus { match self.get_transaction(&out_point.tx_hash()) { @@ -194,7 +206,7 @@ impl CellProvider for &dyn TransactionDependencyProvider { } } } - +#[cfg(not(target_arch = "wasm32"))] impl ExtensionProvider for &dyn TransactionDependencyProvider { fn get_block_extension(&self, hash: &Byte32) -> Option { match TransactionDependencyProvider::get_block_extension(*self, hash).ok() { @@ -421,7 +433,8 @@ impl CellQueryOptions { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] pub trait CellCollector: DynClone + Send + Sync { /// Collect live cells by query options, if `apply_changes` is true will /// mark all collected cells as dead cells. @@ -432,6 +445,7 @@ pub trait CellCollector: DynClone + Send + Sync { ) -> Result<(Vec, u64), CellCollectorError>; /// Collect live cells by query options, if `apply_changes` is true will /// mark all collected cells as dead cells. + #[cfg(not(target_arch = "wasm32"))] fn collect_live_cells( &mut self, query: &CellQueryOptions, @@ -464,7 +478,8 @@ pub trait CellDepResolver: Send + Sync { fn resolve(&self, script: &Script) -> Option; } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] pub trait HeaderDepResolver: Send + Sync { /// Resolve header dep by trancation hash async fn resolve_by_tx_async( @@ -477,12 +492,12 @@ pub trait HeaderDepResolver: Send + Sync { &self, number: u64, ) -> Result, anyhow::Error>; - + #[cfg(not(target_arch = "wasm32"))] /// Resolve header dep by trancation hash fn resolve_by_tx(&self, tx_hash: &Byte32) -> Result, anyhow::Error> { crate::rpc::block_on(self.resolve_by_tx_async(tx_hash)) } - + #[cfg(not(target_arch = "wasm32"))] /// Resolve header dep by block number fn resolve_by_number(&self, number: u64) -> Result, anyhow::Error> { crate::rpc::block_on(self.resolve_by_number_async(number)) diff --git a/src/traits/offchain_impls.rs b/src/traits/offchain_impls.rs index 8fc551aa..1790f91e 100644 --- a/src/traits/offchain_impls.rs +++ b/src/traits/offchain_impls.rs @@ -37,7 +37,8 @@ pub struct OffchainHeaderDepResolver { pub by_number: HashMap, } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl HeaderDepResolver for OffchainHeaderDepResolver { async fn resolve_by_tx_async( &self, @@ -228,7 +229,8 @@ impl OffchainTransactionDependencyProvider { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TransactionDependencyProvider for OffchainTransactionDependencyProvider { // For verify certain cell belong to certain transaction async fn get_transaction_async( diff --git a/src/transaction/builder/mod.rs b/src/transaction/builder/mod.rs index f6edd0f6..0c9b24a2 100644 --- a/src/transaction/builder/mod.rs +++ b/src/transaction/builder/mod.rs @@ -21,11 +21,19 @@ pub use fee_calculator::FeeCalculator; pub use simple::SimpleTransactionBuilder; /// CKB transaction builder trait. +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] pub trait CkbTransactionBuilder { + #[cfg(not(target_arch = "wasm32"))] fn build( self, contexts: &HandlerContexts, ) -> Result; + // #[cfg(target_arch = "wasm32")] + async fn build_async( + self, + contexts: &HandlerContexts, + ) -> Result; } /// Change output builder trait. @@ -71,7 +79,7 @@ impl<'a> DefaultChangeBuilder<'a> { } } -impl<'a> ChangeBuilder for DefaultChangeBuilder<'a> { +impl ChangeBuilder for DefaultChangeBuilder<'_> { fn init(&self, tx: &mut TransactionBuilder) { let (change_output, change_output_data) = self.get_change(); tx.output(change_output); @@ -134,6 +142,7 @@ impl<'a> ChangeBuilder for DefaultChangeBuilder<'a> { } /// a helper fn to build a transaction with common logic +#[cfg(not(target_arch = "wasm32"))] fn inner_build< CB: ChangeBuilder, I: Iterator>, @@ -209,3 +218,81 @@ fn inner_build< Err(BalanceTxCapacityError::CapacityNotEnough("can not find enough inputs".to_string()).into()) } +// #[cfg(target_arch = "wasm32")] +async fn inner_build_async< + CB: ChangeBuilder, + I: async_iterator::Iterator>, +>( + mut tx: TransactionBuilder, + mut change_builder: CB, + mut input_iter: I, + configuration: &TransactionBuilderConfiguration, + contexts: &HandlerContexts, +) -> Result { + let mut lock_groups: HashMap = HashMap::default(); + let mut type_groups: HashMap = HashMap::default(); + + // setup outputs' type script group + for (output_idx, output) in tx.get_outputs().clone().iter().enumerate() { + if let Some(type_script) = &output.type_().to_opt() { + type_groups + .entry(type_script.calc_script_hash()) + .or_insert_with(|| ScriptGroup::from_type_script(type_script)) + .output_indices + .push(output_idx); + } + } + + // setup change output and data + change_builder.init(&mut tx); + + // collect inputs + let mut input_index: usize = 0; + while let Some(input) = input_iter.next().await { + let input = input?; + tx.input(input.cell_input()); + tx.witness(packed::Bytes::default()); + + let previous_output = input.previous_output(); + let lock_script = previous_output.lock(); + lock_groups + .entry(lock_script.calc_script_hash()) + .or_insert_with(|| ScriptGroup::from_lock_script(&lock_script)) + .input_indices + .push(input_index); + + if let Some(type_script) = previous_output.type_().to_opt() { + type_groups + .entry(type_script.calc_script_hash()) + .or_insert_with(|| ScriptGroup::from_type_script(&type_script)) + .input_indices + .push(input_index); + } + + // check if we have enough inputs + if change_builder.check_balance(input, &mut tx) { + // handle script groups + let mut script_groups: Vec = lock_groups + .into_values() + .chain(type_groups.into_values()) + .collect(); + + for script_group in script_groups.iter_mut() { + for handler in configuration.get_script_handlers() { + for context in &contexts.contexts { + if handler.build_transaction(&mut tx, script_group, context.as_ref())? { + break; + } + } + } + } + + let tx_view = change_builder.finalize(tx); + + return Ok(TransactionWithScriptGroups::new(tx_view, script_groups)); + } + input_index += 1; + } + + Err(BalanceTxCapacityError::CapacityNotEnough("can not find enough inputs".to_string()).into()) +} diff --git a/src/transaction/builder/simple.rs b/src/transaction/builder/simple.rs index 4e6756f2..bc8457e3 100644 --- a/src/transaction/builder/simple.rs +++ b/src/transaction/builder/simple.rs @@ -1,3 +1,5 @@ +#[cfg(not(target_arch = "wasm32"))] +use super::inner_build; use crate::{ core::TransactionBuilder, transaction::{ @@ -12,7 +14,9 @@ use ckb_types::{ prelude::{Builder, Entity, Pack}, }; -use super::{inner_build, CkbTransactionBuilder, DefaultChangeBuilder}; +// #[cfg(target_arch = "wasm32")] +use super::inner_build_async; +use super::{CkbTransactionBuilder, DefaultChangeBuilder}; /// A simple transaction builder implementation, it will build a transaction with enough capacity to pay for the outputs and the fee. pub struct SimpleTransactionBuilder { @@ -61,7 +65,10 @@ impl SimpleTransactionBuilder { } } +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl CkbTransactionBuilder for SimpleTransactionBuilder { + #[cfg(not(target_arch = "wasm32"))] fn build( self, contexts: &HandlerContexts, @@ -81,4 +88,24 @@ impl CkbTransactionBuilder for SimpleTransactionBuilder { inner_build(tx, change_builder, input_iter, &configuration, contexts) } + // #[cfg(target_arch = "wasm32")] + async fn build_async( + self, + contexts: &HandlerContexts, + ) -> Result { + let Self { + change_lock, + configuration, + input_iter, + tx, + } = self; + + let change_builder = DefaultChangeBuilder { + configuration: &configuration, + change_lock, + inputs: Vec::new(), + }; + + inner_build_async(tx, change_builder, input_iter, &configuration, contexts).await + } } diff --git a/src/transaction/builder/sudt.rs b/src/transaction/builder/sudt.rs index 5b9097f5..34251437 100644 --- a/src/transaction/builder/sudt.rs +++ b/src/transaction/builder/sudt.rs @@ -10,6 +10,13 @@ use crate::{ }; use anyhow::anyhow; +#[cfg(not(target_arch = "wasm32"))] +use super::inner_build; +// #[cfg(target_arch = "wasm32")] +use super::inner_build_async; + +use super::{CkbTransactionBuilder, DefaultChangeBuilder}; + use ckb_types::{ core::{Capacity, ScriptHashType}, h256, @@ -17,8 +24,6 @@ use ckb_types::{ prelude::*, }; -use super::{inner_build, CkbTransactionBuilder, DefaultChangeBuilder}; - /// A sUDT transaction builder implementation pub struct SudtTransactionBuilder { /// The change lock script, the default change lock script is the last lock script of the input iterator @@ -141,7 +146,10 @@ fn parse_u128(data: &[u8]) -> Result { Ok(u128::from_le_bytes(data_bytes.try_into().unwrap())) } +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl CkbTransactionBuilder for SudtTransactionBuilder { + #[cfg(not(target_arch = "wasm32"))] fn build( mut self, contexts: &HandlerContexts, @@ -202,6 +210,81 @@ impl CkbTransactionBuilder for SudtTransactionBuilder { } } + Err( + BalanceTxCapacityError::CapacityNotEnough("can not find enough inputs".to_string()) + .into(), + ) + } + } + async fn build_async( + mut self, + contexts: &HandlerContexts, + ) -> Result { + if !self.owner_mode { + // Add change output for sudt with zero amount as placeholder + self.add_output(self.change_lock.clone(), 0); + } + + let Self { + change_lock, + configuration, + mut input_iter, + sudt_owner_lock_script, + sudt_type_script, + owner_mode, + mut tx, + .. + } = self; + + let change_builder = DefaultChangeBuilder { + configuration: &configuration, + change_lock, + inputs: Vec::new(), + }; + + if owner_mode { + inner_build_async(tx, change_builder, input_iter, &configuration, contexts).await + } else { + let sudt_type_script = build_sudt_type_script( + configuration.network_info(), + &sudt_owner_lock_script, + sudt_type_script, + ); + let mut sudt_input_iter = input_iter.clone(); + sudt_input_iter.set_type_script(Some(sudt_type_script)); + + let outputs_sudt_amount: u128 = tx + .outputs_data + .iter() + .map(|data| parse_u128(data.raw_data().as_ref())) + .collect::, TxBuilderError>>() + .map(|u128_vec| u128_vec.iter().sum())?; + + let mut inputs_sudt_amount = 0; + + while let Some(input) = + ::next(&mut sudt_input_iter).await + { + let input = input?; + let input_amount = parse_u128(input.live_cell.output_data.as_ref())?; + inputs_sudt_amount += input_amount; + input_iter.push_input(input); + if inputs_sudt_amount >= outputs_sudt_amount { + let change_output_data: Bytes = (inputs_sudt_amount - outputs_sudt_amount) + .to_le_bytes() + .pack(); + tx.set_output_data(tx.outputs_data.len() - 1, change_output_data); + return inner_build_async( + tx, + change_builder, + input_iter, + &configuration, + contexts, + ) + .await; + } + } + Err( BalanceTxCapacityError::CapacityNotEnough("can not find enough inputs".to_string()) .into(), diff --git a/src/transaction/handler/mod.rs b/src/transaction/handler/mod.rs index fa4799c8..ba6829e4 100644 --- a/src/transaction/handler/mod.rs +++ b/src/transaction/handler/mod.rs @@ -14,7 +14,7 @@ pub mod sighash; pub mod sudt; pub mod typeid; -pub trait ScriptHandler { +pub trait ScriptHandler: Send + Sync { /// Try to build transaction with the given script_group and context. /// /// Return true if script_group and context are matched, otherwise return false. @@ -38,7 +38,7 @@ impl Type2Any for T { } } -pub trait HandlerContext: Type2Any {} +pub trait HandlerContext: Type2Any + Send + Sync {} pub struct HandlerContexts { pub contexts: Vec>, diff --git a/src/transaction/input/mod.rs b/src/transaction/input/mod.rs index a79f0eb6..2f26228c 100644 --- a/src/transaction/input/mod.rs +++ b/src/transaction/input/mod.rs @@ -1,4 +1,5 @@ pub mod transaction_input; + use ckb_types::packed::Script; pub use transaction_input::TransactionInput; @@ -72,6 +73,7 @@ impl InputIterator { self.buffer_inputs.push(input); } + #[cfg(not(target_arch = "wasm32"))] fn collect_live_cells(&mut self) -> Result<(), CellCollectorError> { loop { if self.lock_scripts.is_empty() { @@ -103,8 +105,42 @@ impl InputIterator { } Ok(()) } + async fn collect_live_cells_async(&mut self) -> Result<(), CellCollectorError> { + loop { + if self.lock_scripts.is_empty() { + return Ok(()); + } + + if let Some(lock_script) = self.lock_scripts.last() { + let mut query = CellQueryOptions::new_lock(lock_script.clone()); + query.script_search_mode = Some(SearchMode::Exact); + if let Some(type_script) = &self.type_script { + query.secondary_script = Some(type_script.clone()); + } else { + query.secondary_script_len_range = Some(ValueRangeOption::new_exact(0)); + query.data_len_range = Some(ValueRangeOption::new_exact(0)); + }; + let (live_cells, _capacity) = self + .cell_collector + .collect_live_cells_async(&query, true) + .await?; + if live_cells.is_empty() { + self.lock_scripts.pop(); + } else { + self.buffer_inputs = live_cells + .into_iter() + .rev() // reverse the iter, so that the first cell will be consumed while pop + .map(|live_cell| TransactionInput::new(live_cell, 0)) + .collect(); + break; + } + } + } + Ok(()) + } } +#[cfg(not(target_arch = "wasm32"))] impl Iterator for InputIterator { type Item = Result; @@ -121,3 +157,19 @@ impl Iterator for InputIterator { } } } +impl async_iterator::Iterator for InputIterator { + type Item = Result; + + async fn next(&mut self) -> Option { + if let Some(input) = self.buffer_inputs.pop() { + return Some(Ok(input)); + } + + let status = self.collect_live_cells_async().await; + if let Err(status) = status { + Some(Err(status)) + } else { + self.buffer_inputs.pop().map(Ok) + } + } +} diff --git a/src/transaction/signer/mod.rs b/src/transaction/signer/mod.rs index e4279caf..bc65244a 100644 --- a/src/transaction/signer/mod.rs +++ b/src/transaction/signer/mod.rs @@ -13,14 +13,24 @@ use super::handler::Type2Any; pub mod multisig; pub mod sighash; +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +// #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] pub trait CKBScriptSigner { fn match_context(&self, context: &dyn SignContext) -> bool; + #[cfg(not(target_arch = "wasm32"))] fn sign_transaction( &self, tx_view: &core::TransactionView, script_group: &ScriptGroup, context: &dyn SignContext, ) -> Result; + #[cfg(target_arch = "wasm32")] + async fn sign_transaction_async( + &self, + tx_view: &core::TransactionView, + script_group: &ScriptGroup, + context: &dyn SignContext, + ) -> Result; } pub trait SignContext: Type2Any {} @@ -96,6 +106,7 @@ impl TransactionSigner { Self { unlockers } } + #[cfg(not(target_arch = "wasm32"))] pub fn sign_transaction( &self, transaction: &mut TransactionWithScriptGroups, @@ -122,4 +133,33 @@ impl TransactionSigner { transaction.set_tx_view(tx); Ok(signed_groups_indices) } + #[cfg(target_arch = "wasm32")] + pub async fn sign_transaction_async( + &self, + transaction: &mut TransactionWithScriptGroups, + contexts: &SignContexts, + ) -> Result, UnlockError> { + let mut signed_groups_indices = vec![]; + if contexts.is_empty() { + return Ok(signed_groups_indices); + } + let mut tx = transaction.get_tx_view().clone(); + for (idx, script_group) in transaction.get_script_groups().iter().enumerate() { + let script_id = ScriptId::from(&script_group.script); + if let Some(unlocker) = self.unlockers.get(&script_id) { + for context in &contexts.contexts { + if !unlocker.match_context(context.as_ref()) { + continue; + } + tx = unlocker + .sign_transaction_async(&tx, script_group, context.as_ref()) + .await?; + signed_groups_indices.push(idx); + break; + } + } + } + transaction.set_tx_view(tx); + Ok(signed_groups_indices) + } } diff --git a/src/transaction/signer/multisig.rs b/src/transaction/signer/multisig.rs index 282ae70f..15300a00 100644 --- a/src/transaction/signer/multisig.rs +++ b/src/transaction/signer/multisig.rs @@ -34,12 +34,15 @@ impl Secp256k1Blake160MultisigAllSignerContext { impl SignContext for Secp256k1Blake160MultisigAllSignerContext {} +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +// #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl CKBScriptSigner for Secp256k1Blake160MultisigAllSigner { fn match_context(&self, context: &dyn SignContext) -> bool { context .as_any() .is::() } + #[cfg(not(target_arch = "wasm32"))] fn sign_transaction( &self, transaction: &core::TransactionView, @@ -61,4 +64,29 @@ impl CKBScriptSigner for Secp256k1Blake160MultisigAllSigner { Err(UnlockError::SignContextTypeIncorrect) } } + #[cfg(target_arch = "wasm32")] + async fn sign_transaction_async( + &self, + transaction: &core::TransactionView, + script_group: &crate::ScriptGroup, + context: &dyn super::SignContext, + ) -> Result { + let unlocker = if let Some(args) = context + .as_any() + .downcast_ref::() + { + args.build_multisig_unlocker() + } else { + return Err(UnlockError::SignContextTypeIncorrect); + }; + + let tx = unlocker + .unlock_async( + transaction, + script_group, + &DummyTransactionDependencyProvider {}, + ) + .await?; + Ok(tx) + } } diff --git a/src/transaction/signer/sighash.rs b/src/transaction/signer/sighash.rs index 38d4e095..2a03ea51 100644 --- a/src/transaction/signer/sighash.rs +++ b/src/transaction/signer/sighash.rs @@ -21,12 +21,40 @@ impl Secp256k1Blake160SighashAllSignerContext { impl SignContext for Secp256k1Blake160SighashAllSignerContext {} +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +// #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl CKBScriptSigner for Secp256k1Blake160SighashAllSigner { fn match_context(&self, context: &dyn SignContext) -> bool { context .as_any() .is::() } + #[cfg(target_arch = "wasm32")] + async fn sign_transaction_async( + &self, + tx_view: &core::TransactionView, + script_group: &crate::ScriptGroup, + context: &dyn super::SignContext, + ) -> Result { + if let Some(args) = context + .as_any() + .downcast_ref::() + { + let signer = SecpCkbRawKeySigner::new_with_secret_keys(args.keys.clone()); + let unlocker = SecpSighashUnlocker::from(Box::new(signer) as Box<_>); + let tx = unlocker + .unlock_async( + tx_view, + script_group, + &DummyTransactionDependencyProvider {}, + ) + .await?; + Ok(tx) + } else { + Err(UnlockError::SignContextTypeIncorrect) + } + } + #[cfg(not(target_arch = "wasm32"))] fn sign_transaction( &self, transaction: &core::TransactionView, diff --git a/src/tx_builder/acp.rs b/src/tx_builder/acp.rs index 29f414cd..6b08c79a 100644 --- a/src/tx_builder/acp.rs +++ b/src/tx_builder/acp.rs @@ -37,7 +37,8 @@ impl AcpTransferBuilder { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TxBuilder for AcpTransferBuilder { async fn build_base_async( &self, diff --git a/src/tx_builder/cheque.rs b/src/tx_builder/cheque.rs index 745a57a7..525df320 100644 --- a/src/tx_builder/cheque.rs +++ b/src/tx_builder/cheque.rs @@ -43,7 +43,8 @@ impl ChequeClaimBuilder { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TxBuilder for ChequeClaimBuilder { async fn build_base_async( &self, @@ -210,7 +211,8 @@ impl ChequeWithdrawBuilder { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TxBuilder for ChequeWithdrawBuilder { async fn build_base_async( &self, diff --git a/src/tx_builder/dao.rs b/src/tx_builder/dao.rs index a5e8ce91..bc1a8280 100644 --- a/src/tx_builder/dao.rs +++ b/src/tx_builder/dao.rs @@ -44,7 +44,8 @@ impl DaoDepositBuilder { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TxBuilder for DaoDepositBuilder { async fn build_base_async( &self, @@ -121,7 +122,8 @@ impl From> for DaoPrepareBuilder { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TxBuilder for DaoPrepareBuilder { async fn build_base_async( &self, @@ -242,7 +244,8 @@ impl DaoWithdrawBuilder { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TxBuilder for DaoWithdrawBuilder { async fn build_base_async( &self, diff --git a/src/tx_builder/mod.rs b/src/tx_builder/mod.rs index e1f0da42..c6889f6d 100644 --- a/src/tx_builder/mod.rs +++ b/src/tx_builder/mod.rs @@ -6,24 +6,26 @@ pub mod transfer; pub mod udt; use std::collections::{HashMap, HashSet}; +#[cfg(not(target_arch = "wasm32"))] use std::sync::Arc; use anyhow::anyhow; +#[cfg(not(target_arch = "wasm32"))] use ckb_chain_spec::consensus::Consensus; +#[cfg(not(target_arch = "wasm32"))] use ckb_script::{TransactionScriptsVerifier, TxVerifyEnv}; +#[cfg(not(target_arch = "wasm32"))] use ckb_traits::{CellDataProvider, ExtensionProvider, HeaderProvider}; -use thiserror::Error; - +#[cfg(not(target_arch = "wasm32"))] use ckb_types::core::cell::{CellProvider, HeaderChecker}; -use ckb_types::core::HeaderView; +#[cfg(not(target_arch = "wasm32"))] +use ckb_types::core::{cell::resolve_transaction, HeaderView}; use ckb_types::{ - core::{ - cell::resolve_transaction, error::OutPointError, Capacity, CapacityError, FeeRate, - TransactionView, - }, + core::{error::OutPointError, Capacity, CapacityError, FeeRate, TransactionView}, packed::{Byte32, CellInput, CellOutput, Script, WitnessArgs}, prelude::*, }; +use thiserror::Error; use crate::types::ScriptGroup; use crate::types::{HumanCapacity, ScriptId}; @@ -81,7 +83,8 @@ pub enum TxBuilderError { } /// Transaction Builder interface -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] pub trait TxBuilder: Send + Sync { /// Build base transaction async fn build_base_async( @@ -91,7 +94,7 @@ pub trait TxBuilder: Send + Sync { header_dep_resolver: &dyn HeaderDepResolver, tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result; - + #[cfg(not(target_arch = "wasm32"))] /// Build base transaction fn build_base( &self, @@ -130,17 +133,18 @@ pub trait TxBuilder: Send + Sync { ) .await?; let (tx_filled_witnesses, _) = - fill_placeholder_witnesses(base_tx, tx_dep_provider, unlockers)?; - Ok(balance_tx_capacity( + fill_placeholder_witnesses_async(base_tx, tx_dep_provider, unlockers).await?; + Ok(balance_tx_capacity_async( &tx_filled_witnesses, balancer, cell_collector, tx_dep_provider, cell_dep_resolver, header_dep_resolver, - )?) + ) + .await?) } - + #[cfg(not(target_arch = "wasm32"))] fn build_balanced( &self, cell_collector: &mut dyn CellCollector, @@ -168,6 +172,8 @@ pub trait TxBuilder: Send + Sync { /// Return value: /// * The built transaction /// * The script groups that not unlocked by given `unlockers` + /// + #[cfg(not(target_arch = "wasm32"))] fn build_unlocked( &self, cell_collector: &mut dyn CellCollector, @@ -188,6 +194,29 @@ pub trait TxBuilder: Send + Sync { Ok(unlock_tx(balanced_tx, tx_dep_provider, unlockers)?) } + #[cfg(target_arch = "wasm32")] + async fn build_unlocked_async( + &self, + cell_collector: &mut dyn CellCollector, + cell_dep_resolver: &dyn CellDepResolver, + header_dep_resolver: &dyn HeaderDepResolver, + tx_dep_provider: &dyn TransactionDependencyProvider, + balancer: &CapacityBalancer, + unlockers: &HashMap>, + ) -> Result<(TransactionView, Vec), TxBuilderError> { + let balanced_tx = self + .build_balanced_async( + cell_collector, + cell_dep_resolver, + header_dep_resolver, + tx_dep_provider, + balancer, + unlockers, + ) + .await?; + Ok(unlock_tx_async(balanced_tx, tx_dep_provider, unlockers).await?) + } + /// Build unlocked transaction that ready to send or for further unlock, it's similar to `build_unlocked`, /// except it will try to check the consumed cycles limitation: /// If all input unlocked, and transaction fee can not meet the required transaction fee rate because of a big estimated cycles, @@ -196,6 +225,7 @@ pub trait TxBuilder: Send + Sync { /// Return value: /// * The built transaction /// * The script groups that not unlocked by given `unlockers` + #[cfg(not(target_arch = "wasm32"))] fn build_balance_unlocked( &self, cell_collector: &mut dyn CellCollector, @@ -253,6 +283,71 @@ pub trait TxBuilder: Send + Sync { } Ok((tx, unlocked_group)) } + // #[cfg(target_arch = "wasm32")] + #[cfg(not(target_arch = "wasm32"))] + async fn build_balance_unlocked_async( + &self, + cell_collector: &mut dyn CellCollector, + cell_dep_resolver: &dyn CellDepResolver, + header_dep_resolver: &dyn HeaderDepResolver, + tx_dep_provider: &'static dyn TransactionDependencyProvider, + balancer: &CapacityBalancer, + unlockers: &HashMap>, + ) -> Result<(TransactionView, Vec), TxBuilderError> { + let base_tx = self + .build_base_async( + cell_collector, + cell_dep_resolver, + header_dep_resolver, + tx_dep_provider, + ) + .await?; + let (tx_filled_witnesses, _) = + fill_placeholder_witnesses_async(base_tx, tx_dep_provider, unlockers).await?; + let (balanced_tx, mut change_idx) = rebalance_tx_capacity_async( + &tx_filled_witnesses, + balancer, + cell_collector, + tx_dep_provider, + cell_dep_resolver, + header_dep_resolver, + 0, + None, + ) + .await?; + let (mut tx, unlocked_group) = + unlock_tx_async(balanced_tx, tx_dep_provider, unlockers).await?; + if unlocked_group.is_empty() { + let mut ready = false; + const MAX_LOOP_TIMES: u32 = 16; + let mut n = 0; + while !ready && n < MAX_LOOP_TIMES { + n += 1; + + let (new_tx, new_change_idx, ok) = balancer + .check_cycle_fee_async( + tx, + cell_collector, + tx_dep_provider, + cell_dep_resolver, + header_dep_resolver, + change_idx, + ) + .await?; + tx = new_tx; + ready = ok; + change_idx = new_change_idx; + if !ready { + let (new_tx, _) = unlock_tx_async(tx, tx_dep_provider, unlockers).await?; + tx = new_tx + } + } + if !ready && n >= MAX_LOOP_TIMES { + return Err(TxBuilderError::ExceedCycleMaxLoopTimes(n)); + } + } + Ok((tx, unlocked_group)) + } } #[derive(Debug, Eq, PartialEq, Hash, Clone)] @@ -287,6 +382,7 @@ pub enum TransactionFeeError { /// Calculate the actual transaction fee of the transaction, include dao /// withdraw capacity. #[allow(clippy::unnecessary_lazy_evaluations)] +#[cfg(not(target_arch = "wasm32"))] pub fn tx_fee( tx: TransactionView, tx_dep_provider: &dyn TransactionDependencyProvider, @@ -352,6 +448,80 @@ pub fn tx_fee( .ok_or_else(|| TransactionFeeError::CapacityOverflow(output_total - input_total)) } +/// Calculate the actual transaction fee of the transaction, include dao +/// withdraw capacity. +#[allow(clippy::unnecessary_lazy_evaluations)] +pub async fn tx_fee_async( + tx: TransactionView, + tx_dep_provider: &dyn TransactionDependencyProvider, + header_dep_resolver: &dyn HeaderDepResolver, +) -> Result { + let mut input_total: u64 = 0; + for input in tx.inputs() { + let mut is_withdraw = false; + let since: u64 = input.since().unpack(); + let cell = tx_dep_provider + .get_cell_async(&input.previous_output()) + .await?; + if since != 0 { + if let Some(type_script) = cell.type_().to_opt() { + if type_script.code_hash().as_slice() == DAO_TYPE_HASH.as_bytes() { + is_withdraw = true; + } + } + } + let capacity: u64 = if is_withdraw { + let tx_hash = input.previous_output().tx_hash(); + let prepare_header = header_dep_resolver + .resolve_by_tx_async(&tx_hash) + .await + .map_err(TransactionFeeError::HeaderDep)? + .ok_or_else(|| { + TransactionFeeError::HeaderDep(anyhow!( + "resolve prepare header by transaction hash failed: {}", + tx_hash + )) + })?; + let data = tx_dep_provider + .get_cell_data_async(&input.previous_output()) + .await?; + assert_eq!(data.len(), 8); + let deposit_number = { + let mut number_bytes = [0u8; 8]; + number_bytes.copy_from_slice(data.as_ref()); + u64::from_le_bytes(number_bytes) + }; + let deposit_header = header_dep_resolver + .resolve_by_number_async(deposit_number) + .await + .map_err(TransactionFeeError::HeaderDep)? + .ok_or_else(|| { + TransactionFeeError::HeaderDep(anyhow!( + "resolve deposit header by block number failed: {}", + deposit_number + )) + })?; + let occupied_capacity = cell + .occupied_capacity(Capacity::bytes(data.len()).unwrap()) + .unwrap(); + calculate_dao_maximum_withdraw4( + &deposit_header, + &prepare_header, + &cell, + occupied_capacity.as_u64(), + ) + } else { + cell.capacity().unpack() + }; + input_total += capacity; + } + let output_total = tx.outputs_capacity()?.as_u64(); + #[allow(clippy::unnecessary_lazy_evaluations)] + input_total + .checked_sub(output_total) + .ok_or_else(|| TransactionFeeError::CapacityOverflow(output_total - input_total)) +} + #[derive(Debug, Clone)] pub enum SinceSource { /// The vaule in the tuple is offset of the args, and the `since` is stored in `lock.args[offset..offset+8]` @@ -513,7 +683,7 @@ impl CapacityBalancer { pub fn set_max_fee(&mut self, max_fee: Option) { self.force_small_change_as_fee = max_fee; } - + #[cfg(not(target_arch = "wasm32"))] pub fn balance_tx_capacity( &mut self, tx: &TransactionView, @@ -532,7 +702,27 @@ impl CapacityBalancer { ) } + pub async fn balance_tx_capacity_async( + &mut self, + tx: &TransactionView, + cell_collector: &mut dyn CellCollector, + tx_dep_provider: &dyn TransactionDependencyProvider, + cell_dep_resolver: &dyn CellDepResolver, + header_dep_resolver: &dyn HeaderDepResolver, + ) -> Result { + balance_tx_capacity_async( + tx, + self, + cell_collector, + tx_dep_provider, + cell_dep_resolver, + header_dep_resolver, + ) + .await + } + #[allow(clippy::too_many_arguments)] + #[cfg(not(target_arch = "wasm32"))] pub fn rebalance_tx_capacity( &self, tx: &TransactionView, @@ -592,7 +782,69 @@ impl CapacityBalancer { change_index, ) } + #[allow(clippy::too_many_arguments)] + pub async fn rebalance_tx_capacity_async( + &self, + tx: &TransactionView, + cell_collector: &mut dyn CellCollector, + tx_dep_provider: &dyn TransactionDependencyProvider, + cell_dep_resolver: &dyn CellDepResolver, + header_dep_resolver: &dyn HeaderDepResolver, + accepted_min_fee: u64, + change_index: Option, + ) -> Result<(TransactionView, Option), BalanceTxCapacityError> { + if let Some(idx) = change_index { + let output = tx + .outputs() + .get(idx) + .ok_or(BalanceTxCapacityError::ChangeIndexNotFound(idx))?; + let base_change_occupied_capacity = output + .occupied_capacity(Capacity::zero()) + .expect("init change occupied capacity") + .as_u64(); + let output_header_extra = 4 + 4 + 4; + // NOTE: extra_min_fee +1 is for `FeeRate::fee` round + let extra_min_fee = self + .fee_rate + .fee(output.as_slice().len() as u64 + output_header_extra) + .as_u64() + + 1; + let original_fee = + tx_fee_async(tx.clone(), tx_dep_provider, header_dep_resolver).await?; + if original_fee >= accepted_min_fee { + return Err(BalanceTxCapacityError::AlreadyBalance( + original_fee, + accepted_min_fee, + )); + } + let extra_fee = accepted_min_fee - original_fee; + // The extra capacity (delta - extra_min_fee) is enough to hold the change cell. + let original_capacity: u64 = output.capacity().unpack(); + if original_capacity >= base_change_occupied_capacity + extra_min_fee + extra_fee { + let output = output + .as_builder() + .capacity((original_capacity - extra_fee).pack()) + .build(); + let mut outputs: Vec<_> = tx.outputs().into_iter().collect(); + outputs[idx] = output; + let tx = tx.as_advanced_builder().set_outputs(outputs).build(); + return Ok((tx, change_index)); + }; + } + rebalance_tx_capacity_async( + tx, + self, + cell_collector, + tx_dep_provider, + cell_dep_resolver, + header_dep_resolver, + accepted_min_fee, + change_index, + ) + .await + } + #[cfg(not(target_arch = "wasm32"))] pub fn check_cycle_fee( &self, tx: TransactionView, @@ -627,19 +879,58 @@ impl CapacityBalancer { )?; Ok((tx, idx, false)) } + #[cfg(not(target_arch = "wasm32"))] + pub async fn check_cycle_fee_async( + &self, + tx: TransactionView, + cell_collector: &mut dyn CellCollector, + tx_dep_provider: &'static dyn TransactionDependencyProvider, + cell_dep_resolver: &dyn CellDepResolver, + header_dep_resolver: &dyn HeaderDepResolver, + change_index: Option, + ) -> Result<(TransactionView, Option, bool), BalanceTxCapacityError> { + let cycle_resolver = CycleResolver::new(tx_dep_provider); + let cycle = cycle_resolver.estimate_cycles(&tx)?; + let cycle_size = (cycle as f64 * bytes_per_cycle()) as usize; + let serialized_size = tx.data().as_reader().serialized_size_in_block(); + if serialized_size >= cycle_size { + return Ok((tx, None, true)); + } + let fee = tx_fee_async(tx.clone(), tx_dep_provider, header_dep_resolver) + .await + .unwrap(); + let cycle_fee = self.fee_rate.fee(cycle_size as u64).as_u64(); + + if fee >= cycle_fee { + return Ok((tx, None, true)); + } + + let (tx, idx) = self + .rebalance_tx_capacity_async( + &tx, + cell_collector, + tx_dep_provider, + cell_dep_resolver, + header_dep_resolver, + cycle_fee, + change_index, + ) + .await?; + Ok((tx, idx, false)) + } } const DEFAULT_BYTES_PER_CYCLE: f64 = 0.000_170_571_4; pub const fn bytes_per_cycle() -> f64 { DEFAULT_BYTES_PER_CYCLE } - +#[cfg(not(target_arch = "wasm32"))] pub struct CycleResolver
{ tx_dep_provider: DL, tip_header: HeaderView, consensus: Arc, } - +#[cfg(not(target_arch = "wasm32"))] impl< DL: CellDataProvider + HeaderProvider @@ -687,6 +978,7 @@ impl< } /// Fill more inputs to balance the transaction capacity +#[cfg(not(target_arch = "wasm32"))] pub fn balance_tx_capacity( tx: &TransactionView, balancer: &CapacityBalancer, @@ -707,8 +999,30 @@ pub fn balance_tx_capacity( )?; Ok(tx) } +pub async fn balance_tx_capacity_async( + tx: &TransactionView, + balancer: &CapacityBalancer, + cell_collector: &mut dyn CellCollector, + tx_dep_provider: &dyn TransactionDependencyProvider, + cell_dep_resolver: &dyn CellDepResolver, + header_dep_resolver: &dyn HeaderDepResolver, +) -> Result { + let (tx, _change_idx) = rebalance_tx_capacity_async( + tx, + balancer, + cell_collector, + tx_dep_provider, + cell_dep_resolver, + header_dep_resolver, + 0, + None, + ) + .await?; + Ok(tx) +} #[allow(clippy::too_many_arguments)] +#[cfg(not(target_arch = "wasm32"))] fn rebalance_tx_capacity( tx: &TransactionView, balancer: &CapacityBalancer, @@ -1004,12 +1318,313 @@ fn rebalance_tx_capacity( } } } +#[allow(clippy::too_many_arguments)] +async fn rebalance_tx_capacity_async( + tx: &TransactionView, + balancer: &CapacityBalancer, + cell_collector: &mut dyn CellCollector, + tx_dep_provider: &dyn TransactionDependencyProvider, + cell_dep_resolver: &dyn CellDepResolver, + header_dep_resolver: &dyn HeaderDepResolver, + accepted_min_fee: u64, + change_index: Option, +) -> Result<(TransactionView, Option), BalanceTxCapacityError> { + let capacity_provider = &balancer.capacity_provider; + if capacity_provider.lock_scripts.is_empty() { + return Err(BalanceTxCapacityError::EmptyCapacityProvider); + } + let change_lock_script = balancer + .change_lock_script + .clone() + .unwrap_or_else(|| capacity_provider.lock_scripts[0].0.clone()); + let (tx, base_change_output, base_change_occupied_capacity) = if let Some(idx) = change_index { + let outputs = tx.outputs(); + let output = tx + .outputs() + .get(idx) + .ok_or(BalanceTxCapacityError::ChangeIndexNotFound(idx))?; + + // remove change output + let outputs: Vec<_> = outputs + .into_iter() + .enumerate() + .filter_map(|(i, output)| if idx == i { None } else { Some(output) }) + .collect(); + let base_change_occupied_capacity = output + .occupied_capacity(Capacity::zero()) + .expect("init change occupied capacity") + .as_u64(); + let tx = tx.data().as_advanced_builder().set_outputs(outputs).build(); + (tx, output, base_change_occupied_capacity) + } else { + let base_change_output = CellOutput::new_builder().lock(change_lock_script).build(); + let base_change_occupied_capacity = base_change_output + .occupied_capacity(Capacity::zero()) + .expect("init change occupied capacity") + .as_u64(); + ( + tx.clone(), + base_change_output, + base_change_occupied_capacity, + ) + }; + + let mut lock_scripts = Vec::new(); + // remove duplicated lock script + for (script, placeholder, since_source) in &capacity_provider.lock_scripts { + if lock_scripts.iter().all(|(target, _, _)| target != script) { + lock_scripts.push((script.clone(), placeholder.clone(), since_source.clone())); + } + } + let mut lock_script_idx = 0; + let mut cell_deps = Vec::new(); + #[allow(clippy::mutable_key_type)] + let mut resolved_scripts = HashSet::new(); + let mut inputs = Vec::new(); + let mut change_output: Option = if change_index.is_some() { + Some(base_change_output.clone()) + } else { + None + }; + let mut changed_witnesses: HashMap = HashMap::default(); + let mut witnesses = Vec::new(); + loop { + let (lock_script, placeholder_witness, since_source) = &lock_scripts[lock_script_idx]; + let base_query = { + let mut query = CellQueryOptions::new_lock(lock_script.clone()); + query.secondary_script_len_range = Some(ValueRangeOption::new_exact(0)); + query.data_len_range = Some(ValueRangeOption::new_exact(0)); + query + }; + // check if capacity provider lock script already in inputs + let mut has_provider = false; + for input in tx.inputs().into_iter().chain(inputs.clone().into_iter()) { + let cell = tx_dep_provider + .get_cell_async(&input.previous_output()) + .await?; + if cell.lock() == *lock_script { + has_provider = true; + } + } + while tx.witnesses().item_count() + witnesses.len() + < tx.inputs().item_count() + inputs.len() + { + witnesses.push(Default::default()); + } + let mut ret_change_index = None; + let new_tx = { + let mut all_witnesses = tx.witnesses().into_iter().collect::>(); + for (idx, witness_args) in &changed_witnesses { + all_witnesses[*idx] = witness_args.as_bytes().pack(); + } + all_witnesses.extend(witnesses.clone()); + let output_len = tx.outputs().len(); + let mut builder = tx + .data() + .as_advanced_builder() + .cell_deps(cell_deps.clone()) + .inputs(inputs.clone()) + .set_witnesses(all_witnesses); + if let Some(output) = change_output.clone() { + ret_change_index = Some(output_len); + builder = builder.output(output).output_data(Default::default()); + } + builder.build() + }; + let tx_size = new_tx.data().as_reader().serialized_size_in_block(); + let min_fee = accepted_min_fee.max(balancer.fee_rate.fee(tx_size as u64).as_u64()); + let mut need_more_capacity = 1; + let fee_result: Result = + tx_fee_async(new_tx.clone(), tx_dep_provider, header_dep_resolver).await; + match fee_result { + Ok(fee) if fee == min_fee => { + return Ok((new_tx, ret_change_index)); + } + Ok(fee) if fee > min_fee => { + let delta = fee - min_fee; + if let Some(output) = change_output.take() { + // If change cell already exits, just change the capacity field + let old_capacity: u64 = output.capacity().unpack(); + let new_capacity = old_capacity + .checked_add(delta) + .expect("change cell capacity add overflow"); + // next loop round must return new_tx; + change_output = Some(output.as_builder().capacity(new_capacity.pack()).build()); + need_more_capacity = 0; + } else { + // If change cell not exists, add a change cell. + + // The output extra header size is for: + // * first 4 bytes is for output data header (the length) + // * second 4 bytes if for output data offset + // * third 4 bytes is for output offset + let output_header_extra = 4 + 4 + 4; + // NOTE: extra_min_fee +1 is for `FeeRate::fee` round + let extra_min_fee = balancer + .fee_rate + .fee(base_change_output.as_slice().len() as u64 + output_header_extra) + .as_u64() + + 1; + // The extra capacity (delta - extra_min_fee) is enough to hold the change cell. + if delta >= base_change_occupied_capacity + extra_min_fee { + // next loop round must return new_tx; + change_output = Some( + base_change_output + .clone() + .as_builder() + .capacity((delta - extra_min_fee).pack()) + .build(), + ); + need_more_capacity = 0; + } else { + // peek if there is more live cell owned by this capacity provider + let (more_cells, _more_capacity) = cell_collector + .collect_live_cells_async(&base_query, false) + .await?; + if more_cells.is_empty() { + if let Some(capacity) = balancer.force_small_change_as_fee { + if fee > capacity { + return Err( + BalanceTxCapacityError::ForceSmallChangeAsFeeFailed(fee), + ); + } else { + return Ok((new_tx, ret_change_index)); + } + } else if lock_script_idx + 1 == lock_scripts.len() { + return Err(BalanceTxCapacityError::CapacityNotEnough(format!( + "can not create change cell, left capacity={}", + HumanCapacity(delta) + ))); + } else { + lock_script_idx += 1; + continue; + } + } else { + // need more input to balance the capacity + change_output = Some( + base_change_output + .clone() + .as_builder() + .capacity(base_change_occupied_capacity.pack()) + .build(), + ); + } + } + } + } + // fee is positive and `fee < min_fee` + Ok(fee) => { + need_more_capacity = min_fee - fee; + } + Err(TransactionFeeError::CapacityOverflow(delta)) => { + need_more_capacity = delta.checked_add(min_fee).ok_or_else(|| { + BalanceTxCapacityError::CapacityNotEnough(format!( + "need more capacity, value={}", + HumanCapacity(delta) + )) + })?; + } + Err(err) => { + return Err(err.into()); + } + } + if need_more_capacity > 0 { + let query = { + let mut query = base_query.clone(); + query.min_total_capacity = need_more_capacity; + query + }; + let (more_cells, _more_capacity) = cell_collector + .collect_live_cells_async(&query, true) + .await?; + if more_cells.is_empty() { + if lock_script_idx + 1 == lock_scripts.len() { + return Err(BalanceTxCapacityError::CapacityNotEnough(format!( + "need more capacity, value={}", + HumanCapacity(need_more_capacity) + ))); + } else { + lock_script_idx += 1; + continue; + } + } + if !resolved_scripts.contains(lock_script) { + let provider_cell_dep = + cell_dep_resolver.resolve(lock_script).ok_or_else(|| { + BalanceTxCapacityError::ResolveCellDepFailed(lock_script.clone()) + })?; + if tx + .cell_deps() + .into_iter() + .all(|cell_dep| cell_dep != provider_cell_dep) + { + cell_deps.push(provider_cell_dep); + resolved_scripts.insert(lock_script); + } + } + if !has_provider { + if tx.witnesses().item_count() > tx.inputs().item_count() + inputs.len() { + let idx = tx.inputs().item_count() + inputs.len(); + let witness_data = tx.witnesses().get(idx).expect("get witness").raw_data(); + // in case witness filled before balance tx + let mut witness = if witness_data.is_empty() { + WitnessArgs::default() + } else { + WitnessArgs::from_slice(witness_data.as_ref()) + .map_err(|err| BalanceTxCapacityError::InvalidWitnessArgs(err.into()))? + }; + if let Some(data) = placeholder_witness.input_type().to_opt() { + witness = witness + .as_builder() + .input_type(Some(data.raw_data()).pack()) + .build(); + } + if let Some(data) = placeholder_witness.output_type().to_opt() { + witness = witness + .as_builder() + .output_type(Some(data.raw_data()).pack()) + .build(); + } + if let Some(data) = placeholder_witness.lock().to_opt() { + witness = witness + .as_builder() + .lock(Some(data.raw_data()).pack()) + .build(); + } + changed_witnesses.insert(idx, witness); + } else { + witnesses.push(placeholder_witness.as_bytes().pack()); + } + } + let since = match since_source { + SinceSource::LockArgs(offset) => { + let lock_arg = lock_script.args().raw_data(); + if lock_arg.len() < offset + 8 { + return Err(BalanceTxCapacityError::InvalidSinceValue( + *offset, + lock_arg.len(), + )); + } + let mut since_bytes = [0u8; 8]; + since_bytes.copy_from_slice(&lock_arg[*offset..*offset + 8]); + u64::from_le_bytes(since_bytes) + } + SinceSource::Value(since_value) => *since_value, + }; + inputs.extend( + more_cells + .into_iter() + .map(|cell| CellInput::new(cell.out_point, since)), + ); + } + } +} pub struct ScriptGroups { pub lock_groups: HashMap, pub type_groups: HashMap, } - +#[cfg(not(target_arch = "wasm32"))] pub fn gen_script_groups( tx: &TransactionView, tx_dep_provider: &dyn TransactionDependencyProvider, @@ -1044,12 +1659,49 @@ pub fn gen_script_groups( type_groups, }) } +pub async fn gen_script_groups_async( + tx: &TransactionView, + tx_dep_provider: &dyn TransactionDependencyProvider, +) -> Result { + #[allow(clippy::mutable_key_type)] + let mut lock_groups: HashMap = HashMap::default(); + #[allow(clippy::mutable_key_type)] + let mut type_groups: HashMap = HashMap::default(); + for (i, input) in tx.inputs().into_iter().enumerate() { + let output = tx_dep_provider + .get_cell_async(&input.previous_output()) + .await?; + let lock_group_entry = lock_groups + .entry(output.calc_lock_hash()) + .or_insert_with(|| ScriptGroup::from_lock_script(&output.lock())); + lock_group_entry.input_indices.push(i); + if let Some(t) = &output.type_().to_opt() { + let type_group_entry = type_groups + .entry(t.calc_script_hash()) + .or_insert_with(|| ScriptGroup::from_type_script(t)); + type_group_entry.input_indices.push(i); + } + } + for (i, output) in tx.outputs().into_iter().enumerate() { + if let Some(t) = &output.type_().to_opt() { + let type_group_entry = type_groups + .entry(t.calc_script_hash()) + .or_insert_with(|| ScriptGroup::from_type_script(t)); + type_group_entry.output_indices.push(i); + } + } + Ok(ScriptGroups { + lock_groups, + type_groups, + }) +} /// Fill placeholder lock script witnesses /// /// Return value: /// * The updated transaction /// * The script groups that not matched by given `unlockers` +#[cfg(not(target_arch = "wasm32"))] pub fn fill_placeholder_witnesses( balanced_tx: TransactionView, tx_dep_provider: &dyn TransactionDependencyProvider, @@ -1076,11 +1728,49 @@ pub fn fill_placeholder_witnesses( Ok((tx, not_matched)) } +/// Fill placeholder lock script witnesses +/// +/// Return value: +/// * The updated transaction +/// * The script groups that not matched by given `unlockers` +pub async fn fill_placeholder_witnesses_async( + balanced_tx: TransactionView, + tx_dep_provider: &dyn TransactionDependencyProvider, + unlockers: &HashMap>, +) -> Result<(TransactionView, Vec), UnlockError> { + let ScriptGroups { lock_groups, .. } = + gen_script_groups_async(&balanced_tx, tx_dep_provider).await?; + let mut tx = balanced_tx; + let mut not_matched = Vec::new(); + for script_group in lock_groups.values() { + let script_id = ScriptId::from(&script_group.script); + let script_args = script_group.script.args().raw_data(); + if let Some(unlocker) = unlockers.get(&script_id) { + if !unlocker + .is_unlocked_async(&tx, script_group, tx_dep_provider) + .await? + { + if unlocker.match_args(script_args.as_ref()) { + tx = unlocker + .fill_placeholder_witness_async(&tx, script_group, tx_dep_provider) + .await?; + } else { + not_matched.push(script_group.clone()); + } + } + } else { + not_matched.push(script_group.clone()); + } + } + Ok((tx, not_matched)) +} + /// Build unlocked transaction that ready to send or for further unlock. /// /// Return value: /// * The built transaction /// * The script groups that not unlocked by given `unlockers` +#[cfg(not(target_arch = "wasm32"))] pub fn unlock_tx( balanced_tx: TransactionView, tx_dep_provider: &dyn TransactionDependencyProvider, @@ -1107,6 +1797,43 @@ pub fn unlock_tx( Ok((tx, not_unlocked)) } +/// Build unlocked transaction that ready to send or for further unlock. +/// +/// Return value: +/// * The built transaction +/// * The script groups that not unlocked by given `unlockers` +pub async fn unlock_tx_async( + balanced_tx: TransactionView, + tx_dep_provider: &dyn TransactionDependencyProvider, + unlockers: &HashMap>, +) -> Result<(TransactionView, Vec), UnlockError> { + let ScriptGroups { lock_groups, .. } = + gen_script_groups_async(&balanced_tx, tx_dep_provider).await?; + let mut tx = balanced_tx; + let mut not_unlocked = Vec::new(); + for script_group in lock_groups.values() { + let script_id = ScriptId::from(&script_group.script); + let script_args = script_group.script.args().raw_data(); + if let Some(unlocker) = unlockers.get(&script_id) { + if unlocker + .is_unlocked_async(&tx, script_group, tx_dep_provider) + .await? + { + tx = unlocker.clear_placeholder_witness(&tx, script_group)?; + } else if unlocker.match_args(script_args.as_ref()) { + tx = unlocker + .unlock_async(&tx, script_group, tx_dep_provider) + .await?; + } else { + not_unlocked.push(script_group.clone()); + } + } else { + not_unlocked.push(script_group.clone()); + } + } + Ok((tx, not_unlocked)) +} + #[cfg(test)] mod anyhow_tests { use anyhow::anyhow; diff --git a/src/tx_builder/omni_lock.rs b/src/tx_builder/omni_lock.rs index 68ef04fc..5d88b938 100644 --- a/src/tx_builder/omni_lock.rs +++ b/src/tx_builder/omni_lock.rs @@ -35,7 +35,8 @@ impl OmniLockTransferBuilder { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TxBuilder for OmniLockTransferBuilder { async fn build_base_async( &self, diff --git a/src/tx_builder/transfer.rs b/src/tx_builder/transfer.rs index 649ace72..07472119 100644 --- a/src/tx_builder/transfer.rs +++ b/src/tx_builder/transfer.rs @@ -25,7 +25,8 @@ impl CapacityTransferBuilder { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TxBuilder for CapacityTransferBuilder { async fn build_base_async( &self, diff --git a/src/tx_builder/udt/mod.rs b/src/tx_builder/udt/mod.rs index 60479455..d38ce91a 100644 --- a/src/tx_builder/udt/mod.rs +++ b/src/tx_builder/udt/mod.rs @@ -78,7 +78,7 @@ impl UdtTargetReceiver { extra_data: None, } } - + #[cfg(not(target_arch = "wasm32"))] pub fn build( &self, type_script: &Script, @@ -201,7 +201,8 @@ pub struct UdtIssueBuilder { pub receivers: Vec, } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TxBuilder for UdtIssueBuilder { async fn build_base_async( &self, @@ -282,7 +283,8 @@ pub struct UdtTransferBuilder { pub receivers: Vec, } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl TxBuilder for UdtTransferBuilder { async fn build_base_async( &self, diff --git a/src/unlock/unlocker.rs b/src/unlock/unlocker.rs index 6caec562..dd8807d4 100644 --- a/src/unlock/unlocker.rs +++ b/src/unlock/unlocker.rs @@ -48,7 +48,8 @@ pub enum UnlockError { /// * Put extra unlock information into transaction (e.g. SMT proof in omni-lock case) /// /// See example in `examples/script_unlocker_example.rs` -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] pub trait ScriptUnlocker: Sync + Send { fn match_args(&self, args: &[u8]) -> bool; @@ -61,7 +62,7 @@ pub trait ScriptUnlocker: Sync + Send { ) -> Result { Ok(false) } - + #[cfg(not(target_arch = "wasm32"))] fn is_unlocked( &self, tx: &TransactionView, @@ -79,7 +80,7 @@ pub trait ScriptUnlocker: Sync + Send { script_group: &ScriptGroup, tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result; - + #[cfg(not(target_arch = "wasm32"))] fn unlock( &self, tx: &TransactionView, @@ -104,7 +105,7 @@ pub trait ScriptUnlocker: Sync + Send { script_group: &ScriptGroup, tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result; - + #[cfg(not(target_arch = "wasm32"))] fn fill_placeholder_witness( &self, tx: &TransactionView, @@ -179,7 +180,8 @@ impl From> for SecpSighashUnlocker { SecpSighashUnlocker::new(SecpSighashScriptSigner::new(signer)) } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl ScriptUnlocker for SecpSighashUnlocker { fn match_args(&self, args: &[u8]) -> bool { self.signer.match_args(args) @@ -217,7 +219,8 @@ impl From<(Box, MultisigConfig)> for SecpMultisigUnlocker { SecpMultisigUnlocker::new(SecpMultisigScriptSigner::new(signer, config)) } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl ScriptUnlocker for SecpMultisigUnlocker { fn match_args(&self, args: &[u8]) -> bool { (args.len() == 20 || args.len() == 28) && self.signer.match_args(args) @@ -434,7 +437,8 @@ async fn acp_is_unlocked( Ok(true) } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl ScriptUnlocker for AcpUnlocker { fn match_args(&self, args: &[u8]) -> bool { self.signer.match_args(args) @@ -514,7 +518,8 @@ impl From<(Box, ChequeAction)> for ChequeUnlocker { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl ScriptUnlocker for ChequeUnlocker { fn match_args(&self, args: &[u8]) -> bool { self.signer.match_args(args) @@ -672,7 +677,8 @@ impl From<(Box, OmniLockConfig, OmniUnlockMode)> for OmniLockUnlocke OmniLockUnlocker::new(OmniLockScriptSigner::new(signer, config, unlock_mode), cfg) } } -#[async_trait::async_trait] +#[cfg_attr(target_arch="wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl ScriptUnlocker for OmniLockUnlocker { fn match_args(&self, args: &[u8]) -> bool { self.signer.match_args(args) diff --git a/src/util.rs b/src/util.rs index 9b4e2a14..6e9f79f1 100644 --- a/src/util.rs +++ b/src/util.rs @@ -9,7 +9,10 @@ use ckb_types::{ }; use sha3::{Digest, Keccak256}; -use crate::rpc::{CkbRpcAsyncClient, CkbRpcClient}; +#[cfg(not(target_arch = "wasm32"))] +use crate::rpc::CkbRpcClient; + +use crate::rpc::CkbRpcAsyncClient; use crate::traits::LiveCell; use secp256k1::ffi::CPtr; @@ -28,7 +31,7 @@ pub fn zeroize_slice(data: &mut [u8]) { atomic::compiler_fence(atomic::Ordering::SeqCst); } } - +#[cfg(not(target_arch = "wasm32"))] pub fn get_max_mature_number(rpc_client: &CkbRpcClient) -> Result { crate::rpc::block_on(get_max_mature_number_async(&rpc_client.into())) } @@ -108,8 +111,7 @@ pub fn minimal_unlock_point( } else { prepare_point.number() - deposit_point.number() }; - let rest_epoch_cnt = - (passed_epoch_cnt + (LOCK_PERIOD_EPOCHES - 1)) / LOCK_PERIOD_EPOCHES * LOCK_PERIOD_EPOCHES; + let rest_epoch_cnt = passed_epoch_cnt.div_ceil(LOCK_PERIOD_EPOCHES) * LOCK_PERIOD_EPOCHES; EpochNumberWithFraction::new( deposit_point.number() + rest_epoch_cnt, deposit_point.index(), @@ -163,7 +165,7 @@ pub fn convert_keccak256_hash(message: &[u8]) -> H256 { H256::from_slice(r.as_slice()).expect("convert_keccak256_hash") } -#[cfg(test)] +#[cfg(all(test, feature = "test"))] mod tests { use super::*; use crate::test_util::MockRpcResult;