From d092420625bac84ebb50bf7f9dc9664ef1c5bf72 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 20 Mar 2026 10:31:51 +0300 Subject: [PATCH 1/2] feat: add rs-scripts crate with decode-document CLI tool Adds a utility crate for debugging and inspecting Platform data. The first tool, decode-document, deserializes platform documents from base64 or hex bytes using the actual platform serialization code. Supports all system contracts by name or ID (base58/base64/hex). Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 14 +++ Cargo.toml | 1 + packages/rs-scripts/Cargo.toml | 18 +++ packages/rs-scripts/README.md | 59 +++++++++ .../rs-scripts/src/bin/decode_document.rs | 117 ++++++++++++++++++ 5 files changed, 209 insertions(+) create mode 100644 packages/rs-scripts/Cargo.toml create mode 100644 packages/rs-scripts/README.md create mode 100644 packages/rs-scripts/src/bin/decode_document.rs diff --git a/Cargo.lock b/Cargo.lock index b11461fe108..03a7cb40cd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5844,6 +5844,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "rs-scripts" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "chrono", + "clap", + "data-contracts", + "dpp", + "hex", + "platform-version", + "serde_json", +] + [[package]] name = "rs-sdk-ffi" version = "3.1.0-dev.1" diff --git a/Cargo.toml b/Cargo.toml index c4993e32ad8..3a2dc8327cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ members = [ "packages/rs-platform-wallet-ffi", "packages/rs-platform-encryption", "packages/wasm-sdk", + "packages/rs-scripts", ] [workspace.dependencies] diff --git a/packages/rs-scripts/Cargo.toml b/packages/rs-scripts/Cargo.toml new file mode 100644 index 00000000000..dc639760994 --- /dev/null +++ b/packages/rs-scripts/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rs-scripts" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "decode-document" +path = "src/bin/decode_document.rs" + +[dependencies] +dpp = { path = "../rs-dpp", features = ["system_contracts"] } +data-contracts = { path = "../data-contracts" } +platform-version = { path = "../rs-platform-version" } +base64 = "0.22" +chrono = "0.4" +hex = "0.4" +clap = { version = "4", features = ["derive"] } +serde_json = "1" diff --git a/packages/rs-scripts/README.md b/packages/rs-scripts/README.md new file mode 100644 index 00000000000..abdbc174585 --- /dev/null +++ b/packages/rs-scripts/README.md @@ -0,0 +1,59 @@ +# rs-scripts + +Utility scripts for debugging and inspecting Dash Platform data. + +## decode-document + +Decodes a base64-encoded platform document into human-readable output. Uses the actual platform deserialization code, so it handles all document format versions correctly. + +### Usage + +```bash +cargo run -p rs-scripts --bin decode-document -- [OPTIONS] +``` + +### Options + +| Option | Required | Description | +|--------|----------|-------------| +| `-c, --contract` | yes | System data contract name or ID (base58/base64/hex) | +| `-d, --doc-type` | yes | Document type name within the contract | + +### Supported contracts + +`withdrawals`, `dpns`, `dashpay`, `masternode-reward-shares`, `feature-flags`, `wallet-utils`, `token-history`, `keyword-search` + +You can also pass the contract ID directly instead of a name (you'll need `-d` to specify the document type): +```bash +# base58 +cargo run -p rs-scripts --bin decode-document -- -c 4fJLR2GYTPFdomuTVvNy3VRrvWgvkKPzqehEBpNf2nk6 -d withdrawal "base64data..." +# base64 +cargo run -p rs-scripts --bin decode-document -- -c "NmK7YeF/rj6ilM9gMZf7CqttURgL2LYQTElEpi/i2X8=" -d withdrawal "base64data..." +# hex +cargo run -p rs-scripts --bin decode-document -- -c 3662bb61e17fae3ea294cf603197fb0aab6d51180bd8b6104c4944a62fe2d97f -d withdrawal "base64data..." +``` + +### Examples + +Decode a withdrawal document: +```bash +cargo run -p rs-scripts --bin decode-document -- -c withdrawals -d withdrawal "AgIintqUs1vl..." +``` + +Decode a DPNS domain document: +```bash +cargo run -p rs-scripts --bin decode-document -- -c dpns -d domain "base64data..." +``` + +Pipe from a gRPC query (decode each document from the response): +```bash +echo '{"v0":{"prove":false,"data_contract_id":"NmK7YeF/rj6ilM9gMZf7CqttURgL2LYQTElEpi/i2X8=","document_type":"withdrawal","where":"gYNmc3RhdHVzYT0C","limit":10}}' \ + | grpcurl -insecure -import-path packages/dapi-grpc/protos -d @ \ + -proto platform/v0/platform.proto \ + :443 org.dash.platform.dapi.v0.Platform/getDocuments \ + | jq -r '.v0.documents.documents[]' \ + | while read doc; do + cargo run -p rs-scripts --bin decode-document -- -c withdrawals -d withdrawal "$doc" + echo "---" + done +``` diff --git a/packages/rs-scripts/src/bin/decode_document.rs b/packages/rs-scripts/src/bin/decode_document.rs new file mode 100644 index 00000000000..0b32b5a842c --- /dev/null +++ b/packages/rs-scripts/src/bin/decode_document.rs @@ -0,0 +1,117 @@ +use base64::Engine; +use clap::Parser; +use data_contracts::SystemDataContract; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::document::DocumentV0Getters; +use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; +use dpp::document::Document; +use dpp::system_data_contracts::load_system_data_contract; +use dpp::platform_value::Identifier; +use platform_version::version::PlatformVersion; + +const SYSTEM_CONTRACTS: &[(&str, SystemDataContract)] = &[ + ("withdrawals", SystemDataContract::Withdrawals), + ("dpns", SystemDataContract::DPNS), + ("dashpay", SystemDataContract::Dashpay), + ("masternode-reward-shares", SystemDataContract::MasternodeRewards), + ("feature-flags", SystemDataContract::FeatureFlags), + ("wallet-utils", SystemDataContract::WalletUtils), + ("token-history", SystemDataContract::TokenHistory), + ("keyword-search", SystemDataContract::KeywordSearch), +]; + +#[derive(Parser)] +#[command(name = "decode-document", about = "Decode a platform document from base64 bytes")] +struct Args { + /// Document bytes (base64 or hex encoded) + doc_bytes: String, + + /// System data contract: name (e.g. "withdrawals") or ID in base58/base64/hex + #[arg(short, long)] + contract: String, + + /// Document type name within the contract (e.g. "withdrawal", "domain") + #[arg(short, long)] + doc_type: String, +} + +fn resolve_system_contract(input: &str) -> SystemDataContract { + // Try by name first + for (name, sc) in SYSTEM_CONTRACTS { + if input.eq_ignore_ascii_case(name) { + return *sc; + } + } + + // Try parsing as an identifier (base58, base64, or hex) + let id = Identifier::from_string_unknown_encoding(input) + .unwrap_or_else(|_| { + eprintln!("Unknown contract: '{input}'"); + eprintln!("Must be a name ({}) or an ID in base58/base64/hex", + SYSTEM_CONTRACTS.iter().map(|(n, _)| *n).collect::>().join(", ")); + std::process::exit(1); + }); + + for (_, sc) in SYSTEM_CONTRACTS { + if sc.id() == id { + return *sc; + } + } + + eprintln!("No system contract found with ID {id}"); + std::process::exit(1); +} + +fn main() { + let args = Args::parse(); + + let platform_version = PlatformVersion::latest(); + + let system_contract = resolve_system_contract(&args.contract); + + let data_contract = load_system_data_contract(system_contract, platform_version) + .expect("failed to load system data contract"); + + let document_type = data_contract + .document_type_for_name(&args.doc_type) + .expect("failed to get document type"); + + let bytes = if let Ok(b) = hex::decode(&args.doc_bytes) { + b + } else if let Ok(b) = base64::engine::general_purpose::STANDARD.decode(&args.doc_bytes) { + b + } else { + eprintln!("Failed to decode document bytes as hex or base64"); + std::process::exit(1); + }; + + let document = Document::from_bytes(&bytes, document_type, platform_version) + .expect("failed to deserialize document"); + + println!("id: {}", document.id()); + println!("owner_id: {}", document.owner_id()); + if let Some(created_at) = document.created_at() { + println!("created_at: {} ({}ms)", format_ts(created_at), created_at); + } + if let Some(updated_at) = document.updated_at() { + println!("updated_at: {} ({}ms)", format_ts(updated_at), updated_at); + } + if let Some(revision) = document.revision() { + println!("revision: {}", revision); + } + println!(); + println!("properties:"); + for (key, value) in document.properties() { + println!(" {key}: {value}"); + } +} + +fn format_ts(ms: u64) -> String { + let secs = (ms / 1000) as i64; + let nanos = ((ms % 1000) * 1_000_000) as u32; + let dt = chrono::DateTime::from_timestamp(secs, nanos); + match dt { + Some(dt) => dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(), + None => format!("invalid timestamp: {ms}"), + } +} From fb3d88c857ef3c7cba8fb7a887f5f0d221a9c171 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 3 Apr 2026 19:35:46 +0300 Subject: [PATCH 2/2] fix: address review feedback on decode-document CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace all .expect() with proper error handling (eprintln + exit 1) - Fix about text: "base64 bytes" → "hex or base64 bytes" - Add --format flag (base64|hex|auto) to control input decoding - Try base64 before hex in auto mode (gRPC output is base64) - Add hint about --format when auto-detection fails - List available document types on unknown doc type error - Add sync comment on SYSTEM_CONTRACTS list - Update README to mention hex and --format flag Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/rs-scripts/README.md | 5 +- .../rs-scripts/src/bin/decode_document.rs | 106 +++++++++++++----- 2 files changed, 84 insertions(+), 27 deletions(-) diff --git a/packages/rs-scripts/README.md b/packages/rs-scripts/README.md index abdbc174585..32dee2822e2 100644 --- a/packages/rs-scripts/README.md +++ b/packages/rs-scripts/README.md @@ -4,12 +4,12 @@ Utility scripts for debugging and inspecting Dash Platform data. ## decode-document -Decodes a base64-encoded platform document into human-readable output. Uses the actual platform deserialization code, so it handles all document format versions correctly. +Decodes a hex or base64-encoded platform document into human-readable output. Uses the actual platform deserialization code, so it handles all document format versions correctly. ### Usage ```bash -cargo run -p rs-scripts --bin decode-document -- [OPTIONS] +cargo run -p rs-scripts --bin decode-document -- [OPTIONS] ``` ### Options @@ -18,6 +18,7 @@ cargo run -p rs-scripts --bin decode-document -- [OPTIONS] |--------|----------|-------------| | `-c, --contract` | yes | System data contract name or ID (base58/base64/hex) | | `-d, --doc-type` | yes | Document type name within the contract | +| `-f, --format` | no | Input encoding: `base64`, `hex`, or `auto` (default: `auto`) | ### Supported contracts diff --git a/packages/rs-scripts/src/bin/decode_document.rs b/packages/rs-scripts/src/bin/decode_document.rs index 0b32b5a842c..79442016f39 100644 --- a/packages/rs-scripts/src/bin/decode_document.rs +++ b/packages/rs-scripts/src/bin/decode_document.rs @@ -2,18 +2,22 @@ use base64::Engine; use clap::Parser; use data_contracts::SystemDataContract; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::document::DocumentV0Getters; use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; use dpp::document::Document; -use dpp::system_data_contracts::load_system_data_contract; +use dpp::document::DocumentV0Getters; use dpp::platform_value::Identifier; +use dpp::system_data_contracts::load_system_data_contract; use platform_version::version::PlatformVersion; +// Keep in sync with SystemDataContract enum in packages/data-contracts/src/lib.rs const SYSTEM_CONTRACTS: &[(&str, SystemDataContract)] = &[ ("withdrawals", SystemDataContract::Withdrawals), ("dpns", SystemDataContract::DPNS), ("dashpay", SystemDataContract::Dashpay), - ("masternode-reward-shares", SystemDataContract::MasternodeRewards), + ( + "masternode-reward-shares", + SystemDataContract::MasternodeRewards, + ), ("feature-flags", SystemDataContract::FeatureFlags), ("wallet-utils", SystemDataContract::WalletUtils), ("token-history", SystemDataContract::TokenHistory), @@ -21,7 +25,10 @@ const SYSTEM_CONTRACTS: &[(&str, SystemDataContract)] = &[ ]; #[derive(Parser)] -#[command(name = "decode-document", about = "Decode a platform document from base64 bytes")] +#[command( + name = "decode-document", + about = "Decode a platform document from hex or base64 bytes" +)] struct Args { /// Document bytes (base64 or hex encoded) doc_bytes: String, @@ -33,6 +40,10 @@ struct Args { /// Document type name within the contract (e.g. "withdrawal", "domain") #[arg(short, long)] doc_type: String, + + /// Input encoding: "base64", "hex", or "auto" (default: auto, tries base64 then hex) + #[arg(short, long, default_value = "auto")] + format: String, } fn resolve_system_contract(input: &str) -> SystemDataContract { @@ -44,13 +55,18 @@ fn resolve_system_contract(input: &str) -> SystemDataContract { } // Try parsing as an identifier (base58, base64, or hex) - let id = Identifier::from_string_unknown_encoding(input) - .unwrap_or_else(|_| { - eprintln!("Unknown contract: '{input}'"); - eprintln!("Must be a name ({}) or an ID in base58/base64/hex", - SYSTEM_CONTRACTS.iter().map(|(n, _)| *n).collect::>().join(", ")); - std::process::exit(1); - }); + let id = Identifier::from_string_unknown_encoding(input).unwrap_or_else(|_| { + eprintln!("Unknown contract: '{input}'"); + eprintln!( + "Must be a name ({}) or an ID in base58/base64/hex", + SYSTEM_CONTRACTS + .iter() + .map(|(n, _)| *n) + .collect::>() + .join(", ") + ); + std::process::exit(1); + }); for (_, sc) in SYSTEM_CONTRACTS { if sc.id() == id { @@ -69,24 +85,64 @@ fn main() { let system_contract = resolve_system_contract(&args.contract); - let data_contract = load_system_data_contract(system_contract, platform_version) - .expect("failed to load system data contract"); + let data_contract = match load_system_data_contract(system_contract, platform_version) { + Ok(c) => c, + Err(e) => { + eprintln!("Failed to load system data contract: {e}"); + std::process::exit(1); + } + }; - let document_type = data_contract - .document_type_for_name(&args.doc_type) - .expect("failed to get document type"); + let document_type = match data_contract.document_type_for_name(&args.doc_type) { + Ok(dt) => dt, + Err(e) => { + eprintln!("Unknown document type '{}': {e}", args.doc_type); + eprintln!( + "Available types: {}", + data_contract + .document_types() + .keys() + .cloned() + .collect::>() + .join(", ") + ); + std::process::exit(1); + } + }; - let bytes = if let Ok(b) = hex::decode(&args.doc_bytes) { - b - } else if let Ok(b) = base64::engine::general_purpose::STANDARD.decode(&args.doc_bytes) { - b - } else { - eprintln!("Failed to decode document bytes as hex or base64"); - std::process::exit(1); + let bytes = match args.format.as_str() { + "base64" => base64::engine::general_purpose::STANDARD + .decode(&args.doc_bytes) + .unwrap_or_else(|e| { + eprintln!("Invalid base64: {e}"); + std::process::exit(1); + }), + "hex" => hex::decode(&args.doc_bytes).unwrap_or_else(|e| { + eprintln!("Invalid hex: {e}"); + std::process::exit(1); + }), + "auto" | _ => { + // Try base64 first (most common — gRPC responses are base64), + // then hex. This avoids misinterpreting hex-only base64 strings. + if let Ok(b) = base64::engine::general_purpose::STANDARD.decode(&args.doc_bytes) { + b + } else if let Ok(b) = hex::decode(&args.doc_bytes) { + b + } else { + eprintln!("Failed to decode document bytes as base64 or hex"); + eprintln!("Hint: use --format base64 or --format hex to force a specific encoding"); + std::process::exit(1); + } + } }; - let document = Document::from_bytes(&bytes, document_type, platform_version) - .expect("failed to deserialize document"); + let document = match Document::from_bytes(&bytes, document_type, platform_version) { + Ok(doc) => doc, + Err(e) => { + eprintln!("Failed to deserialize document: {e}"); + std::process::exit(1); + } + }; println!("id: {}", document.id()); println!("owner_id: {}", document.owner_id());