cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 8c7399b3a322fb837c218d7c89952f0933b427b6
parent d3f18e4000ce1cd733321bde1a06063a4004a2fe
Author: triesap <tyson@radroots.org>
Date:   Tue, 14 Jul 2026 11:12:43 +0000

cli: align with sealed event runtime APIs

- replace direct event envelope construction with typed constructors

- read sealed draft and event data through accessors

- update proxy publish tests for raw signed event JSON requests

- preserve u64 event timestamps in CLI output views

Diffstat:
MCargo.lock | 1+
Msrc/ops/exec/basket.rs | 7++++---
Msrc/runtime/farm.rs | 8++++----
Msrc/runtime/listing.rs | 67+++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/runtime/order.rs | 16+++++++++-------
Msrc/runtime/sync.rs | 11+++++++----
Msrc/runtime/validation_receipt.rs | 40++++++++++++++++++++--------------------
Msrc/view/runtime.rs | 2+-
Mtests/support/mod.rs | 27+++++++++++++++------------
Mtests/target_cli.rs | 37++++++++++++++++++++-----------------
10 files changed, 126 insertions(+), 90 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3671,6 +3671,7 @@ version = "0.1.0-alpha.2" dependencies = [ "hex", "radroots_core", + "secp256k1", "serde", "serde_json", "sha2", diff --git a/src/ops/exec/basket.rs b/src/ops/exec/basket.rs @@ -1315,9 +1315,9 @@ fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError { mod tests { use std::path::{Path, PathBuf}; - use radroots_event::RadrootsEventEnvelope; use radroots_event::ids::RadrootsListingAddress; use radroots_event::kinds::{KIND_FARM, KIND_LISTING}; + use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts}; use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event}; use radroots_secret_vault::RadrootsSecretBackend; use radroots_sql_core::{SqlExecutor, SqliteExecutor}; @@ -1770,7 +1770,7 @@ mod tests { fn seed_current_listing(config: &RuntimeConfig) { crate::runtime::store::init(config).expect("store init"); let (seller_pubkey, listing_id) = listing_addr_parts(LISTING_ADDR); - let event = RadrootsEventEnvelope { + let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: "2".repeat(64), author: seller_pubkey.clone(), created_at: 1, @@ -1818,7 +1818,8 @@ mod tests { ], content: "# Market Eggs".to_owned(), sig: "f".repeat(128), - }; + }) + .expect("seed listing event"); let executor = SqliteExecutor::open(&config.local.replica_store_path).expect("open replica"); assert_eq!( diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs @@ -1180,10 +1180,10 @@ fn sdk_enqueued_publish_view( fn sdk_plan_event_view(plan: &FarmPublishPlan) -> FarmPublishEventView { FarmPublishEventView { - kind: plan.frozen_draft.kind, - author: plan.frozen_draft.expected_pubkey.clone(), - content: plan.frozen_draft.content.clone(), - tags: plan.frozen_draft.tags.clone(), + kind: plan.frozen_draft.kind_u32(), + author: plan.frozen_draft.expected_pubkey_str().to_owned(), + content: plan.frozen_draft.content().to_owned(), + tags: plan.frozen_draft.tags_as_vec(), event_id: Some(plan.expected_event_id.as_str().to_owned()), event_addr: Some(plan.farm_addr.as_str().to_owned()), } diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs @@ -10,7 +10,6 @@ use radroots_core::{ RadrootsCoreDiscountThreshold, RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCorePercent, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; -use radroots_event::RadrootsEventEnvelope; use radroots_event::contract::RadrootsActorRole; use radroots_event::farm::RadrootsFarmRef; use radroots_event::ids::{RadrootsDTag, RadrootsInventoryBinId}; @@ -21,6 +20,7 @@ use radroots_event::listing::{ RadrootsListingStatus, }; use radroots_event::trade_validation::RadrootsTradeValidationListingError; +use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts}; use radroots_event_codec::d_tag::is_d_tag_base64url; use radroots_event_codec::listing::encode::to_wire_parts_with_kind; use radroots_replica_store::ReplicaSql; @@ -588,14 +588,20 @@ pub fn validate( issue, )); } - let event = RadrootsEventEnvelope { - id: String::new(), - author: canonical.seller_pubkey.clone(), - created_at: 0, - kind: KIND_LISTING_DRAFT, - tags: parts.tags, - content: parts.content, - sig: String::new(), + let event = match listing_validation_event(&canonical, parts) { + Ok(event) => event, + Err(error) => { + return Ok(invalid_validation_view( + args.file.as_path(), + &parsed, + &context, + ListingValidationIssueView { + field: "listing".to_owned(), + message: format!("invalid listing event envelope: {error}"), + line: None, + }, + )); + } }; match validate_listing_event(&event) { Ok(_) => Ok(ListingValidateView { @@ -1157,14 +1163,15 @@ fn listing_ready_issues( }]; } }; - let event = RadrootsEventEnvelope { - id: String::new(), - author: canonical.seller_pubkey.clone(), - created_at: 0, - kind: KIND_LISTING_DRAFT, - tags: parts.tags, - content: parts.content, - sig: String::new(), + let event = match listing_validation_event(canonical, parts) { + Ok(event) => event, + Err(error) => { + return vec![ListingValidationIssueView { + field: "listing".to_owned(), + message: format!("invalid listing event envelope: {error}"), + line: None, + }]; + } }; match validate_listing_event(&event) { Ok(_) => Vec::new(), @@ -1947,17 +1954,33 @@ fn sdk_enqueued_publish_view( fn sdk_plan_event_view(plan: &ListingPublishPlan) -> ListingMutationEventView { ListingMutationEventView { - kind: plan.frozen_draft.kind, - author: plan.frozen_draft.expected_pubkey.clone(), - created_at: Some(plan.frozen_draft.created_at), - content: plan.frozen_draft.content.clone(), - tags: plan.frozen_draft.tags.clone(), + kind: plan.frozen_draft.kind_u32(), + author: plan.frozen_draft.expected_pubkey_str().to_owned(), + created_at: Some(plan.frozen_draft.created_at_u64()), + content: plan.frozen_draft.content().to_owned(), + tags: plan.frozen_draft.tags_as_vec(), event_id: Some(plan.expected_event_id.as_str().to_owned()), signature: None, event_addr: plan.public_listing_addr.as_str().to_owned(), } } +fn listing_validation_event( + canonical: &CanonicalListingDraft, + parts: radroots_event::wire::RadrootsNip01EventWireParts, +) -> Result<RadrootsEventEnvelope, String> { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: "0".repeat(64), + author: canonical.seller_pubkey.clone(), + created_at: 0, + kind: KIND_LISTING_DRAFT, + tags: parts.tags, + content: parts.content, + sig: "0".repeat(128), + }) + .map_err(|error| error.to_string()) +} + fn sdk_push_event_for_listing<'a>( enqueue: &ListingEnqueueReceipt, push: &'a PushOutboxReceipt, diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -1734,8 +1734,9 @@ fn order_event_list_entry_from_event( let event = radroots_event_from_nostr(event); let envelope = order_request_from_event(&event) .map_err(|error| RuntimeError::Config(format!("decode order request event: {error}")))?; + let event_tags = event.tags_as_vec(); let context = - order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &event.tags) + order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &event_tags) .map_err(|error| RuntimeError::Config(format!("decode order request tags: {error}")))?; if context.counterparty_pubkey != seller_pubkey @@ -1747,13 +1748,13 @@ fn order_event_list_entry_from_event( } let listing_event_id = context.listing_event.as_ref().map(|event| event.id.clone()); - let created_at_unix = u64::from(event.created_at); + let created_at_unix = event.created_at_u64(); Ok(OrderEventListEntryView { id: envelope.order_id.clone(), state: "requested".to_owned(), - event_id: Some(event.id), - event_kind: Some(event.kind), + event_id: Some(event.id_str().to_owned()), + event_kind: Some(event.kind_u32()), listing_lookup: None, listing_addr: Some(envelope.listing_addr), listing_event_id, @@ -4718,8 +4719,9 @@ fn order_submit_request_from_event( let event = radroots_event_from_nostr(event); let envelope = order_request_from_event(&event) .map_err(|error| RuntimeError::Config(format!("decode order request event: {error}")))?; + let event_tags = event.tags_as_vec(); let context = - order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &event.tags) + order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &event_tags) .map_err(|error| RuntimeError::Config(format!("decode order request tags: {error}")))?; if envelope.order_id != loaded.document.order.order_id @@ -4743,12 +4745,12 @@ fn order_submit_request_from_event( "order request listing address is outside seller authority".to_owned(), )); } - let payload = canonicalize_order_request_for_signer(envelope.payload, event.author.as_str()) + let payload = canonicalize_order_request_for_signer(envelope.payload, event.author_str()) .map_err(|error| RuntimeError::Config(format!("canonicalize order request: {error}")))?; let listing_event_id = context.listing_event.as_ref().map(|event| event.id.clone()); Ok(ResolvedOrderSubmitRequest { - request_event_id: event.id, + request_event_id: event.id_str().to_owned(), listing_event_id, payload, }) diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs @@ -1783,11 +1783,11 @@ mod tests { use radroots_event::list_set::RadrootsListSet; use radroots_event::plot::RadrootsPlot; use radroots_event::profile::{RadrootsProfile, RadrootsProfileType}; + use radroots_event::wire::RadrootsNip01EventWireParts; use radroots_event_codec::farm::encode as farm_encode; use radroots_event_codec::list_set::encode as list_set_encode; use radroots_event_codec::plot::encode as plot_encode; use radroots_event_codec::profile::encode as profile_encode; - use radroots_event_codec::wire::WireEventParts; use radroots_identity::RadrootsIdentity; use radroots_nostr::prelude::{ RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrTimestamp, radroots_nostr_build_event, @@ -2929,7 +2929,7 @@ mod tests { let events = vec![ signed_event( &seller, - WireEventParts { + RadrootsNip01EventWireParts { kind: KIND_POST, content: "hello".to_owned(), tags: Vec::new(), @@ -2937,7 +2937,7 @@ mod tests { ), signed_event( &seller, - WireEventParts { + RadrootsNip01EventWireParts { kind: KIND_LISTING, content: "not a listing".to_owned(), tags: Vec::new(), @@ -3152,7 +3152,10 @@ mod tests { .expect("signed event") } - fn signed_event(identity: &RadrootsIdentity, parts: WireEventParts) -> RadrootsNostrEvent { + fn signed_event( + identity: &RadrootsIdentity, + parts: RadrootsNip01EventWireParts, + ) -> RadrootsNostrEvent { radroots_nostr_build_event(parts.kind, parts.content, parts.tags) .expect("event builder") .sign_with_keys(identity.keys()) diff --git a/src/runtime/validation_receipt.rs b/src/runtime/validation_receipt.rs @@ -107,7 +107,7 @@ pub struct ValidationReceiptResourceView { pub struct ValidationReceiptEventView { pub id: String, pub author: String, - pub created_at: u32, + pub created_at: u64, pub kind: u32, pub sig: String, pub tags: Vec<Vec<String>>, @@ -179,7 +179,7 @@ pub struct ValidationReceiptSummaryView { pub receipt_event_id: String, pub order_id: String, pub author: String, - pub created_at: u32, + pub created_at: u64, pub receipt_type: String, pub result: String, pub proof_system: String, @@ -394,7 +394,7 @@ fn inspected_event_view( failed_transport_targets: Vec<TransportTargetFailureView>, relay_reason_code: Option<String>, ) -> ValidationReceiptInspectionView { - let event_id = sdk_receipt.event.id.clone(); + let event_id = sdk_receipt.event.id_str().to_owned(); let order_id = sdk_receipt.tags.order_id.clone(); let proof_verification = proof_verification_view_for_receipt( &sdk_receipt.receipt, @@ -457,8 +457,8 @@ fn invalid_inspected_event_view( ) -> ValidationReceiptInspectionView { ValidationReceiptInspectionView { state: "invalid".to_owned(), - resource: Some(validation_receipt_resource(&invalid.event.id)), - receipt_event_id: Some(invalid.event.id.clone()), + resource: Some(validation_receipt_resource(invalid.event.id_str())), + receipt_event_id: Some(invalid.event.id_str().to_owned()), order_id: None, validation_state: "invalid".to_owned(), proof_verification: None, @@ -496,8 +496,8 @@ fn list_from_sdk_receipt( ); if proof_state_is_invalid(proof_verification.state.as_str()) { invalid_receipts.push(ValidationReceiptInvalidCandidateView { - receipt_event_id: sdk_event.event.id, - kind: sdk_event.event.kind, + receipt_event_id: sdk_event.event.id_str().to_owned(), + kind: sdk_event.event.kind_u32(), reason_code: proof_verification .reason_code .clone() @@ -718,13 +718,13 @@ fn validation_receipt_resource(id: &str) -> ValidationReceiptResourceView { fn event_view(event: radroots_event::RadrootsEventEnvelope) -> ValidationReceiptEventView { ValidationReceiptEventView { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - sig: event.sig, - tags: event.tags, - content: event.content, + id: event.id_str().to_owned(), + author: event.author_str().to_owned(), + created_at: event.created_at_u64(), + kind: event.kind_u32(), + sig: event.sig_str().to_owned(), + tags: event.tags_as_vec(), + content: event.content().to_owned(), } } @@ -1189,8 +1189,8 @@ fn invalid_candidate_view( candidate: TradeValidationReceiptInvalidCandidate, ) -> ValidationReceiptInvalidCandidateView { ValidationReceiptInvalidCandidateView { - receipt_event_id: candidate.event.id, - kind: candidate.event.kind, + receipt_event_id: candidate.event.id_str().to_owned(), + kind: candidate.event.kind_u32(), reason_code: candidate.reason_code, reason: candidate.reason, proof_verification: None, @@ -1269,11 +1269,11 @@ fn summary_view( proof_verification: &ValidationReceiptProofVerificationView, ) -> ValidationReceiptSummaryView { ValidationReceiptSummaryView { - resource: validation_receipt_resource(&event.id), - receipt_event_id: event.id.clone(), + resource: validation_receipt_resource(event.id_str()), + receipt_event_id: event.id_str().to_owned(), order_id: tags.order_id.clone(), - author: event.author.clone(), - created_at: event.created_at, + author: event.author_str().to_owned(), + created_at: event.created_at_u64(), receipt_type: receipt_type_label(receipt.receipt_type).to_owned(), result: receipt_result_label(receipt.result).to_owned(), proof_system: receipt.proof.system.as_str().to_owned(), diff --git a/src/view/runtime.rs b/src/view/runtime.rs @@ -3036,7 +3036,7 @@ pub struct ListingMutationEventView { pub kind: u32, pub author: String, #[serde(skip_serializing_if = "Option::is_none")] - pub created_at: Option<u32>, + pub created_at: Option<u64>, pub content: String, pub tags: Vec<Vec<String>>, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/tests/support/mod.rs b/tests/support/mod.rs @@ -4,9 +4,9 @@ use std::process::{Command, Output}; use std::sync::Mutex; use assert_cmd::prelude::*; -use radroots_event::RadrootsEventEnvelope; use radroots_event::ids::RadrootsListingAddress; use radroots_event::kinds::{KIND_FARM, KIND_LISTING}; +use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts}; use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; use radroots_protected_store::RadrootsProtectedFileSecretVault; use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event}; @@ -274,7 +274,7 @@ pub fn seed_orderable_listing(sandbox: &RadrootsCliSandbox, listing_addr: &str) .expect("replica db path from store init"); let (seller_pubkey, listing_id) = listing_addr_parts(listing_addr); let event_id = "2".repeat(64); - let event = RadrootsEventEnvelope { + let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: event_id.clone(), author: seller_pubkey.clone(), created_at: 1, @@ -322,7 +322,8 @@ pub fn seed_orderable_listing(sandbox: &RadrootsCliSandbox, listing_addr: &str) ], content: "# Market Eggs".to_owned(), sig: "f".repeat(128), - }; + }) + .expect("seed listing event"); let executor = SqliteExecutor::open(Path::new(db_path)).expect("open replica db"); assert_eq!( radroots_replica_ingest_event(&executor, &event).expect("ingest listing"), @@ -437,24 +438,26 @@ fn seed_orderable_listing_signed_event( store.migrate_up().expect("migrate runtime store"); store .append_record(&RuntimeStoreRecordInput { - record_id: format!("test:signed_listing:{}", event.id), + record_id: format!("test:signed_listing:{}", event.id_str()), family: RuntimeStoreRecordFamily::SignedEvent, status: RuntimeStoreRecordStatus::Published, source_runtime: SourceRuntime::Cli, created_at_ms: 1_779_000_001_000, inserted_at_ms: 1_779_000_001_000, owner_account_id: None, - owner_pubkey: Some(event.author.clone()), + owner_pubkey: Some(event.author_str().to_owned()), farm_id: None, listing_addr: Some(listing_addr.to_owned()), local_work_json: None, - event_id: Some(event.id.clone()), - event_kind: Some(i64::from(event.kind)), - event_pubkey: Some(event.author.clone()), - event_created_at: Some(i64::try_from(event.created_at).expect("event created_at")), - event_tags_json: Some(json!(event.tags)), - event_content: Some(event.content.clone()), - event_sig: Some(event.sig.clone()), + event_id: Some(event.id_str().to_owned()), + event_kind: Some(i64::from(event.kind_u32())), + event_pubkey: Some(event.author_str().to_owned()), + event_created_at: Some( + i64::try_from(event.created_at_u64()).expect("event created_at"), + ), + event_tags_json: Some(json!(event.tags_as_vec())), + event_content: Some(event.content().to_owned()), + event_sig: Some(event.sig_str().to_owned()), raw_event_json: Some(json!(event)), outbox_status: PublishOutboxStatus::Acknowledged, relay_set_fingerprint: None, diff --git a/tests/target_cli.rs b/tests/target_cli.rs @@ -738,7 +738,8 @@ fn handle_proxy_connection(mut stream: TcpStream, expected_token: Option<&str>) let request: Value = serde_json::from_str(body.as_str()).expect("proxy json"); assert_eq!(request["jsonrpc"], "2.0"); assert_eq!(request["method"], "transport.publish.event"); - let event = &request["params"]["event"]; + assert!(request["params"].get("event").is_none()); + let event = proxy_request_raw_event(&request); let target_result = json!({ "transport_kind": "nostr", "endpoint_uri": "wss://daemon-resolved.radroots.test", @@ -756,9 +757,9 @@ fn handle_proxy_connection(mut stream: TcpStream, expected_token: Option<&str>) "status": "delivery_satisfied", "terminal": true, "delivery_satisfied": true, - "event_id": event["id"], - "pubkey": event["pubkey"], - "event_kind": event["kind"], + "event_id": event["id"].clone(), + "pubkey": event["pubkey"].clone(), + "event_kind": event["kind"].clone(), "target_policy": request["params"]["target_policy"], "delivery_policy": request["params"]["delivery_policy"], "target_count": 1, @@ -782,6 +783,15 @@ fn handle_proxy_connection(mut stream: TcpStream, expected_token: Option<&str>) request } +fn proxy_request_raw_event(request: &Value) -> Value { + serde_json::from_str( + request["params"]["raw_event_json"] + .as_str() + .expect("proxy raw event json"), + ) + .expect("proxy raw event") +} + fn http_body_start(bytes: &[u8]) -> Option<usize> { bytes .windows(4) @@ -3607,10 +3617,8 @@ token_file = "{}" "SDK listing publish ยท configured signer" ); assert_eq!(value["result"]["dry_run"], false); - assert_eq!( - request["params"]["event"]["id"], - value["result"]["event_id"] - ); + let event = proxy_request_raw_event(&request); + assert_eq!(event["id"], value["result"]["event_id"]); assert_eq!( request["params"]["target_policy"]["relay_urls"] .as_array() @@ -3890,15 +3898,10 @@ token_file = "{}" assert!(report.req_count >= 1); assert_eq!(report.sign_request_count, 1); assert_eq!(report.published_events.len(), 0); - assert_eq!(request["params"]["event"]["kind"], KIND_LISTING); - assert_eq!( - request["params"]["event"]["pubkey"], - user_public.public_key_hex - ); - assert_eq!( - request["params"]["event"]["id"], - value["result"]["event_id"] - ); + let event = proxy_request_raw_event(&request); + assert_eq!(event["kind"], KIND_LISTING); + assert_eq!(event["pubkey"], user_public.public_key_hex); + assert_eq!(event["id"], value["result"]["event_id"]); } #[test]