-
Notifications
You must be signed in to change notification settings - Fork 56
feat: add rs-scripts crate with decode-document CLI tool #3391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 -- <BASE64_DOC> [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 \ | ||
| <node-ip>: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 | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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), | ||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+25
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 Nitpick: SYSTEM_CONTRACTS list must be manually kept in sync with SystemDataContract enum Verified: The For a small debugging tool this is acceptable, but if a new system contract is added to the enum, this list will silently become incomplete. A source: ['claude']
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment: |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #[derive(Parser)] | ||||||||||||||||||||||||||||||||||||||||
| #[command(name = "decode-document", about = "Decode a platform document from base64 bytes")] | ||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 Nitpick: CLI about text says 'base64 bytes' but tool also accepts hex Verified: Line 24 has 💡 Suggested change
Suggested change
source: ['claude']
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — about text now says "Decode a platform document from hex or 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::<Vec<_>>().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(); | ||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: PlatformVersion::latest() can cause historical documents to decode incorrectly Verified: A document serialized under platform v8 may fail or be misinterpreted when deserialized with a v9+ contract config loaded via source: ['claude', 'codex'] 🤖 Fix this with AI agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point. For a debugging tool, |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| 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"); | ||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Hex-first decode ordering silently misinterprets some base64 inputs The code tries Verified against code at lines 79-86: confirmed hex is tried first. The simplest fix would be to either swap to base64-first (base64 is stricter about padding so fewer false positives) or add an explicit source: ['claude', 'codex'] 🤖 Fix this with AI agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — swapped to base64-first in auto mode and added |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| let document = Document::from_bytes(&bytes, document_type, platform_version) | ||||||||||||||||||||||||||||||||||||||||
| .expect("failed to deserialize document"); | ||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: User-facing errors should not panic — use consistent error handling Verified: Lines 73, 77, and 89 use For a CLI tool, all user-facing errors should produce clean messages. Invalid contract name, unknown doc type, and deserialization failure are all expected user-input scenarios, not programming bugs. The 💡 Suggested change
Suggested change
source: ['claude', 'codex'] 🤖 Fix this with AI agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — all |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+165
to
+168
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n -C2 'format_ts|as i64|from_timestamp' packages/rs-scripts/src/bin/decode_document.rsRepository: dashpay/platform Length of output: 762 🏁 Script executed: #!/bin/bash
# Find chrono dependency and version
fd 'Cargo.toml' packages/rs-scripts/ | xargs cat | grep -A5 'dependencies'Repository: dashpay/platform Length of output: 270 🏁 Script executed: #!/bin/bash
# Check the full function implementation and context
sed -n '109,120p' packages/rs-scripts/src/bin/decode_document.rsRepository: dashpay/platform Length of output: 381 🌐 Web query:
💡 Result: The DateTime::from_timestamp(secs: i64, nsecs: u32) method in the chrono crate creates a new DateTime from the given UNIX timestamp (non-leap seconds since 1970-01-01 00:00:00 UTC) and nanoseconds. This method panics if the resulting NaiveDateTime is out of range. The valid range for NaiveDateTime (and thus DateTime) corresponds to NaiveDate::MIN at 00:00:00 to NaiveDate::MAX at 23:59:59.999999999: - Minimum: January 1, -262144 (262145 BCE) - Maximum: December 31, 262142 (262143 CE) An i64 timestamp spans from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 seconds (~292 billion years), far exceeding Chrono's date limits (~262,000 years from epoch). Thus, from_timestamp panics for timestamps too far from the epoch, even if within i64. Use the fallible DateTime::from_timestamp_opt(secs: i64, nsecs: u32) -> Option<DateTime> (or Utc::timestamp_opt) instead, which returns None for out-of-range values. To compute exact min/max i64 timestamps, calculate seconds from epoch to MIN/MAX dates, but typically unnecessary—check the Option result. Example: use chrono::{DateTime, Utc}; let dt = DateTime::from_timestamp_opt(1_500_000_000i64, 0).unwrap; // Fri, 14 Jul 2017 02:40:00 UTC let out_of_range = DateTime::from_timestamp_opt(i64::MIN, 0); // None Citations:
Use checked casting for explicit timestamp validation. The cast from Suggested fix (checked conversion) fn format_ts(ms: u64) -> String {
- let secs = (ms / 1000) as i64;
+ let secs = match i64::try_from(ms / 1000) {
+ Ok(v) => v,
+ Err(_) => return format!("invalid timestamp: {ms}"),
+ };
let nanos = ((ms % 1000) * 1_000_000) as u32;
let dt = chrono::DateTime::from_timestamp(secs, nanos);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The cast is safe:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The existing |
||||||||||||||||||||||||||||||||||||||||
| match dt { | ||||||||||||||||||||||||||||||||||||||||
| Some(dt) => dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(), | ||||||||||||||||||||||||||||||||||||||||
| None => format!("invalid timestamp: {ms}"), | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+173
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 Nitpick: No tests for the crate Verified: The crate has no source: ['claude', 'codex']
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged — this is a small CLI debugging tool, not a library. The |
||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.