Skip to content

feat: add rs-scripts crate with decode-document CLI tool#3391

Open
QuantumExplorer wants to merge 1 commit intov3.1-devfrom
feat/rs-scripts
Open

feat: add rs-scripts crate with decode-document CLI tool#3391
QuantumExplorer wants to merge 1 commit intov3.1-devfrom
feat/rs-scripts

Conversation

@QuantumExplorer
Copy link
Member

@QuantumExplorer QuantumExplorer commented Mar 20, 2026

Summary

  • Adds rs-scripts workspace crate with a decode-document CLI tool for deserializing platform documents from base64 or hex bytes
  • Uses the actual platform serialization code, so it handles all document format versions correctly
  • Supports all system contracts by name (e.g. withdrawals) or by ID in base58/base64/hex
  • Installable via cargo install --path packages/rs-scripts

Usage

decode-document -c withdrawals -d withdrawal "AgIintqUs1vl..."

Test plan

  • Verified decoding withdrawal documents from base64 and hex
  • Verified contract resolution by name, base58, base64, and hex ID
  • Verified cargo install works

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • New decode-document utility for decoding and debugging base64-encoded platform documents, supporting all system contract types and document format versions.
  • Documentation

    • Added comprehensive guide with CLI usage, supported contracts list, and integration examples with common debugging tools.

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) <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

A new Rust package rs-scripts is added to the workspace, providing a decode-document binary utility that decodes base64/hex-encoded platform documents. The package includes manifest configuration, documentation, and implementation with CLI argument parsing, system contract resolution, and document deserialization.

Changes

Cohort / File(s) Summary
Workspace Configuration
Cargo.toml
Root workspace manifest updated to include packages/rs-scripts as a new member in the workspace members list.
Package Setup & Documentation
packages/rs-scripts/Cargo.toml, packages/rs-scripts/README.md
New package manifest defining metadata, binary target decode-document, and dependencies (workspace crates: dpp, data-contracts, platform-version; external: base64, chrono, hex, clap, serde_json). README documents the utility's purpose, CLI interface, supported system contracts, and usage examples.
Binary Implementation
packages/rs-scripts/src/bin/decode_document.rs
New decode-document binary that parses CLI arguments, resolves system contract identifiers (supporting base58/base64/hex formats and case-insensitive name matching), loads platform contracts, and deserializes base64/hex-encoded documents with formatted output of document fields and properties.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A carrot of code, in base64 dressed,
Our decoder hops forth—put it to the test!
Documents bloom where cryptic bytes lay,
Now platform scripts skip and decode the day! 🌿

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add rs-scripts crate with decode-document CLI tool' directly and clearly summarizes the main change: adding a new workspace crate with a CLI tool.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rs-scripts
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added this to the v3.1.0 milestone Mar 20, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/rs-scripts/README.md`:
- Around line 7-13: Update the README usage text for the decode-document binary
to indicate it accepts both base64 and hex input (not only base64); change the
placeholder and description for the CLI invocation of the decode-document binary
(binary name: decode-document) to something like <ENCODED_DOC> and note that the
input may be base64 or hex, and adjust the preceding sentence to state “accepts
base64 or hex-encoded platform documents” so users aren’t misled by the current
base64-only wording.

In `@packages/rs-scripts/src/bin/decode_document.rs`:
- Around line 109-112: Replace the unchecked cast in format_ts by performing a
checked conversion for seconds: compute secs_u64 = ms / 1000 and use
i64::try_from(secs_u64) to obtain secs; on Err, return the same fallback used
when chrono::DateTime::from_timestamp returns None (preserve the existing None
branch behavior) so out-of-range values are handled explicitly rather than by
implicit wrapping; keep the nanos computation and the rest of the dt handling
unchanged.
- Around line 72-73: The three `.expect()` calls (around
`load_system_data_contract`, the contract type lookup, and the document
deserialization) must be replaced with the same graceful error handling used
earlier in this file (lines ~48–52): handle the Result/Option with `match` or
`if let Err/None` to print a structured error to stderr (e.g. `eprintln!`) and
call `std::process::exit(1)` instead of panicking. Specifically, replace the
`expect` on `load_system_data_contract(...)` with error branching that logs the
failure and exits, do the same for the contract type lookup (the lookup function
used in this file) and for the deserialization step (the function that parses
the document); keep the existing error message style and include the underlying
error details when available.
- Around line 79-86: The current decoding chooses hex first by testing
hex::decode(&args.doc_bytes) before base64, causing base64 payloads composed of
hex-safe chars (e.g., "414243") to be mis-decoded; update the CLI parsing (where
args.doc_bytes is defined) to add an explicit --encoding flag (enum like
Encoding::Auto|Hex|Base64, default Auto) and change the decoding logic in this
block to: if args.encoding == Encoding::Hex -> try hex::decode, else if
Encoding::Base64 -> try base64::engine::general_purpose::STANDARD.decode, else
(Auto) try base64 first and fall back to hex or validate decoded bytes
downstream; ensure error messages reference args.doc_bytes and the selected
encoding when failing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b9a116c-b3ea-4537-bc2a-9d1a3414d5a2

📥 Commits

Reviewing files that changed from the base of the PR and between 2bb561b and d092420.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • Cargo.toml
  • packages/rs-scripts/Cargo.toml
  • packages/rs-scripts/README.md
  • packages/rs-scripts/src/bin/decode_document.rs

Comment on lines +7 to +13
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]
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documentation still says base64-only input.

Line 7 and Line 12 imply only base64 is accepted, but the CLI supports both base64 and hex. Please update wording/placeholder to avoid user confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-scripts/README.md` around lines 7 - 13, Update the README usage
text for the decode-document binary to indicate it accepts both base64 and hex
input (not only base64); change the placeholder and description for the CLI
invocation of the decode-document binary (binary name: decode-document) to
something like <ENCODED_DOC> and note that the input may be base64 or hex, and
adjust the preceding sentence to state “accepts base64 or hex-encoded platform
documents” so users aren’t misled by the current base64-only wording.

