From ca9139cb0210ffa5f8748c9b1e835398c927802e Mon Sep 17 00:00:00 2001 From: olatechy0 <302475381+olatechy0@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:28:39 +0100 Subject: [PATCH 1/2] feat(xdr): define a versioned JSON schema for decoded values --- CHANGELOG.md | 9 ++ README.md | 7 +- crates/devkit-cli/src/main.rs | 22 ++- crates/devkit-cli/tests/xdr_schema.rs | 39 +++++ crates/devkit-xdr/src/lib.rs | 132 +++++++++++++++-- crates/devkit-xdr/tests/schema_snapshots.rs | 133 +++++++++++++++++ .../devkit-xdr/tests/snapshots/envelope.json | 23 +++ crates/devkit-xdr/tests/snapshots/event.json | 21 +++ crates/devkit-xdr/tests/snapshots/scval.json | 7 + docs/ARCHITECTURE.md | 9 +- docs/CLI.md | 14 +- docs/PROJECT_STATUS.md | 3 +- docs/SCVAL.md | 115 +++++++++++++++ docs/XDR.md | 134 ++++++++++++++++++ 14 files changed, 637 insertions(+), 31 deletions(-) create mode 100644 crates/devkit-cli/tests/xdr_schema.rs create mode 100644 crates/devkit-xdr/tests/schema_snapshots.rs create mode 100644 crates/devkit-xdr/tests/snapshots/envelope.json create mode 100644 crates/devkit-xdr/tests/snapshots/event.json create mode 100644 crates/devkit-xdr/tests/snapshots/scval.json create mode 100644 docs/SCVAL.md create mode 100644 docs/XDR.md diff --git a/CHANGELOG.md b/CHANGELOG.md index bc7e492..39bf6cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- `devkit-xdr`: versioned decoded-XDR JSON schema (`schema_version` `1.0`) for + `ScVal`, `TransactionEnvelope`, and `ContractEvent`, an opt-in `raw_xdr` + echo (`--raw`), golden snapshots, and the `docs/XDR.md` and `docs/SCVAL.md` + schema references. + ## [0.1.0] - 2026-10-28 First tagged release. Prebuilt binaries for Linux x86_64, macOS arm64, and diff --git a/README.md b/README.md index 668be93..f58a82b 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ The core works fully **offline** and never requires AI. - `devkit-core` — shared errors, network profiles, output format. - `devkit-xdr` — decode `ScVal`, `TransactionEnvelope`, and `ContractEvent` from - base64 XDR into JSON; classify Stellar strkeys; bounded malformed-input - handling. + base64 XDR into a versioned JSON document (`schema_version` 1.0) with an + opt-in `raw_xdr` echo; classify Stellar strkeys; bounded malformed-input + handling. See [docs/XDR.md](docs/XDR.md) and [docs/SCVAL.md](docs/SCVAL.md). - `devkit-rpc` — endpoint validation, typed protocol layer, deterministic mock transport, retry policy, and a bounded HTTP JSON-RPC transport. - `devkit-contract` — non-executing WASM validation, import/export/custom-section @@ -106,6 +107,8 @@ Decode commands accept `-` to read one bounded value from stdin. Exit codes: - [Architecture](docs/ARCHITECTURE.md) - [Getting started](docs/GETTING_STARTED.md) - [CLI reference](docs/CLI.md) +- [XDR JSON schema](docs/XDR.md) +- [SCVal JSON schema](docs/SCVAL.md) - [Configuration](docs/CONFIGURATION.md) - [Implemented capabilities](docs/IMPLEMENTED_CAPABILITIES.md) - [Memory measurement](docs/MEMORY.md) diff --git a/crates/devkit-cli/src/main.rs b/crates/devkit-cli/src/main.rs index fd9e51f..b2eb619 100644 --- a/crates/devkit-cli/src/main.rs +++ b/crates/devkit-cli/src/main.rs @@ -49,6 +49,10 @@ struct Cli { #[arg(long, value_enum, default_value_t = FormatArg::Terminal, global = true)] format: FormatArg, + /// Include the raw base64 XDR input in decoded JSON output. + #[arg(long, global = true)] + raw: bool, + #[command(subcommand)] command: Command, } @@ -173,6 +177,9 @@ fn print_json_error(category: &str, message: &str) { fn run(cli: Cli) -> Result { let format: OutputFormat = cli.format.into(); + let options = xdr::DecodeOptions { + include_raw: cli.raw, + }; let config_path = cli.config.clone(); match cli.command { Command::Version => { @@ -192,20 +199,23 @@ fn run(cli: Cli) -> Result { } Command::Scval { value } => { let value = read_value(value)?; - let decoded = xdr::inspect_scval_base64(&value).map_err(runtime)?; - print_value(format, &xdr::summary("scval", &decoded)); + let decoded = + xdr::inspect_scval_base64_with_options(&value, options).map_err(runtime)?; + print_value(format, &decoded); Ok(EXIT_SUCCESS) } Command::Envelope { value } => { let value = read_value(value)?; - let decoded = xdr::inspect_envelope_base64(&value).map_err(runtime)?; - print_value(format, &xdr::summary("transaction_envelope", &decoded)); + let decoded = + xdr::inspect_envelope_base64_with_options(&value, options).map_err(runtime)?; + print_value(format, &decoded); Ok(EXIT_SUCCESS) } Command::Event { value } => { let value = read_value(value)?; - let decoded = xdr::inspect_event_base64(&value).map_err(runtime)?; - print_value(format, &xdr::summary("contract_event", &decoded)); + let decoded = + xdr::inspect_event_base64_with_options(&value, options).map_err(runtime)?; + print_value(format, &decoded); Ok(EXIT_SUCCESS) } Command::InspectWasm { path } => { diff --git a/crates/devkit-cli/tests/xdr_schema.rs b/crates/devkit-cli/tests/xdr_schema.rs new file mode 100644 index 0000000..52c1af9 --- /dev/null +++ b/crates/devkit-cli/tests/xdr_schema.rs @@ -0,0 +1,39 @@ +use std::process::{Command, Output}; + +fn run_cli(args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_stellar-foundry")) + .args(args) + .output() + .expect("run CLI") +} + +#[test] +fn scval_json_output_is_versioned() { + let output = run_cli(&["--format", "json", "scval", "AAAAAwAAACo="]); + assert!( + output.status.success(), + "CLI failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let value: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("valid JSON output"); + assert_eq!(value["schema_version"], "1.0"); + assert_eq!(value["kind"], "scval"); + assert_eq!(value["value"]["u32"], 42); + assert!(value.get("raw_xdr").is_none()); +} + +#[test] +fn raw_flag_echoes_the_base64_input() { + let output = run_cli(&["--format", "json", "--raw", "scval", "AAAAAwAAACo="]); + assert!( + output.status.success(), + "CLI failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let value: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("valid JSON output"); + assert_eq!(value["schema_version"], "1.0"); + assert_eq!(value["kind"], "scval"); + assert_eq!(value["raw_xdr"], "AAAAAwAAACo="); +} diff --git a/crates/devkit-xdr/src/lib.rs b/crates/devkit-xdr/src/lib.rs index d1ea258..94554a2 100644 --- a/crates/devkit-xdr/src/lib.rs +++ b/crates/devkit-xdr/src/lib.rs @@ -10,9 +10,25 @@ use devkit_core::DevkitError; use serde::Serialize; -use serde_json::{json, Value}; +use serde_json::Value; use stellar_xdr::{ContractEvent, Limits, ReadXdr, ScVal, TransactionEnvelope}; +/// Version of the decoded-XDR JSON schema emitted by this crate. +/// +/// The schema covers the document envelope (`schema_version`, `kind`, `value`, +/// and optional `raw_xdr`). See `docs/XDR.md` and `docs/SCVAL.md`. +pub const XDR_JSON_SCHEMA_VERSION: &str = "1.0"; + +/// Options controlling decoded-XDR output. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct DecodeOptions { + /// Include the canonical base64 XDR input as `raw_xdr`. + /// + /// Off by default so machine-readable output stays compact; the raw + /// encoding is always available behind this flag. + pub include_raw: bool, +} + /// Maximum recursion depth applied when decoding untrusted XDR. /// /// Bounds stack usage for deeply nested values. @@ -46,24 +62,81 @@ pub struct StrkeyInfo { pub value: String, } -/// Decodes a base64 `ScVal` and returns developer-friendly JSON. +/// Build a versioned decoded-XDR document. +/// +/// The envelope is the stable part of the schema: +/// +/// ```json +/// { +/// "schema_version": "1.0", +/// "kind": "scval", +/// "value": { "...": "XDR-JSON payload" }, +/// "raw_xdr": "AAAAAwAAACo=" +/// } +/// ``` +/// +/// `raw_xdr` is only present when [`DecodeOptions::include_raw`] is set. +fn document(kind: &str, value: Value, input: &str, options: DecodeOptions) -> Value { + let mut object = serde_json::Map::new(); + object.insert( + "schema_version".to_string(), + Value::String(XDR_JSON_SCHEMA_VERSION.to_string()), + ); + object.insert("kind".to_string(), Value::String(kind.to_string())); + object.insert("value".to_string(), value); + if options.include_raw { + object.insert("raw_xdr".to_string(), Value::String(input.to_string())); + } + Value::Object(object) +} + +/// Decodes a base64 `ScVal` and returns a versioned JSON document. pub fn inspect_scval_base64(input: &str) -> Result { - let value = ScVal::from_xdr_base64(input.trim(), limits()).map_err(DevkitError::decode)?; - serde_json::to_value(&value).map_err(DevkitError::decode) + inspect_scval_base64_with_options(input, DecodeOptions::default()) } -/// Decodes a base64 `TransactionEnvelope` and returns developer-friendly JSON. +/// Decodes a base64 `ScVal` with explicit output options. +pub fn inspect_scval_base64_with_options( + input: &str, + options: DecodeOptions, +) -> Result { + let input = input.trim(); + let value = ScVal::from_xdr_base64(input, limits()).map_err(DevkitError::decode)?; + let value = serde_json::to_value(&value).map_err(DevkitError::decode)?; + Ok(document("scval", value, input, options)) +} + +/// Decodes a base64 `TransactionEnvelope` and returns a versioned JSON document. pub fn inspect_envelope_base64(input: &str) -> Result { - let value = TransactionEnvelope::from_xdr_base64(input.trim(), limits()) - .map_err(DevkitError::decode)?; - serde_json::to_value(&value).map_err(DevkitError::decode) + inspect_envelope_base64_with_options(input, DecodeOptions::default()) } -/// Decodes a base64 `ContractEvent` and returns developer-friendly JSON. -pub fn inspect_event_base64(input: &str) -> Result { +/// Decodes a base64 `TransactionEnvelope` with explicit output options. +pub fn inspect_envelope_base64_with_options( + input: &str, + options: DecodeOptions, +) -> Result { + let input = input.trim(); let value = - ContractEvent::from_xdr_base64(input.trim(), limits()).map_err(DevkitError::decode)?; - serde_json::to_value(&value).map_err(DevkitError::decode) + TransactionEnvelope::from_xdr_base64(input, limits()).map_err(DevkitError::decode)?; + let value = serde_json::to_value(&value).map_err(DevkitError::decode)?; + Ok(document("transaction_envelope", value, input, options)) +} + +/// Decodes a base64 `ContractEvent` and returns a versioned JSON document. +pub fn inspect_event_base64(input: &str) -> Result { + inspect_event_base64_with_options(input, DecodeOptions::default()) +} + +/// Decodes a base64 `ContractEvent` with explicit output options. +pub fn inspect_event_base64_with_options( + input: &str, + options: DecodeOptions, +) -> Result { + let input = input.trim(); + let value = ContractEvent::from_xdr_base64(input, limits()).map_err(DevkitError::decode)?; + let value = serde_json::to_value(&value).map_err(DevkitError::decode)?; + Ok(document("contract_event", value, input, options)) } /// Classifies a Stellar strkey. @@ -85,9 +158,12 @@ pub fn describe_strkey(input: &str) -> Result { }) } -/// Builds a compact summary object for a decoded value, used by the CLI. +/// Builds a versioned document for an already-decoded value. +/// +/// Prefer the `inspect_*` functions, which decode under bounded limits. This +/// helper remains for callers that already hold a decoded value. pub fn summary(kind: &str, value: &Value) -> Value { - json!({ "kind": kind, "value": value }) + document(kind, value.clone(), "", DecodeOptions::default()) } #[cfg(test)] @@ -103,10 +179,38 @@ mod tests { fn decodes_scval_u32_roundtrip() { let encoded = roundtrip_scval(&ScVal::U32(42)); let decoded = inspect_scval_base64(&encoded).unwrap(); + assert_eq!(decoded["schema_version"], XDR_JSON_SCHEMA_VERSION); + assert_eq!(decoded["kind"], "scval"); + assert!(decoded.get("value").is_some(), "missing value: {decoded}"); let text = serde_json::to_string(&decoded).unwrap(); assert!(text.contains("42"), "unexpected json: {text}"); } + #[test] + fn raw_xdr_is_opt_in_and_preserved_verbatim() { + let encoded = roundtrip_scval(&ScVal::U32(7)); + let default = inspect_scval_base64(&encoded).unwrap(); + assert!(default.get("raw_xdr").is_none()); + + let with_raw = inspect_scval_base64_with_options( + &format!(" {encoded}\n"), + DecodeOptions { include_raw: true }, + ) + .unwrap(); + assert_eq!(with_raw["raw_xdr"], encoded); + assert_eq!(with_raw["schema_version"], XDR_JSON_SCHEMA_VERSION); + } + + #[test] + fn summary_uses_the_versioned_envelope() { + let value = serde_json::json!({ "u32": 1 }); + let summary = summary("scval", &value); + assert_eq!(summary["schema_version"], XDR_JSON_SCHEMA_VERSION); + assert_eq!(summary["kind"], "scval"); + assert_eq!(summary["value"], value); + assert!(summary.get("raw_xdr").is_none()); + } + #[test] fn decodes_scval_bool_roundtrip() { let encoded = roundtrip_scval(&ScVal::Bool(true)); diff --git a/crates/devkit-xdr/tests/schema_snapshots.rs b/crates/devkit-xdr/tests/schema_snapshots.rs new file mode 100644 index 0000000..b9434ea --- /dev/null +++ b/crates/devkit-xdr/tests/schema_snapshots.rs @@ -0,0 +1,133 @@ +//! Golden snapshots and structural validation for the versioned XDR schema. +//! +//! The snapshot files under `tests/snapshots/` are the reviewable contract for +//! schema version 1.0. If a change intentionally alters them, bump +//! `XDR_JSON_SCHEMA_VERSION` and document the migration in `docs/XDR.md`. + +use std::path::{Path, PathBuf}; + +use devkit_xdr::{ + inspect_envelope_base64_with_options, inspect_event_base64_with_options, + inspect_scval_base64_with_options, DecodeOptions, XDR_JSON_SCHEMA_VERSION, +}; +use stellar_xdr::{ + BytesM, ContractEvent, ContractEventBody, ContractEventType, ContractEventV0, ContractId, + DecoratedSignature, ExtensionPoint, Hash, Limits, Memo, MuxedAccount, Preconditions, ScVal, + SequenceNumber, Signature, SignatureHint, Transaction, TransactionEnvelope, TransactionExt, + TransactionV1Envelope, Uint256, VecM, WriteXdr, +}; + +fn snapshot_path(name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("snapshots") + .join(format!("{name}.json")) +} + +fn assert_snapshot(name: &str, value: &serde_json::Value) { + let actual = format!("{}\n", serde_json::to_string_pretty(value).unwrap()); + let path = snapshot_path(name); + if !path.exists() { + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, &actual).unwrap(); + panic!( + "created missing snapshot {}; re-run the test to compare", + path.display() + ); + } + let expected = std::fs::read_to_string(&path).unwrap(); + assert_eq!(actual, expected, "snapshot {} drifted", path.display()); +} + +fn assert_document(document: &serde_json::Value, kind: &str, raw_xdr: Option<&str>) { + let object = document.as_object().expect("document is an object"); + assert_eq!(object["schema_version"], XDR_JSON_SCHEMA_VERSION); + assert_eq!(object["kind"], kind); + assert!(object.contains_key("value"), "document has no value"); + + let allowed = if raw_xdr.is_some() { + vec!["schema_version", "kind", "value", "raw_xdr"] + } else { + vec!["schema_version", "kind", "value"] + }; + for key in object.keys() { + assert!( + allowed.contains(&key.as_str()), + "unexpected document field `{key}`" + ); + } + assert_eq!(object.get("raw_xdr").and_then(|v| v.as_str()), raw_xdr); +} + +fn sample_envelope() -> TransactionEnvelope { + TransactionEnvelope::Tx(TransactionV1Envelope { + tx: Transaction { + source_account: MuxedAccount::Ed25519(Uint256([0x22; 32])), + fee: 100, + seq_num: SequenceNumber(42), + cond: Preconditions::None, + memo: Memo::None, + operations: VecM::try_from(Vec::new()).unwrap(), + ext: TransactionExt::V0, + }, + signatures: VecM::try_from(vec![DecoratedSignature { + hint: SignatureHint([0xab; 4]), + signature: Signature(BytesM::try_from(vec![0xcd; 64]).unwrap()), + }]) + .unwrap(), + }) +} + +fn sample_event() -> ContractEvent { + ContractEvent { + ext: ExtensionPoint::V0, + contract_id: Some(ContractId(Hash([0x11; 32]))), + type_: ContractEventType::Contract, + body: ContractEventBody::V0(ContractEventV0 { + topics: VecM::try_from(vec![ScVal::Symbol( + "transfer".try_into().expect("valid symbol"), + )]) + .unwrap(), + data: ScVal::U32(7), + }), + } +} + +#[test] +fn scval_document_matches_golden_snapshot_and_schema() { + let encoded = ScVal::U32(42).to_xdr_base64(Limits::none()).unwrap(); + let document = inspect_scval_base64_with_options(&encoded, DecodeOptions::default()).unwrap(); + assert_document(&document, "scval", None); + assert_snapshot("scval", &document); +} + +#[test] +fn envelope_document_matches_golden_snapshot_and_schema() { + let encoded = sample_envelope().to_xdr_base64(Limits::none()).unwrap(); + let document = + inspect_envelope_base64_with_options(&encoded, DecodeOptions::default()).unwrap(); + assert_document(&document, "transaction_envelope", None); + assert_snapshot("envelope", &document); +} + +#[test] +fn event_document_matches_golden_snapshot_and_schema() { + let encoded = sample_event().to_xdr_base64(Limits::none()).unwrap(); + let document = inspect_event_base64_with_options(&encoded, DecodeOptions::default()).unwrap(); + assert_document(&document, "contract_event", None); + assert_snapshot("event", &document); +} + +#[test] +fn raw_xdr_is_only_present_when_requested() { + let encoded = ScVal::Bool(true).to_xdr_base64(Limits::none()).unwrap(); + let plain = inspect_scval_base64_with_options(&encoded, DecodeOptions::default()).unwrap(); + assert_document(&plain, "scval", None); + + let raw = inspect_scval_base64_with_options( + &format!("{encoded}\n"), + DecodeOptions { include_raw: true }, + ) + .unwrap(); + assert_document(&raw, "scval", Some(&encoded)); +} diff --git a/crates/devkit-xdr/tests/snapshots/envelope.json b/crates/devkit-xdr/tests/snapshots/envelope.json new file mode 100644 index 0000000..e2371a7 --- /dev/null +++ b/crates/devkit-xdr/tests/snapshots/envelope.json @@ -0,0 +1,23 @@ +{ + "kind": "transaction_envelope", + "schema_version": "1.0", + "value": { + "tx": { + "signatures": [ + { + "hint": "abababab", + "signature": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" + } + ], + "tx": { + "cond": "none", + "ext": "v0", + "fee": 100, + "memo": "none", + "operations": [], + "seq_num": "42", + "source_account": "GARCEIRCEIRCEIRCEIRCEIRCEIRCEIRCEIRCEIRCEIRCEIRCEIRCFRVX" + } + } + } +} diff --git a/crates/devkit-xdr/tests/snapshots/event.json b/crates/devkit-xdr/tests/snapshots/event.json new file mode 100644 index 0000000..b530def --- /dev/null +++ b/crates/devkit-xdr/tests/snapshots/event.json @@ -0,0 +1,21 @@ +{ + "kind": "contract_event", + "schema_version": "1.0", + "value": { + "body": { + "v0": { + "data": { + "u32": 7 + }, + "topics": [ + { + "symbol": "transfer" + } + ] + } + }, + "contract_id": "CAIRCEIRCEIRCEIRCEIRCEIRCEIRCEIRCEIRCEIRCEIRCEIRCEIRDB3V", + "ext": "v0", + "type": "contract" + } +} diff --git a/crates/devkit-xdr/tests/snapshots/scval.json b/crates/devkit-xdr/tests/snapshots/scval.json new file mode 100644 index 0000000..cd25e70 --- /dev/null +++ b/crates/devkit-xdr/tests/snapshots/scval.json @@ -0,0 +1,7 @@ +{ + "kind": "scval", + "schema_version": "1.0", + "value": { + "u32": 42 + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index bfef842..3aab909 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -67,9 +67,12 @@ Design rules: - Failures are structured, never panics. Malformed input is a first-class test case; `fixtures/malformed/` and `crates/devkit-xdr/tests/malformed_corpus.rs` cover the regression corpus. -- JSON is produced by `stellar-xdr`'s `serde` representation today. A curated, - versioned schema is future work; until then the JSON shape is documented as - **unstable**. +- JSON is emitted as a versioned document: `schema_version`, `kind`, `value`, + and an opt-in `raw_xdr`. The envelope and the frozen XDR-JSON payload rules + are specified in [`XDR.md`](XDR.md), with SCVal details in + [`SCVAL.md`](SCVAL.md). Additive changes bump the schema minor version; + breaking changes require a major bump and a migration note. +- Golden snapshots in `crates/devkit-xdr/tests/snapshots/` pin the schema. ## RPC architecture diff --git a/docs/CLI.md b/docs/CLI.md index f907175..6b8e2f7 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -27,15 +27,17 @@ Validates a Stellar strkey and reports its kind (`account`, `contract`, or ### `scval ` -Decodes a base64 XDR `ScVal` into JSON. Pass `-` to read one value from stdin, for example `printf 'AAAAAwAAACo=' | stellar-foundry --format json scval -`. +Decodes a base64 XDR `ScVal` into a versioned JSON document. Pass `-` to read one value from stdin, for example `printf 'AAAAAwAAACo=' | stellar-foundry --format json scval -`. ### `envelope ` -Decodes a base64 XDR `TransactionEnvelope` into JSON. Pass `-` to read one value from stdin. +Decodes a base64 XDR `TransactionEnvelope` into a versioned JSON document. Pass `-` to read one value from stdin. ### `event ` -Decodes a base64 XDR `ContractEvent` into JSON. Pass `-` to read one value from stdin. +Decodes a base64 XDR `ContractEvent` into a versioned JSON document. Pass `-` to read one value from stdin. + +Decoded output includes `schema_version`, `kind`, and `value`. Add the global `--raw` flag to also echo the canonical base64 input as `raw_xdr`. See [XDR.md](XDR.md) and [SCVAL.md](SCVAL.md) for the schema and migration policy. When `-` is supplied, stdin must be piped or redirected; the CLI never waits for interactive terminal input. Input is limited to 16 MiB and must be valid UTF-8. Only one value is read; streaming multiple values is not supported. @@ -86,5 +88,7 @@ The category is one of `usage`, `decode`, `encode`, `invalid_input`, `configurat ## Output stability - `terminal` output is human-oriented and may change. -- `json` output is machine-readable. Until a versioned schema is published - (tracked as an issue), treat the shape as unstable. +- `json` output for `scval`, `envelope`, and `event` is machine-readable and + versioned. Consumers must check `schema_version` (currently `1.0`) before + using the payload; see [XDR.md](XDR.md). Other JSON shapes remain + human-oriented until they gain their own versioned envelope. diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index b69f82c..3b1e75d 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -14,7 +14,8 @@ The merge commit is `aec759c`. CI and the RustSec audit are green on `main`. - `devkit-core`: structured errors, network profiles, output formats. - `devkit-xdr`: bounded decoding of base64 `ScVal`, `TransactionEnvelope`, and - `ContractEvent`; strkey classification; malformed-input regression fixtures. + `ContractEvent` into a schema-versioned JSON document (1.0) with an opt-in raw + XDR echo; strkey classification; malformed-input regression fixtures. - `stellar-foundry` CLI: `version`, `doctor`, `strkey`, `scval`, `envelope`, `event`, `endpoint`, `inspect-wasm`, `analyze-wasm`, and `rpc-health`; terminal/JSON output, SARIF output for analysis, and exit codes 0/2/3. diff --git a/docs/SCVAL.md b/docs/SCVAL.md new file mode 100644 index 0000000..bf811fb --- /dev/null +++ b/docs/SCVAL.md @@ -0,0 +1,115 @@ +# SCVal JSON schema + +This page defines the `scval` document emitted by `devkit-xdr` and the +`stellar-foundry scval` command. The shared envelope is specified in +[`XDR.md`](XDR.md); this page covers the `value` payload. + +Current schema version: **1.0**. + +## Schema + +```json +{ + "schema_version": "1.0", + "kind": "scval", + "value": { "u32": 42 } +} +``` + +- `schema_version` — `1.0`. +- `kind` — always `scval`. +- `value` — exactly one tagged `ScVal` object or unit string. +- `raw_xdr` — optional base64 input, enabled with `--raw`. + +A decoder must reject a document whose `kind` is not `scval` or whose major +schema version is not `1`. + +## Variant mapping + +| SCVal variant | JSON shape | Example | +| ------------- | ---------- | ------- | +| `Bool` | `{"bool": true}` | `{"bool":false}` | +| `Void` | `"void"` | `"void"` | +| `Error` | `{"error": {...}}` | `{"error":{"contract":1}}` | +| `U32` | `{"u32": 42}` | number | +| `I32` | `{"i32": -1}` | number | +| `U64` | `{"u64": "42"}` | string, no precision loss | +| `I64` | `{"i64": "-1"}` | string | +| `Timepoint` | `{"timepoint": "1700000000"}` | string | +| `Duration` | `{"duration": "60"}` | string | +| `U128` | `{"u128": {"hi": "...", "lo": "..."}}` | parts | +| `I128` | `{"i128": {"hi": "...", "lo": "..."}}` | parts | +| `U256` | `{"u256": {"hi_hi": "...", "hi_lo": "...", "lo_hi": "...", "lo_lo": "..."}}` | parts | +| `I256` | `{"i256": {...}}` | parts | +| `Bytes` | `{"bytes": ""}` | lowercase hex | +| `String` | `{"string": "..."}` | UTF-8 | +| `Symbol` | `{"symbol": "transfer"}` | UTF-8 | +| `Vec` | `{"vec": [ ... ]}` or `{"vec": null}` | recursive `ScVal` | +| `Map` | `{"map": [{"key": ..., "value": ...}]}` or `null` | recursive, ordered | +| `Address` | `{"address": "G..."}` / `"C..."` | strkey | +| `ContractInstance` | `{"contract_instance": {...}}` | XDR-JSON | +| `LedgerKeyContractInstance` | `"ledger_key_contract_instance"` | unit string | +| `LedgerKeyNonce` | `{"ledger_key_nonce": {...}}` | XDR-JSON | +| `ExecutableTag` | `{"executable_tag": "..."}` | string | + +The table reflects the `stellar-xdr` 28 serde mapping frozen by schema 1.0. If a +future `stellar-xdr` release adds a variant, the decoder reports a decode error +until the variant is documented here and the schema minor version is bumped. + +## Examples + +### Integer + +```console +$ stellar-foundry --format json scval AAAAAwAAACo= +{"kind":"scval","schema_version":"1.0","value":{"u32":42}} +``` + +### Symbol and vector + +```json +{ + "schema_version": "1.0", + "kind": "scval", + "value": { + "vec": [ + { "symbol": "transfer" }, + { "u32": 7 } + ] + } +} +``` + +### Map + +Maps are arrays of `key`/`value` pairs so arbitrary SCVal keys survive JSON +round-trips without string coercion: + +```json +{ + "schema_version": "1.0", + "kind": "scval", + "value": { + "map": [ + { "key": { "symbol": "admin" }, "value": { "address": "G..." } } + ] + } +} +``` + +## Recursion and limits + +- Nested `Vec`, `Map`, and contract-instance values use the same schema + recursively. +- Decoding is bounded to depth 128 and 16 MiB of XDR. Exceeding a limit returns + a structured decode error; no partial document is emitted. +- The `value` payload never contains raw binary; bytes and hashes are hex and + ids are strkeys. + +## Stability + +Within schema 1.x, existing variant tags and shapes do not change. Additive +changes (new documented variants or optional fields) bump the minor version. +Removing or renaming a tag requires schema 2.0 and a migration note in +`XDR.md`. Golden SCVal output is covered by +`crates/devkit-xdr/tests/snapshots/scval.json`. diff --git a/docs/XDR.md b/docs/XDR.md new file mode 100644 index 0000000..d05c509 --- /dev/null +++ b/docs/XDR.md @@ -0,0 +1,134 @@ +# XDR JSON schema + +`devkit-xdr` emits a versioned document for every decoded value. The envelope is +the stable contract; the `value` member is the XDR-JSON mapping defined below. + +Current schema version: **1.0** (`devkit_xdr::XDR_JSON_SCHEMA_VERSION`). + +## Document envelope + +```json +{ + "schema_version": "1.0", + "kind": "scval", + "value": { "u32": 42 } +} +``` + +| Field | Type | Presence | Meaning | +| ----- | ---- | -------- | ------- | +| `schema_version` | string | always | Schema version in `MAJOR.MINOR` form. | +| `kind` | string | always | `scval`, `transaction_envelope`, or `contract_event`. | +| `value` | any JSON | always | Decoded XDR-JSON payload (see below). | +| `raw_xdr` | string | opt-in | Canonical, trimmed base64 XDR input. | + +`raw_xdr` is omitted by default so reports stay compact. Request it with +`--raw` on the CLI or `DecodeOptions { include_raw: true }` in the library. + +```console +$ stellar-foundry --format json --raw scval AAAAAwAAACo= +{"kind":"scval","raw_xdr":"AAAAAwAAACo=","schema_version":"1.0","value":{"u32":42}} +``` + +The raw encoding is always recoverable: the input is base64 XDR and the decoder +never rewrites it. The flag only echoes it into the document. + +## Kinds + +### `scval` + +`value` is the XDR-JSON mapping of a single `ScVal`. See +[`SCVAL.md`](SCVAL.md) for the variant table and examples. + +### `transaction_envelope` + +`value` is the XDR-JSON mapping of a `TransactionEnvelope`. The top-level key +identifies the envelope variant: + +- `tx_v0` — legacy `TransactionV0Envelope` +- `tx` — `TransactionV1Envelope` (contains `tx` and `signatures`) +- `tx_fee_bump` — `FeeBumpTransactionEnvelope` + +Example (`tx` variant, abbreviated): + +```json +{ + "schema_version": "1.0", + "kind": "transaction_envelope", + "value": { + "tx": { + "tx": { + "source_account": "G...", + "fee": 100, + "seq_num": "42", + "cond": "none", + "memo": "none", + "operations": [] + }, + "signatures": [ + { "hint": "abababab", "signature": "cdcd..." } + ] + } + } +} +``` + +### `contract_event` + +`value` is the XDR-JSON mapping of a `ContractEvent`: + +| Field | Meaning | +| ----- | ------- | +| `type` | `system`, `contract`, or `diagnostic` | +| `contract_id` | strkey contract id, or `null` for system events | +| `body.v0.topics` | array of `ScVal` values, each in the `scval` schema | +| `body.v0.data` | `ScVal` payload, in the `scval` schema | +| `ext` | extension point (`v0` in schema 1.0) | + +## XDR-JSON payload rules + +The `value` payload is the deterministic `stellar-xdr` serde mapping, frozen as +part of schema 1.0. Rules that consumers may rely on: + +- Enums and union variants are tagged objects or strings using `snake_case` + names, for example `{"u32":42}`, `{"symbol":"transfer"}`, `"none"`. +- 64-bit integers use JSON strings (`"42"`), matching `stellar-xdr`'s + `NumberOrString` mapping, so no precision is lost. +- Byte arrays are lowercase hex strings; hashes and signature hints are hex. +- Account, muxed-account, and contract ids are strkeys. +- Field names use `snake_case` (`seq_num`, `source_account`). +- Maps and vectors preserve XDR order; the decoder does not reorder them. +- Undecoded or unknown future XDR variants are a decode error, not silent data + loss. + +## Versioning and migration + +- `MAJOR` changes when an existing field is removed, renamed, retyped, or + reordered in a way that breaks consumers. A major bump requires a migration + section in this file and a new snapshot set. +- `MINOR` changes are additive: new optional fields, new `kind` values, or new + documented XDR variants. Existing fields keep their names and meanings. +- Every successfully decoded document carries `schema_version`. Decode errors + are returned through the existing structured error type, not as documents. +- Unknown fields must be ignored by consumers. + +### Migrating from the unversioned output + +Before schema 1.0, CLI output was `{ "kind": ..., "value": ... }` with no +version. Migration is additive: + +1. Read `schema_version`; reject documents whose major version is not `1`. +2. Keep reading `kind` and `value` as before. +3. Treat `raw_xdr` as optional and only present when requested. + +No field was removed or renamed, so existing consumers that ignored unknown +fields continue to work. + +## Tests + +- `crates/devkit-xdr/tests/schema_snapshots.rs` validates the envelope and + compares all three kinds against committed golden files in + `crates/devkit-xdr/tests/snapshots/`. +- Unit tests assert that `raw_xdr` is opt-in and preserved verbatim. +- Changing a snapshot requires updating this document and bumping the schema + version when the change is not additive. From d4b1378fee14e275873186eb8ec3ee9589deebf8 Mon Sep 17 00:00:00 2001 From: olatechy0 <302475381+olatechy0@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:36:38 +0100 Subject: [PATCH 2/2] feat: add testnet demo and crates.io publishing setup Add a reproducible demo that decodes a captured testnet envelope and ScVal, inspects a minimal WASM module, emits SARIF, and optionally checks testnet RPC health. Add a Publish workflow with a dry-run mode, per-crate metadata, and PUBLISHING.md. Publishing requires a CARGO_REGISTRY_TOKEN secret. --- .github/workflows/publish.yml | 56 ++++++++++++++++ README.md | 20 ++++++ crates/devkit-analysis/Cargo.toml | 4 ++ crates/devkit-cli/Cargo.toml | 4 ++ crates/devkit-config/Cargo.toml | 4 ++ crates/devkit-contract/Cargo.toml | 4 ++ crates/devkit-core/Cargo.toml | 4 ++ crates/devkit-rpc/Cargo.toml | 4 ++ crates/devkit-xdr/Cargo.toml | 4 ++ demo/README.md | 100 +++++++++++++++++++++++++++++ demo/fixtures/minimal.wasm | Bin 0 -> 8 bytes demo/fixtures/testnet-envelope.xdr | 1 + demo/fixtures/testnet-scval.xdr | 1 + demo/run-demo.ps1 | 40 ++++++++++++ demo/run-demo.sh | 63 ++++++++++++++++++ docs/BUILD_REPORT.md | 5 +- docs/DRIPS_APPLICATION_DRAFT.md | 4 +- docs/DRIPS_READINESS.md | 3 +- docs/FINAL_AUDIT.md | 4 +- docs/PROJECT_STATUS.md | 4 +- docs/PUBLISHING.md | 86 +++++++++++++++++++++++++ 21 files changed, 407 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 demo/README.md create mode 100644 demo/fixtures/minimal.wasm create mode 100644 demo/fixtures/testnet-envelope.xdr create mode 100644 demo/fixtures/testnet-scval.xdr create mode 100644 demo/run-demo.ps1 create mode 100644 demo/run-demo.sh create mode 100644 docs/PUBLISHING.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..63c87ac --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,56 @@ +name: Publish + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Package only; do not publish to crates.io" + required: true + type: boolean + default: true + +permissions: + contents: read + +concurrency: + group: publish + cancel-in-progress: false + +jobs: + publish: + name: Publish crates + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable + with: + toolchain: stable + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 + with: + workspaces: "." + + - name: Package workspace + run: cargo package --workspace --no-verify + + - name: Publish crates in dependency order + if: inputs.dry_run == false + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + set -euo pipefail + + publish() { + echo "Publishing $1" + cargo publish -p "$1" --locked + # Give the crates.io index time to expose the new version before + # dependent crates resolve it. + sleep 30 + } + + publish devkit-core + publish devkit-config + publish devkit-xdr + publish devkit-rpc + publish devkit-contract + publish devkit-analysis + publish devkit-cli diff --git a/README.md b/README.md index f58a82b..48b3a8a 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,24 @@ stellar-foundry rpc-health https://soroban-testnet.stellar.org Decode commands accept `-` to read one bounded value from stdin. Exit codes: `0` success, `2` usage error, `3` runtime/decode error. +## Demo + +Run a short tour with captured Soroban testnet data: + +```bash +bash demo/run-demo.sh +``` + +On Windows: + +```powershell +pwsh demo/run-demo.ps1 +``` + +The demo decodes a real testnet transaction and event value, inspects a minimal +WASM module, emits SARIF, and optionally checks testnet RPC health. See +[demo/README.md](demo/README.md). + ## Scope and limitations - `contractspecv0` entries are decoded into XDR-JSON definitions with a variant @@ -111,6 +129,8 @@ Decode commands accept `-` to read one bounded value from stdin. Exit codes: - [SCVal JSON schema](docs/SCVAL.md) - [Configuration](docs/CONFIGURATION.md) - [Implemented capabilities](docs/IMPLEMENTED_CAPABILITIES.md) +- [Testnet demo](demo/README.md) +- [Publishing to crates.io](docs/PUBLISHING.md) - [Memory measurement](docs/MEMORY.md) - [Testing guide](docs/TESTING.md) - [Project status](docs/PROJECT_STATUS.md) diff --git a/crates/devkit-analysis/Cargo.toml b/crates/devkit-analysis/Cargo.toml index 7ca0c4f..41911f7 100644 --- a/crates/devkit-analysis/Cargo.toml +++ b/crates/devkit-analysis/Cargo.toml @@ -5,6 +5,10 @@ edition.workspace = true license.workspace = true authors.workspace = true repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/devkit-analysis" +keywords = ["stellar", "soroban", "sarif", "static-analysis"] +categories = ["development-tools"] description = "Deterministic structural security checks and SARIF output for Soroban WASM." [dependencies] diff --git a/crates/devkit-cli/Cargo.toml b/crates/devkit-cli/Cargo.toml index e886cfa..ba87334 100644 --- a/crates/devkit-cli/Cargo.toml +++ b/crates/devkit-cli/Cargo.toml @@ -5,6 +5,10 @@ edition.workspace = true license.workspace = true authors.workspace = true repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/devkit-cli" +keywords = ["stellar", "soroban", "cli", "developer-tools"] +categories = ["command-line-utilities", "development-tools"] description = "The `stellar-foundry` command-line interface." [[bin]] diff --git a/crates/devkit-config/Cargo.toml b/crates/devkit-config/Cargo.toml index db1f4d2..dfd62ed 100644 --- a/crates/devkit-config/Cargo.toml +++ b/crates/devkit-config/Cargo.toml @@ -5,6 +5,10 @@ edition.workspace = true license.workspace = true authors.workspace = true repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/devkit-config" +keywords = ["stellar", "soroban", "config", "toml"] +categories = ["config", "development-tools"] description = "Validated configuration and secret-safe diagnostics for StellarFoundry DevKit." [dependencies] diff --git a/crates/devkit-contract/Cargo.toml b/crates/devkit-contract/Cargo.toml index 8dce73c..73646d6 100644 --- a/crates/devkit-contract/Cargo.toml +++ b/crates/devkit-contract/Cargo.toml @@ -5,6 +5,10 @@ edition.workspace = true license.workspace = true authors.workspace = true repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/devkit-contract" +keywords = ["stellar", "soroban", "wasm", "contracts"] +categories = ["development-tools", "wasm", "parser-implementations"] description = "Bounded, non-executing inspection of Soroban contract WASM artifacts." [dependencies] diff --git a/crates/devkit-core/Cargo.toml b/crates/devkit-core/Cargo.toml index a600360..eda0517 100644 --- a/crates/devkit-core/Cargo.toml +++ b/crates/devkit-core/Cargo.toml @@ -5,6 +5,10 @@ edition.workspace = true license.workspace = true authors.workspace = true repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/devkit-core" +keywords = ["stellar", "soroban", "devkit", "developer-tools"] +categories = ["development-tools"] description = "Core types, errors, configuration, and network profiles for StellarFoundry DevKit." [dependencies] diff --git a/crates/devkit-rpc/Cargo.toml b/crates/devkit-rpc/Cargo.toml index d2bf3c9..d1437a1 100644 --- a/crates/devkit-rpc/Cargo.toml +++ b/crates/devkit-rpc/Cargo.toml @@ -5,6 +5,10 @@ edition.workspace = true license.workspace = true authors.workspace = true repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/devkit-rpc" +keywords = ["stellar", "soroban", "rpc", "json-rpc"] +categories = ["development-tools", "network-programming", "api-bindings"] description = "Typed Stellar RPC protocol layer with an injectable transport for deterministic tests." [dependencies] diff --git a/crates/devkit-xdr/Cargo.toml b/crates/devkit-xdr/Cargo.toml index 32ceca3..f65b7a1 100644 --- a/crates/devkit-xdr/Cargo.toml +++ b/crates/devkit-xdr/Cargo.toml @@ -5,6 +5,10 @@ edition.workspace = true license.workspace = true authors.workspace = true repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/devkit-xdr" +keywords = ["stellar", "soroban", "xdr", "scval"] +categories = ["development-tools", "encoding"] description = "XDR, SCVal, transaction, and event inspection utilities for StellarFoundry DevKit." [dependencies] diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..d053978 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,100 @@ +# Testnet demo + +A short, reproducible tour of DevKit using captured Soroban testnet data. The +decode and WASM steps are fully offline; only the final `rpc-health` step uses +the network. + +## Prerequisites + +- Rust 1.91 or newer, **or** a `stellar-foundry` binary on `PATH` +- Network access only for `rpc-health` + +## Run + +```bash +bash demo/run-demo.sh +``` + +or on Windows: + +```powershell +pwsh demo/run-demo.ps1 +``` + +Skip the live RPC step: + +```bash +SKIP_NETWORK=1 bash demo/run-demo.sh +``` + +```powershell +$env:SKIP_NETWORK = "1"; pwsh demo/run-demo.ps1 +``` + +## What it shows + +1. `version` and the tool identity. +2. `scval` decoding a real testnet event value. +3. `envelope` decoding a real testnet payment transaction. +4. `inspect-wasm` on a minimal valid WASM module. +5. `analyze-wasm --format sarif` emitting SARIF 2.1.0. +6. `rpc-health` against Soroban testnet (optional). + +## Expected output (abridged) + +```json +{"kind":"scval","schema_version":"1.0","value":{"i128":"100"}} +``` + +```json +{ + "kind": "transaction_envelope", + "schema_version": "1.0", + "value": { + "tx": { + "tx": { + "fee": 100, + "operations": [ + { + "body": { + "payment": { + "amount": "1", + "asset": "native", + "destination": "GD64X2WOT23F3INTH3IXP4B4YGEPGPW6ZGXWQSGTFNQPUCGRN7WWAKQS" + } + } + } + ], + "source_account": "GD64X2WOT23F3INTH3IXP4B4YGEPGPW6ZGXWQSGTFNQPUCGRN7WWAKQS" + } + } + } +} +``` + +```json +{ + "contract_spec_bytes": 0, + "contract_spec_entries": [], + "custom_sections": [], + "exports": [], + "has_contract_spec": false, + "imports": [], + "wasm_bytes": 8 +} +``` + +`analyze-wasm` emits a SARIF document with rule `DEVKIT-WASM-001` for the +missing `contractspecv0` section. This is a structural observation, not a +vulnerability finding. + +## Fixtures + +| File | Source | +| ---- | ------ | +| `fixtures/testnet-envelope.xdr` | Soroban testnet ledger 5434653, tx `ff4ec67e7123f35ba33a899195b035691f4fdc4a52a740a66b9710c0ddac9d97`, captured 2026-10-28 | +| `fixtures/testnet-scval.xdr` | `getEvents` value from contract `CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC`, tx `1c3d38c13a4508499bc8926a1012fb46f7bdd05e79b2b6e32c76b254ae3b4538`, ledger 5434564, i128 `100` | +| `fixtures/minimal.wasm` | A minimal valid 8-byte WASM module (`\0asm` + version 1) | + +These are captured inputs, not live network calls. Re-capture them if the +testnet history window no longer contains the referenced ledgers. diff --git a/demo/fixtures/minimal.wasm b/demo/fixtures/minimal.wasm new file mode 100644 index 0000000000000000000000000000000000000000..d8fc92d022fbf4d1072da17bc8e0840054b51ddc GIT binary patch literal 8 PcmZQbEY4+QU|;|M2ZjMd literal 0 HcmV?d00001 diff --git a/demo/fixtures/testnet-envelope.xdr b/demo/fixtures/testnet-envelope.xdr new file mode 100644 index 0000000..716304d --- /dev/null +++ b/demo/fixtures/testnet-envelope.xdr @@ -0,0 +1 @@ +AAAAAgAAAAD9y+rOnrZdobM+0XfwPMGI8z7eya9oSNMrYPoI0W/tYAAAAGQAA+0HAAEdpAAAAAAAAAADSwJMos6mZdzlGmvZGohWJYTkmRDY5wDO2SSPgABKK1oAAAABAAAAAAAAAAEAAAAA/cvqzp62XaGzPtF38DzBiPM+3smvaEjTK2D6CNFv7WAAAAAAAAAAAAAAAAEAAAAAAAAAAdFv7WAAAABAFAB//EPQU/wtiIQ3jaNC+M5hGiTi2sxlc1f/VEGo14VxjwK6yrnsXfyKjqDZKoBETLEc93HRMnhX2KSh+dS5Bg== diff --git a/demo/fixtures/testnet-scval.xdr b/demo/fixtures/testnet-scval.xdr new file mode 100644 index 0000000..84eb537 --- /dev/null +++ b/demo/fixtures/testnet-scval.xdr @@ -0,0 +1 @@ +AAAACgAAAAAAAAAAAAAAAAAAAGQ= diff --git a/demo/run-demo.ps1 b/demo/run-demo.ps1 new file mode 100644 index 0000000..d4535be --- /dev/null +++ b/demo/run-demo.ps1 @@ -0,0 +1,40 @@ +$ErrorActionPreference = "Stop" + +Set-Location (Join-Path $PSScriptRoot "..") + +function Invoke-Devkit { + param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]] $Arguments + ) + + if (Get-Command stellar-foundry -ErrorAction SilentlyContinue) { + & stellar-foundry @Arguments + } else { + & cargo run -q -p devkit-cli -- @Arguments + } +} + +Write-Host "== version" +Invoke-Devkit version + +Write-Host "`n== scval (testnet event value)" +$scval = (Get-Content demo/fixtures/testnet-scval.xdr -Raw).Trim() +Invoke-Devkit scval --format json $scval + +Write-Host "`n== envelope (testnet payment)" +$envelope = (Get-Content demo/fixtures/testnet-envelope.xdr -Raw).Trim() +Invoke-Devkit envelope --format json $envelope + +Write-Host "`n== inspect-wasm (minimal module)" +Invoke-Devkit inspect-wasm demo/fixtures/minimal.wasm + +Write-Host "`n== analyze-wasm (SARIF)" +Invoke-Devkit analyze-wasm --format sarif demo/fixtures/minimal.wasm + +if ($env:SKIP_NETWORK -ne "1") { + Write-Host "`n== rpc-health (network)" + Invoke-Devkit rpc-health https://soroban-testnet.stellar.org +} else { + Write-Host "`nSKIP_NETWORK=1; skipping rpc-health" +} diff --git a/demo/run-demo.sh b/demo/run-demo.sh new file mode 100644 index 0000000..84ed3fd --- /dev/null +++ b/demo/run-demo.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +CARGO_BIN="${CARGO:-}" +if [ -z "$CARGO_BIN" ]; then + if command -v cargo >/dev/null 2>&1; then + CARGO_BIN="cargo" + elif [ -x "$HOME/.cargo/bin/cargo" ]; then + CARGO_BIN="$HOME/.cargo/bin/cargo" + elif [ -x "${USERPROFILE:-$HOME}/.cargo/bin/cargo.exe" ]; then + CARGO_BIN="${USERPROFILE:-$HOME}/.cargo/bin/cargo.exe" + elif command -v cmd.exe >/dev/null 2>&1; then + CARGO_WIN="$(cmd.exe /c where cargo 2>/dev/null | head -n 1 | tr -d '\r')" + if [ -n "$CARGO_WIN" ]; then + if command -v cygpath >/dev/null 2>&1; then + CARGO_BIN="$(cygpath -u "$CARGO_WIN")" + else + CARGO_BIN="$CARGO_WIN" + fi + fi + fi +fi + +run() { + if command -v stellar-foundry >/dev/null 2>&1; then + stellar-foundry "$@" + elif [ -n "$CARGO_BIN" ]; then + "$CARGO_BIN" run -q -p devkit-cli -- "$@" + else + echo "error: neither stellar-foundry nor cargo was found on PATH" >&2 + exit 1 + fi +} + +echo "== version" +run version + +echo +echo "== scval (testnet event value)" +run scval --format json "$(tr -d '\n' < demo/fixtures/testnet-scval.xdr)" + +echo +echo "== envelope (testnet payment)" +run envelope --format json "$(tr -d '\n' < demo/fixtures/testnet-envelope.xdr)" + +echo +echo "== inspect-wasm (minimal module)" +run inspect-wasm demo/fixtures/minimal.wasm + +echo +echo "== analyze-wasm (SARIF)" +run analyze-wasm --format sarif demo/fixtures/minimal.wasm + +if [ "${SKIP_NETWORK:-0}" != "1" ]; then + echo + echo "== rpc-health (network)" + run rpc-health https://soroban-testnet.stellar.org +else + echo + echo "SKIP_NETWORK=1; skipping rpc-health" +fi diff --git a/docs/BUILD_REPORT.md b/docs/BUILD_REPORT.md index aaeae68..dbca01a 100644 --- a/docs/BUILD_REPORT.md +++ b/docs/BUILD_REPORT.md @@ -37,7 +37,7 @@ See [ARCHITECTURE.md](ARCHITECTURE.md). ## Tests -43 tests across unit, integration, malformed-corpus, and CLI tests. Run with +51 tests across unit, integration, malformed-corpus, and CLI tests. Run with `cargo test --locked --workspace --all-features`. CI runs the same suite on Linux, Windows, and macOS, plus an MSRV job on Rust 1.91.0. @@ -73,7 +73,8 @@ x86_64 archives plus SHA256 checksums: -crates.io publication is still pending. +crates.io publication is prepared but still pending; see +[PUBLISHING.md](PUBLISHING.md). ## Issue backlog diff --git a/docs/DRIPS_APPLICATION_DRAFT.md b/docs/DRIPS_APPLICATION_DRAFT.md index 6894fce..c3147ca 100644 --- a/docs/DRIPS_APPLICATION_DRAFT.md +++ b/docs/DRIPS_APPLICATION_DRAFT.md @@ -89,8 +89,8 @@ available. artifact exists and code-scanning permissions are available. - RPC configuration is wired into `rpc-health`; other commands do not consume RPC settings. -- crates.io publication is still pending; install from the GitHub Release or - build from source. +- crates.io publication is prepared but still pending; install from the GitHub + Release or build from source. See `docs/PUBLISHING.md`. - External adoption is limited; no independent external contributor has merged a PR yet. diff --git a/docs/DRIPS_READINESS.md b/docs/DRIPS_READINESS.md index 594bf7f..6fd7fdf 100644 --- a/docs/DRIPS_READINESS.md +++ b/docs/DRIPS_READINESS.md @@ -74,7 +74,8 @@ exact scope. - RPC config is wired into `rpc-health`; other commands do not use RPC settings. Endpoint precedence is command line, then environment, then config file. - `v0.1.0` is released with Linux, macOS, and Windows binaries and checksums; - crates.io publication is still pending. + crates.io publication is prepared but still pending (needs a + `CARGO_REGISTRY_TOKEN` secret). - The project has limited external adoption: no external contributors have merged PRs yet, and the repository is new. Do not present issue counts or labels as evidence of traction. diff --git a/docs/FINAL_AUDIT.md b/docs/FINAL_AUDIT.md index 4f7924c..7f3c924 100644 --- a/docs/FINAL_AUDIT.md +++ b/docs/FINAL_AUDIT.md @@ -9,7 +9,7 @@ Audit date: 2026-10-28. Commit: `aec759c`. | Item | Status | Evidence | | ---- | ------ | -------- | | Build succeeds | Verified | `cargo build` | -| Tests pass | Verified | `cargo test --locked --workspace --all-features` (43 tests); post-merge CI | +| Tests pass | Verified | `cargo test --locked --workspace --all-features` (51 tests); post-merge CI | | Lint passes | Verified | `cargo clippy --locked --all-targets --all-features -- -D warnings` | | Formatting passes | Verified | `cargo fmt --all -- --check` | | CI passes | Verified | CI run 40690141880 on Linux/Windows/macOS | @@ -27,7 +27,7 @@ Audit date: 2026-10-28. Commit: `aec759c`. | Performance measured | Partial | Memory measurement example; benchmarks pending | | Composite GitHub Action | Pending | Tracked as an issue; only manual SARIF upload exists | | Tagged release + binaries | Verified | `v0.1.0` with Linux/macOS/Windows archives and SHA256 checksums | -| crates.io publication | Pending | No crates.io package yet | +| crates.io publication | Pending | Publish workflow and docs ready; requires `CARGO_REGISTRY_TOKEN` | | External contributors | Pending | No independent external contributor has merged a PR yet | ## Notes diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 3b1e75d..40c3dcd 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -55,7 +55,9 @@ checksums: - https://github.com/StellarFoundry/stellar-devkit/releases/tag/v0.1.0 -crates.io publication remains outstanding. +crates.io publication is prepared but not executed: the `Publish` workflow and +`docs/PUBLISHING.md` are ready, and the repository needs a +`CARGO_REGISTRY_TOKEN` secret. ## Remaining limitations diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md new file mode 100644 index 0000000..bf6969e --- /dev/null +++ b/docs/PUBLISHING.md @@ -0,0 +1,86 @@ +# Publishing to crates.io + +The workspace is packaged but **not yet published** to crates.io. Publishing is +irreversible: a published version cannot be overwritten, only yanked. Read this +page before running the workflow. + +## Prerequisites + +1. A crates.io account with a verified email. +2. A crates.io API token with the `publish-new` and `publish-update` scopes. +3. The token stored as the `CARGO_REGISTRY_TOKEN` repository secret: + **Settings → Secrets and variables → Actions → New repository secret**. +4. The version in `[workspace.package]` (`Cargo.toml`) and `CHANGELOG.md` must + agree, and the working tree must be clean. + +## Dry run + +Run the **Publish** workflow with `dry_run = true` (the default). It executes: + +```bash +cargo package --workspace --no-verify +``` + +This produces `.crate` archives under `target/package/` without contacting the +registry. To inspect a package: + +```bash +cargo package -p devkit-core --no-verify +tar -tf target/package/devkit-core-0.1.0.crate +``` + +## Publish order + +Crates must be published after their dependencies are visible on crates.io: + +1. `devkit-core` +2. `devkit-config` +3. `devkit-xdr` +4. `devkit-rpc` +5. `devkit-contract` +6. `devkit-analysis` +7. `devkit-cli` + +The workflow waits 30 seconds between publishes so the crates.io index can +expose each version before the next crate resolves it. + +## Manual fallback + +If the workflow cannot run, use the same order locally: + +```bash +cargo login +cargo publish -p devkit-core --locked +sleep 30 +cargo publish -p devkit-config --locked +sleep 30 +cargo publish -p devkit-xdr --locked +sleep 30 +cargo publish -p devkit-rpc --locked +sleep 30 +cargo publish -p devkit-contract --locked +sleep 30 +cargo publish -p devkit-analysis --locked +sleep 30 +cargo publish -p devkit-cli --locked +``` + +## After publishing + +1. Confirm every crate appears on crates.io. +2. Install from crates.io: + ```bash + cargo install devkit-cli --version 0.1.0 + stellar-foundry version + ``` +3. Update `README.md`, `docs/PROJECT_STATUS.md`, and `CHANGELOG.md` to say the + crates are published. +4. Do not delete the `v0.1.0` tag or the GitHub Release. + +## Notes + +- The crate names `devkit-*` are currently unclaimed on crates.io. +- Do not publish from a dirty working tree; the workflow uses the checked-out + commit. +- If a publish fails partway through, fix the cause and continue from the failed + crate; already-published crates stay published.