diff --git a/Cargo.lock b/Cargo.lock index d7dc0762..3ca4fa72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -879,7 +879,7 @@ dependencies = [ "atty", "bitflags", "clap_derive", - "indexmap", + "indexmap 1.9.3", "lazy_static", "os_str_bytes", "strsim", @@ -1067,7 +1067,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" dependencies = [ "cfg-if 1.0.0", - "hashbrown", + "hashbrown 0.12.3", "lock_api", "once_cell", "parking_lot_core 0.9.8", @@ -1224,6 +1224,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.1" @@ -1506,9 +1512,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.19" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes 1.4.0", "fnv", @@ -1516,7 +1522,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.2.2", "slab", "tokio", "tokio-util 0.7.8", @@ -1532,6 +1538,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heapsize" version = "0.4.2" @@ -1733,7 +1745,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg 1.1.0", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", ] [[package]] @@ -1974,7 +1996,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" dependencies = [ - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -3025,7 +3047,7 @@ version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ - "indexmap", + "indexmap 1.9.3", "ryu", "serde", "yaml-rust", diff --git a/README.md b/README.md index 3129ce87..16250d60 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ hash: 0x0384ebc55b7cb56e51044743e05fb83a4edb7173524339c35df4c71fcdb0854d ### Example: Get live cell (json output format) ``` -ckb-cli rpc get_live_cell --tx-hash 0x4ec75b5a8de8d180853d5046760a99285c73283a5dc528f81d6ee056f5335172 --index 0 --output-format json +ckb-cli rpc get_live_cell --tx-hash 0x4ec75b5a8de8d180853d5046760a99285c73283a5dc528f81d6ee056f5335172 --index 0 ``` **Response:** @@ -90,3 +90,49 @@ ckb-cli rpc get_live_cell --tx-hash 0x4ec75b5a8de8d180853d5046760a99285c73283a5d "status": "live" } ``` + +### Example: Indexer get cells (yaml output format) + +Prepare file searchkey.json as input parameters: + +```json +{ + "script": { + "code_hash": "0xbbad126377d45f90a8ee120da988a2d7332c78ba8fd679aab478a19d6c133494", + "hash_type": "data1", + "args": "0x" + }, + "script_type": "type", + "script_search_mode": "prefix", + "filter": { + "output_data": "0xa58618a553", + "output_data_filter_mode": "partial" + }, + "with_data": false +} +``` + +``` +ckb-cli rpc get_transactions --json-path ./searchkey.json --order asc --limit 3 +``` +Response: + +```yaml +last_cursor: 0xa0bbad126377d45f90a8ee120da988a2d7332c78ba8fd679aab478a19d6c13349402013368282f4cde04254a3a6a2027b33f7c974046a4d5cbd96bc47d7f058c18090000000000b29e04000000050000000000 +objects: + - block_number: 10375179 + io_index: 1 + io_type: output + tx_hash: 0x551ec96717c336b74bbb2e56a1cb9c73e2a9d4b56321079b454cfc1c0e6036ac + tx_index: 7 + - block_number: 11705844 + io_index: 0 + io_type: output + tx_hash: 0xd690aa336c0d05808e08a97ba2f3031b7691341df9002b305c2d27cb116e2705 + tx_index: 5 + - block_number: 11705860 + io_index: 0 + io_type: input + tx_hash: 0xa3282c23227992933eee0a07e5cbf52ca62006b98f2d113ebf579c5e59cf5a62 + tx_index: 5 + ``` \ No newline at end of file diff --git a/src/subcommands/rpc.rs b/src/subcommands/rpc.rs index 70d72b5f..d7afa993 100644 --- a/src/subcommands/rpc.rs +++ b/src/subcommands/rpc.rs @@ -16,7 +16,7 @@ use crate::utils::arg_parser::{ FromStrParser, HexParser, }; use crate::utils::rpc::{ - BannedAddr, BlockEconomicState, BlockView, EpochView, HeaderView, HttpRpcClient, + parse_order, BannedAddr, BlockEconomicState, BlockView, EpochView, HeaderView, HttpRpcClient, RawHttpRpcClient, RemoteNode, Timestamp, TransactionProof, TransactionWithStatus, }; @@ -349,7 +349,82 @@ impl<'a> RpcSubCommand<'a> { .validator(|input| HexParser.validate(input)) .about("Block assembler message (hex format)") ) - .about("[TEST ONLY] Generate an empty block") + .about("[TEST ONLY] Generate an empty block"), + // [`Indexer`] + App::new("get_indexer_tip").about("Returns the indexed tip"), + App::new("get_cells") + .arg( + Arg::with_name("json-path") + .long("json-path") + .takes_value(true) + .validator(|input| FilePathParser::new(true).validate(input)) + .required(true) + .about("Indexer search key")) + .arg( + Arg::with_name("order") + .long("order") + .takes_value(true) + .possible_values(&["asc", "desc"]) + .required(true) + .about("Indexer search order") + ) + .arg( + Arg::with_name("limit") + .long("limit") + .takes_value(true) + .validator(|input| FromStrParser::::default().validate(input)) + .required(true) + .about("Limit the number of results") + ) + .arg( + Arg::with_name("after") + .long("after") + .takes_value(true) + .validator(|input| HexParser.validate(input)) + .about("Pagination parameter") + ) + .about("Returns the live cells collection by the lock or type script"), + App::new("get_transactions") + .arg( + Arg::with_name("json-path") + .long("json-path") + .takes_value(true) + .validator(|input| FilePathParser::new(true).validate(input)) + .required(true) + .about("Indexer search key")) + .arg( + Arg::with_name("order") + .long("order") + .takes_value(true) + .possible_values(&["asc", "desc"]) + .required(true) + .about("Indexer search order") + ) + .arg( + Arg::with_name("limit") + .long("limit") + .takes_value(true) + .validator(|input| FromStrParser::::default().validate(input)) + .required(true) + .about("Limit the number of results") + ) + .arg( + Arg::with_name("after") + .long("after") + .takes_value(true) + .validator(|input| HexParser.validate(input)) + .about("Pagination parameter") + ) + .about("Returns the transactions collection by the lock or type script"), + App::new("get_cells_capacity") + .arg( + Arg::with_name("json-path") + .long("json-path") + .takes_value(true) + .validator(|input| FilePathParser::new(true).validate(input)) + .required(true) + .about("Indexer search key")) + .about("Returns the live cells capacity by the lock or type script"), ]) } } @@ -1068,6 +1143,96 @@ impl<'a> CliSubCommand for RpcSubCommand<'a> { .generate_block(script_opt, message_opt.map(JsonBytes::from_bytes))?; Ok(Output::new_output(resp)) } + // [Indexer] + ("get_indexer_tip", Some(m)) => { + let is_raw_data = is_raw_data || m.is_present("raw-data"); + if is_raw_data { + let resp = self + .raw_rpc_client + .get_indexer_tip() + .map_err(|err| err.to_string())?; + Ok(Output::new_output(resp)) + } else { + let resp = self.rpc_client.get_indexer_tip()?; + Ok(Output::new_output(resp)) + } + } + ("get_cells", Some(m)) => { + let json_path: PathBuf = FilePathParser::new(true) + .from_matches_opt(m, "json-path")? + .expect("json-path is required"); + let content = fs::read_to_string(json_path).map_err(|err| err.to_string())?; + let search_key = serde_json::from_str(&content).map_err(|err| err.to_string())?; + let order_str = m.value_of("order").expect("order is required"); + let order = parse_order(order_str)?; + let limit: u32 = FromStrParser::::default().from_matches(m, "limit")?; + let after_opt: Option = HexParser + .from_matches_opt::(m, "after")? + .map(JsonBytes::from_bytes); + + let is_raw_data = is_raw_data || m.is_present("raw-data"); + if is_raw_data { + let resp = self + .raw_rpc_client + .get_cells(search_key, order, limit.into(), after_opt) + .map_err(|err| err.to_string())?; + Ok(Output::new_output(resp)) + } else { + let resp = + self.rpc_client + .get_cells(search_key, order, limit.into(), after_opt)?; + Ok(Output::new_output(resp)) + } + } + ("get_transactions", Some(m)) => { + let json_path: PathBuf = FilePathParser::new(true) + .from_matches_opt(m, "json-path")? + .expect("json-path is required"); + let content = fs::read_to_string(json_path).map_err(|err| err.to_string())?; + let search_key = serde_json::from_str(&content).map_err(|err| err.to_string())?; + let order_str = m.value_of("order").expect("order is required"); + let order = parse_order(order_str)?; + let limit: u32 = FromStrParser::::default().from_matches(m, "limit")?; + let after_opt: Option = HexParser + .from_matches_opt::(m, "after")? + .map(JsonBytes::from_bytes); + + let is_raw_data = is_raw_data || m.is_present("raw-data"); + if is_raw_data { + let resp = self + .raw_rpc_client + .get_transactions(search_key, order, limit.into(), after_opt) + .map_err(|err| err.to_string())?; + Ok(Output::new_output(resp)) + } else { + let resp = self.rpc_client.get_transactions( + search_key, + order, + limit.into(), + after_opt, + )?; + Ok(Output::new_output(resp)) + } + } + ("get_cells_capacity", Some(m)) => { + let json_path: PathBuf = FilePathParser::new(true) + .from_matches_opt(m, "json-path")? + .expect("json-path is required"); + let content = fs::read_to_string(json_path).map_err(|err| err.to_string())?; + let search_key = serde_json::from_str(&content).map_err(|err| err.to_string())?; + + let is_raw_data = is_raw_data || m.is_present("raw-data"); + if is_raw_data { + let resp = self + .raw_rpc_client + .get_cells_capacity(search_key) + .map_err(|err| err.to_string())?; + Ok(Output::new_output(resp)) + } else { + let resp = self.rpc_client.get_cells_capacity(search_key)?; + Ok(Output::new_output(resp)) + } + } _ => Err(Self::subcommand().generate_usage()), } } diff --git a/src/utils/rpc/client.rs b/src/utils/rpc/client.rs index bb6035b2..30594cad 100644 --- a/src/utils/rpc/client.rs +++ b/src/utils/rpc/client.rs @@ -1,14 +1,16 @@ use std::convert::TryInto; use ckb_jsonrpc_types::{ - Alert, BlockNumber, CellWithStatus, EpochNumber, JsonBytes, OutputsValidator, Script, + Alert, BlockNumber, CellWithStatus, EpochNumber, JsonBytes, OutputsValidator, Script, Uint32, }; +pub use ckb_sdk::{ + rpc::ckb_indexer::{Order, Pagination, SearchKey}, + CkbRpcClient as RawHttpRpcClient, +}; +use ckb_types::{packed, H256}; use super::primitive; use super::types; -use ckb_types::{packed, H256}; - -pub use ckb_sdk::CkbRpcClient as RawHttpRpcClient; pub struct HttpRpcClient { url: String, @@ -411,4 +413,54 @@ impl HttpRpcClient { .notify_transaction(tx.into()) .map_err(|err| err.to_string()) } + + // Indexer + pub fn get_indexer_tip(&mut self) -> Result, String> { + self.client + .get_indexer_tip() + .map(|opt| opt.map(Into::into)) + .map_err(|err| err.to_string()) + } + + pub fn get_cells( + &mut self, + search_key: SearchKey, + order: Order, + limit: Uint32, + after: Option, + ) -> Result, String> { + self.client + .get_cells(search_key, order, limit, after) + .map(|p| Pagination { + objects: p.objects.into_iter().map(Into::into).collect(), + last_cursor: p.last_cursor, + }) + .map_err(|err| err.to_string()) + } + + pub fn get_transactions( + &mut self, + search_key: SearchKey, + order: Order, + limit: Uint32, + after: Option, + ) -> Result, String> { + self.client + .get_transactions(search_key, order, limit, after) + .map(|p| Pagination { + objects: p.objects.into_iter().map(Into::into).collect(), + last_cursor: p.last_cursor, + }) + .map_err(|err| err.to_string()) + } + + pub fn get_cells_capacity( + &mut self, + search_key: SearchKey, + ) -> Result, String> { + self.client + .get_cells_capacity(search_key) + .map(|opt| opt.map(Into::into)) + .map_err(|err| err.to_string()) + } } diff --git a/src/utils/rpc/mod.rs b/src/utils/rpc/mod.rs index 7e1fb21b..f7ecfa98 100644 --- a/src/utils/rpc/mod.rs +++ b/src/utils/rpc/mod.rs @@ -6,10 +6,11 @@ mod types; pub use client::{HttpRpcClient, RawHttpRpcClient}; pub use primitive::{Capacity, EpochNumberWithFraction, Since, Timestamp}; pub use types::{ - Alert, AlertMessage, BannedAddr, Block, BlockEconomicState, BlockIssuance, BlockResponse, - BlockView, Byte32, CellDep, CellInput, CellOutput, ChainInfo, DepType, EpochView, Header, - HeaderView, JsonBytes, LocalNode, MerkleProof, MinerReward, NodeAddress, OutPoint, - PackedBlockResponse, ProposalShortId, RemoteNode, Script, ScriptHashType, Transaction, - TransactionAndWitnessProof, TransactionProof, TransactionView, TransactionWithStatus, - TransactionWithStatusResponse, TxPoolInfo, TxStatus, Uint128, UncleBlock, UncleBlockView, + parse_order, Alert, AlertMessage, BannedAddr, Block, BlockEconomicState, BlockIssuance, + BlockResponse, BlockView, Byte32, CellDep, CellInput, CellOutput, ChainInfo, DepType, + EpochView, Header, HeaderView, JsonBytes, LocalNode, MerkleProof, MinerReward, NodeAddress, + OutPoint, PackedBlockResponse, ProposalShortId, RemoteNode, Script, ScriptHashType, + Transaction, TransactionAndWitnessProof, TransactionProof, TransactionView, + TransactionWithStatus, TransactionWithStatusResponse, TxPoolInfo, TxStatus, Uint128, + UncleBlock, UncleBlockView, }; diff --git a/src/utils/rpc/types.rs b/src/utils/rpc/types.rs index ddcc0804..4255263b 100644 --- a/src/utils/rpc/types.rs +++ b/src/utils/rpc/types.rs @@ -11,6 +11,7 @@ use ckb_types::{core, packed, prelude::*, H256, U256}; use super::primitive::{Capacity, EpochNumberWithFraction, Since, Timestamp}; use crate::utils::rpc::json_rpc; use ckb_sdk::constants::{DAO_TYPE_HASH, MULTISIG_TYPE_HASH, SIGHASH_TYPE_HASH}; +use ckb_sdk::rpc::ckb_indexer::{self, CellType, Order}; type Version = u32; type BlockNumber = u64; @@ -1701,3 +1702,129 @@ impl From for FeeRateStatistics { } } } + +/// Response type of the RPC method `get_indexer_tip`. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct IndexerTip { + pub block_hash: H256, + pub block_number: BlockNumber, +} + +impl From for IndexerTip { + fn from(tip: ckb_indexer::Tip) -> IndexerTip { + IndexerTip { + block_hash: tip.block_hash, + block_number: tip.block_number.into(), + } + } +} + +/// Response type of the RPC method `get_cells`. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Cell { + pub output: CellOutput, + pub output_data: Option, + pub out_point: OutPoint, + pub block_number: BlockNumber, + pub tx_index: Uint32, +} + +impl From for Cell { + fn from(cell: ckb_indexer::Cell) -> Cell { + Cell { + output: cell.output.into(), + output_data: cell.output_data.map(Into::into), + out_point: cell.out_point.into(), + block_number: cell.block_number.into(), + tx_index: cell.tx_index.into(), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(untagged)] +pub enum Tx { + Ungrouped(TxWithCell), + Grouped(TxWithCells), +} + +impl From for Tx { + fn from(tx: ckb_indexer::Tx) -> Tx { + match tx { + ckb_indexer::Tx::Ungrouped(tx_with_cell) => Tx::Ungrouped(tx_with_cell.into()), + ckb_indexer::Tx::Grouped(tx_with_cells) => Tx::Grouped(tx_with_cells.into()), + } + } +} + +/// Response type of the RPC method `get_transactions`. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TxWithCell { + pub tx_hash: H256, + pub block_number: BlockNumber, + pub tx_index: Uint32, + pub io_index: Uint32, + pub io_type: CellType, +} + +impl From for TxWithCell { + fn from(tx_with_cell: ckb_indexer::TxWithCell) -> TxWithCell { + TxWithCell { + tx_hash: tx_with_cell.tx_hash, + block_number: tx_with_cell.block_number.into(), + tx_index: tx_with_cell.tx_index.into(), + io_index: tx_with_cell.io_index.into(), + io_type: tx_with_cell.io_type, + } + } +} + +/// Response type of the RPC method `get_transactions`. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TxWithCells { + pub tx_hash: H256, + pub block_number: BlockNumber, + pub tx_index: Uint32, + pub cells: Vec<(CellType, Uint32)>, +} + +impl From for TxWithCells { + fn from(tx_with_cells: ckb_indexer::TxWithCells) -> TxWithCells { + TxWithCells { + tx_hash: tx_with_cells.tx_hash, + block_number: tx_with_cells.block_number.into(), + tx_index: tx_with_cells.tx_index.into(), + cells: tx_with_cells + .cells + .into_iter() + .map(|(cell_type, io_index)| (cell_type, io_index.into())) + .collect(), + } + } +} + +/// Response type of the RPC method `get_cells_capacity`. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct CellsCapacity { + pub capacity: Capacity, + pub block_hash: H256, + pub block_number: BlockNumber, +} + +impl From for CellsCapacity { + fn from(json: ckb_indexer::CellsCapacity) -> CellsCapacity { + CellsCapacity { + capacity: json.capacity.into(), + block_hash: json.block_hash, + block_number: json.block_number.into(), + } + } +} + +pub fn parse_order(order_str: &str) -> Result { + match order_str.to_lowercase().as_str() { + "desc" => Ok(Order::Desc), + "asc" => Ok(Order::Asc), + _ => Err(format!("Invalid order: {}", order_str)), + } +}