Comment on lines +72 to +73
let data_contract = load_system_data_contract(system_contract, platform_version)
.expect("failed to load system data contract");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n '\.expect\(' packages/rs-scripts/src/bin/decode_document.rs

Repository: dashpay/platform

Length of output: 223


🏁 Script executed:

cat -n packages/rs-scripts/src/bin/decode_document.rs | head -100

Repository: dashpay/platform

Length of output: 4320


🏁 Script executed:

find packages/rs-scripts -type f -name "*.rs" | head -20

Repository: dashpay/platform

Length of output: 106


🏁 Script executed:

rg -n "fn main" packages/rs-scripts/src/bin/decode_document.rs

Repository: dashpay/platform

Length of output: 74


🏁 Script executed:

rg -n "Result|Error" packages/rs-scripts/src/bin/decode_document.rs | head -20

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

find packages/rs-scripts/src/bin -type f -name "*.rs" | xargs -I {} sh -c 'echo "=== {} ===" && rg -c "\.expect\(" {}'

Repository: dashpay/platform

Length of output: 116


🏁 Script executed:

find packages/rs-scripts/src/bin -type f -name "*.rs" | xargs -I {} sh -c 'echo "=== {} ===" && rg -c "eprintln\!" {}'

Repository: dashpay/platform

Length of output: 218


🏁 Script executed:

rg "eprintln" packages/rs-scripts/src/bin/decode_document.rs -n

Repository: dashpay/platform

Length of output: 327


Replace .expect() calls with graceful error handling to match established patterns in this file.

Lines 73, 77, and 89 use .expect() causing panic instead of the consistent error handling used elsewhere (lines 48–52). Return structured errors to stderr with exit code 1 for all failures (contract loading, type lookup, deserialization).

Suggested fix
-    let data_contract = load_system_data_contract(system_contract, platform_version)
-        .expect("failed to load system data contract");
+    let data_contract = load_system_data_contract(system_contract, platform_version)
+        .unwrap_or_else(|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");
+        .unwrap_or_else(|| {
+            eprintln!("Unknown document type '{}' for selected contract", args.doc_type);
+            std::process::exit(1);
+        });

-    let document = Document::from_bytes(&bytes, document_type, platform_version)
-        .expect("failed to deserialize document");
+    let document = Document::from_bytes(&bytes, document_type, platform_version)
+        .unwrap_or_else(|e| {
+            eprintln!("Failed to deserialize document: {e}");
+            std::process::exit(1);
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-scripts/src/bin/decode_document.rs` around lines 72 - 73, The
three `.expect()` calls (around `load_system_data_contract`, the contract type
lookup, and the document deserialization) must be replaced with the same
graceful error handling used earlier in this file (lines ~48–52): handle the
Result/Option with `match` or `if let Err/None` to print a structured error to
stderr (e.g. `eprintln!`) and call `std::process::exit(1)` instead of panicking.
Specifically, replace the `expect` on `load_system_data_contract(...)` with
error branching that logs the failure and exits, do the same for the contract
type lookup (the lookup function used in this file) and for the deserialization
step (the function that parses the document); keep the existing error message
style and include the underlying error details when available.

Comment on lines +79 to +86
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);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 'hex::decode|STANDARD.decode|doc_bytes' packages/rs-scripts/src/bin/decode_document.rs

Repository: dashpay/platform

Length of output: 504


🏁 Script executed:

# Check current Rust formatting and clippy status
cd packages/rs-scripts && cargo clippy --all-targets 2>&1 | head -100

Repository: dashpay/platform

Length of output: 658


🏁 Script executed:

# Check rustfmt compliance
cd packages/rs-scripts && cargo fmt --all --check 2>&1 | head -50

Repository: dashpay/platform

Length of output: 434


🏁 Script executed:

# Examine the full decode_document.rs file to understand downstream impact
wc -l packages/rs-scripts/src/bin/decode_document.rs

Repository: dashpay/platform

Length of output: 110


🏁 Script executed:

# Look for tests and how the decoded bytes are used
rg -n -A5 'hex::decode|STANDARD.decode' packages/rs-scripts/src/bin/decode_document.rs | head -60

Repository: dashpay/platform

Length of output: 381


🏁 Script executed:

# Check if there are integration tests for this binary
fd -t f 'decode_document' packages/rs-scripts

Repository: dashpay/platform

Length of output: 106


🏁 Script executed:

# Read the full file to understand context and downstream usage
cat -n packages/rs-scripts/src/bin/decode_document.rs

Repository: dashpay/platform

Length of output: 4907


🏁 Script executed:

# Check if there are any tests in the packages/rs-scripts directory
fd -t f '\.rs$' packages/rs-scripts/tests 2>/dev/null || echo "No tests directory"

Repository: dashpay/platform

Length of output: 78


🏁 Script executed:

# Search for how bytes variable is used after decoding
rg -n -A20 'let bytes = if let Ok' packages/rs-scripts/src/bin/decode_document.rs

Repository: dashpay/platform

Length of output: 980


Hex-first auto-detection can misinterpret valid base64 input with overlapping character sets.

When both hex::decode() and base64::decode() succeed on the same input, hex is always chosen due to the if-else ordering at line 79. This causes incorrect deserialization for base64 strings that contain only characters valid in hex (e.g., "414243"). Resolve this by adding an explicit --encoding flag (defaulting to auto-detection with user override) or restructure to validate the decoded result downstream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-scripts/src/bin/decode_document.rs` around lines 79 - 86, The
current decoding chooses hex first by testing hex::decode(&args.doc_bytes)
before base64, causing base64 payloads composed of hex-safe chars (e.g.,
"414243") to be mis-decoded; update the CLI parsing (where args.doc_bytes is
defined) to add an explicit --encoding flag (enum like
Encoding::Auto|Hex|Base64, default Auto) and change the decoding logic in this
block to: if args.encoding == Encoding::Hex -> try hex::decode, else if
Encoding::Base64 -> try base64::engine::general_purpose::STANDARD.decode, else
(Auto) try base64 first and fall back to hex or validate decoded bytes
downstream; ensure error messages reference args.doc_bytes and the selected
encoding when failing.

Comment on lines +109 to +112
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 'format_ts|as i64|from_timestamp' packages/rs-scripts/src/bin/decode_document.rs

Repository: 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.rs

Repository: dashpay/platform

Length of output: 381


🌐 Web query:

chrono DateTime::from_timestamp i64 range limits documentation

💡 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 u64 to i64 on line 110 can theoretically overflow for very large values. While the existing error handling via match dt { Some(_) => ..., None => ... } catches out-of-range timestamps from the chrono library, using i64::try_from(ms / 1000) makes the intent of validated conversion explicit and avoids relying on implicit wrapping behavior.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
fn format_ts(ms: u64) -> String {
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);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-scripts/src/bin/decode_document.rs` around lines 109 - 112,
Replace the unchecked cast in format_ts by performing a checked conversion for
seconds: compute secs_u64 = ms / 1000 and use i64::try_from(secs_u64) to obtain
secs; on Err, return the same fallback used when
chrono::DateTime::from_timestamp returns None (preserve the existing None branch
behavior) so out-of-range values are handled explicitly rather than by implicit
wrapping; keep the nanos computation and the rest of the dt handling unchanged.

Copy link
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Clean, well-structured CLI debugging tool for decoding platform documents. All agent findings verified against source code. No truly blocking issues for a developer utility — the main concerns are the hex-first decode ambiguity (can silently produce wrong bytes), use of PlatformVersion::latest() (can misinterpret historical documents due to config version differences), and inconsistent error handling (mix of panic and graceful exit).

Reviewed commit: d092420

🟡 3 suggestion(s) | 💬 3 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-scripts/src/bin/decode_document.rs`:
- [SUGGESTION] lines 79-86: Hex-first decode ordering silently misinterprets some base64 inputs
  The code tries `hex::decode` before `base64::decode`. Any base64 string that consists entirely of hex characters (0-9, a-f, A-F) with even length will be silently decoded as hex, producing different (wrong) bytes. Examples: `deadbeef`, `41414141`, `cafe0000`. Since this is a debugging tool, silently producing wrong bytes is particularly harmful — the user may not realize the output is wrong.

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 `--format hex|base64` flag.
- [SUGGESTION] line 68: PlatformVersion::latest() can cause historical documents to decode incorrectly
  Verified: `load_system_data_contract` returns different contracts for different platform versions. Tests in `rs-dpp/src/system_data_contracts.rs` confirm that v8 TokenHistory != v9 TokenHistory and v1 Withdrawals != v9 Withdrawals. The difference is in `DataContractConfig` version (v8 uses config v0, v9+ uses config v1 with `sized_integer_types` enabled), which affects serialization format.

A document serialized under platform v8 may fail or be misinterpreted when deserialized with a v9+ contract config loaded via `PlatformVersion::latest()`. For a debugging tool, this is a meaningful limitation worth addressing.
- [SUGGESTION] lines 72-89: User-facing errors should not panic — use consistent error handling
  Verified: Lines 73, 77, and 89 use `.expect()` which panics with a stack trace on failure, while `resolve_system_contract` (lines 49-52, 61-62) and the byte-decoding fallback (lines 84-85) use `eprintln!` + `exit(1)` for clean error messages.

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 `.expect()` calls produce confusing panic output (with backtrace) for normal usage errors.

Comment on lines +79 to +86
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);
};
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 hex::decode before base64::decode. Any base64 string that consists entirely of hex characters (0-9, a-f, A-F) with even length will be silently decoded as hex, producing different (wrong) bytes. Examples: deadbeef, 41414141, cafe0000. Since this is a debugging tool, silently producing wrong bytes is particularly harmful — the user may not realize the output is wrong.

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 --format hex|base64 flag.

source: ['claude', 'codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-scripts/src/bin/decode_document.rs`:
- [SUGGESTION] lines 79-86: Hex-first decode ordering silently misinterprets some base64 inputs
  The code tries `hex::decode` before `base64::decode`. Any base64 string that consists entirely of hex characters (0-9, a-f, A-F) with even length will be silently decoded as hex, producing different (wrong) bytes. Examples: `deadbeef`, `41414141`, `cafe0000`. Since this is a debugging tool, silently producing wrong bytes is particularly harmful — the user may not realize the output is wrong.

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 `--format hex|base64` flag.

fn main() {
let args = Args::parse();

let platform_version = PlatformVersion::latest();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: PlatformVersion::latest() can cause historical documents to decode incorrectly

Verified: load_system_data_contract returns different contracts for different platform versions. Tests in rs-dpp/src/system_data_contracts.rs confirm that v8 TokenHistory != v9 TokenHistory and v1 Withdrawals != v9 Withdrawals. The difference is in DataContractConfig version (v8 uses config v0, v9+ uses config v1 with sized_integer_types enabled), which affects serialization format.

A document serialized under platform v8 may fail or be misinterpreted when deserialized with a v9+ contract config loaded via PlatformVersion::latest(). For a debugging tool, this is a meaningful limitation worth addressing.

source: ['claude', 'codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-scripts/src/bin/decode_document.rs`:
- [SUGGESTION] line 68: PlatformVersion::latest() can cause historical documents to decode incorrectly
  Verified: `load_system_data_contract` returns different contracts for different platform versions. Tests in `rs-dpp/src/system_data_contracts.rs` confirm that v8 TokenHistory != v9 TokenHistory and v1 Withdrawals != v9 Withdrawals. The difference is in `DataContractConfig` version (v8 uses config v0, v9+ uses config v1 with `sized_integer_types` enabled), which affects serialization format.

A document serialized under platform v8 may fail or be misinterpreted when deserialized with a v9+ contract config loaded via `PlatformVersion::latest()`. For a debugging tool, this is a meaningful limitation worth addressing.

Comment on lines +72 to +89
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");
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 .expect() which panics with a stack trace on failure, while resolve_system_contract (lines 49-52, 61-62) and the byte-decoding fallback (lines 84-85) use eprintln! + exit(1) for clean error messages.

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 .expect() calls produce confusing panic output (with backtrace) for normal usage errors.

💡 Suggested change
Suggested change
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");
Replace `.expect(...)` on lines 73, 77, and 89 with `unwrap_or_else(|e| { eprintln!("...: {e}"); std::process::exit(1); })` to match the existing error handling style.

source: ['claude', 'codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-scripts/src/bin/decode_document.rs`:
- [SUGGESTION] lines 72-89: User-facing errors should not panic — use consistent error handling
  Verified: Lines 73, 77, and 89 use `.expect()` which panics with a stack trace on failure, while `resolve_system_contract` (lines 49-52, 61-62) and the byte-decoding fallback (lines 84-85) use `eprintln!` + `exit(1)` for clean error messages.

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 `.expect()` calls produce confusing panic output (with backtrace) for normal usage errors.

];

#[derive(Parser)]
#[command(name = "decode-document", about = "Decode a platform document from base64 bytes")]
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 about = "Decode a platform document from base64 bytes" but the tool accepts both base64 and hex (line 26 comment and lines 79-86 logic). The about text shown in --help output is misleading.

💡 Suggested change
Suggested change
#[command(name = "decode-document", about = "Decode a platform document from base64 bytes")]
#[command(name = "decode-document", about = "Decode a platform document from base64 or hex bytes")]

source: ['claude']

Comment on lines +12 to +21
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),
];
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 SystemDataContract enum (in packages/data-contracts/src/lib.rs) has exactly 8 variants and the SYSTEM_CONTRACTS array has 8 entries — currently in sync. However, there is no compile-time check ensuring completeness. The enum does not derive strum::EnumIter or similar.

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 match statement on SystemDataContract would give a compiler warning when variants are added.

source: ['claude']

Comment on lines +1 to +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::<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();

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}"),
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: No tests for the crate

Verified: The crate has no #[cfg(test)] module and no test files. For a small CLI debugging tool this is understandable, but resolve_system_contract and format_ts are pure functions that would benefit from basic unit tests.

source: ['claude', 'codex']

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants