sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit aed7a1428859a2a2d180b2a7568e41f6601362ff
parent 66015806a147824bad4c29ef9ae8e395d7838595
Author: triesap <tyson@radroots.org>
Date:   Mon, 22 Jun 2026 01:34:37 +0000

contracts: own sdk export metadata

Diffstat:
MCargo.lock | 2++
Acontracts/exports/go.toml | 19+++++++++++++++++++
Acontracts/exports/kotlin.toml | 19+++++++++++++++++++
Acontracts/exports/py.toml | 19+++++++++++++++++++
Acontracts/exports/swift.toml | 19+++++++++++++++++++
Acontracts/exports/ts.toml | 20++++++++++++++++++++
Acontracts/packages/go.toml | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/packages/kotlin.toml | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/packages/py.toml | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/packages/swift.toml | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/packages/ts.toml | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/xtask/Cargo.toml | 2++
Mcrates/xtask/src/check.rs | 2++
Acrates/xtask/src/contracts.rs | 411+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/xtask/src/main.rs | 1+
Mcrates/xtask/src/output.rs | 1-
Mpackages/trade-bindings/src/generated/types.ts | 1-
17 files changed, 856 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2031,7 +2031,9 @@ dependencies = [ "radroots_sdk_binding_model", "radroots_trade_bindings", "radroots_types_bindings", + "serde", "serde_json", + "toml", ] [[package]] diff --git a/contracts/exports/go.toml b/contracts/exports/go.toml @@ -0,0 +1,19 @@ +[language] +id = "go" +repository = "sdk-go" + +[packages] +"radroots_core" = "github.com/radrootslabs/sdk-go" +"radroots_events" = "github.com/radrootslabs/sdk-go" +"radroots_trade" = "github.com/radrootslabs/sdk-go" +"radroots_identity" = "github.com/radrootslabs/sdk-go" + +[artifacts] +models_dir = "internal/generated" +constants_dir = "internal/generated" +manifest_file = "export-manifest.json" + +[runtime] +networking = "native" +signing = "native" +deterministic_codec = "native_or_wasm" diff --git a/contracts/exports/kotlin.toml b/contracts/exports/kotlin.toml @@ -0,0 +1,19 @@ +[language] +id = "kotlin" +repository = "sdk-kotlin" + +[packages] +"radroots_core" = "radroots.sdk" +"radroots_events" = "radroots.sdk" +"radroots_trade" = "radroots.sdk" +"radroots_identity" = "radroots.sdk" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +manifest_file = "export-manifest.json" + +[runtime] +networking = "native" +signing = "native" +deterministic_codec = "native_or_wasm" diff --git a/contracts/exports/py.toml b/contracts/exports/py.toml @@ -0,0 +1,19 @@ +[language] +id = "py" +repository = "sdk-python" + +[packages] +"radroots_core" = "radroots_sdk" +"radroots_events" = "radroots_sdk" +"radroots_trade" = "radroots_sdk" +"radroots_identity" = "radroots_sdk" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +manifest_file = "export-manifest.json" + +[runtime] +networking = "native" +signing = "native" +deterministic_codec = "native_or_wasm" diff --git a/contracts/exports/swift.toml b/contracts/exports/swift.toml @@ -0,0 +1,19 @@ +[language] +id = "swift" +repository = "sdk-swift" + +[packages] +"radroots_core" = "RadrootsSDK" +"radroots_events" = "RadrootsSDK" +"radroots_trade" = "RadrootsSDK" +"radroots_identity" = "RadrootsSDK" + +[artifacts] +models_dir = "Sources/Generated" +constants_dir = "Sources/Generated" +manifest_file = "export-manifest.json" + +[runtime] +networking = "native" +signing = "native" +deterministic_codec = "native_or_wasm" diff --git a/contracts/exports/ts.toml b/contracts/exports/ts.toml @@ -0,0 +1,20 @@ +[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots_core" = "@radroots/sdk" +"radroots_events" = "@radroots/sdk" +"radroots_trade" = "@radroots/sdk" +"radroots_identity" = "@radroots/sdk" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" + +[runtime] +networking = "native" +signing = "native" +deterministic_codec = "wasm" diff --git a/contracts/packages/go.toml b/contracts/packages/go.toml @@ -0,0 +1,67 @@ +[language] +id = "go" +repository = "sdk-go" + +[sdk] +package = "github.com/radrootslabs/sdk-go" +deterministic_codec = "native_or_wasm" +signing = "native" +networking = "native" + +[rollout] +stage = "deferred" +order = 3 + +[operations] +"profile.build_draft" = "profile.BuildDraft" +"farm.build_draft" = "farm.BuildDraft" +"listing.build_tags" = "listing.BuildTags" +"listing.build_draft" = "listing.BuildDraft" +"listing.parse_event" = "listing.ParseEvent" +"social.post.build_tags" = "social.PostBuildTags" +"social.comment.build_tags" = "social.CommentBuildTags" +"social.reaction.build_tags" = "social.ReactionBuildTags" +"social.article.build_tags" = "social.ArticleBuildTags" +"social.file_metadata.build_tags" = "social.FileMetadataBuildTags" +"social.calendar_date_event.build_tags" = "social.CalendarDateEventBuildTags" +"social.calendar_time_event.build_tags" = "social.CalendarTimeEventBuildTags" +"order.build_order_request_draft" = "order.BuildOrderRequestDraft" +"order.build_order_decision_draft" = "order.BuildOrderDecisionDraft" +"order.parse_order_request" = "order.ParseOrderRequest" +"order.parse_order_decision" = "order.ParseOrderDecision" +"order.parse_listing_address" = "order.ParseListingAddress" +"trade_validation.validate_listing_event" = "tradevalidation.ValidateListingEvent" + +[shared_types] +"WireEventParts" = "WireEventParts" +"RadrootsFrozenEventDraft" = "RadrootsFrozenEventDraft" +"RadrootsSignedNostrEvent" = "RadrootsSignedNostrEvent" +"RadrootsNostrEvent" = "RadrootsNostrEvent" +"RadrootsNostrEventRef" = "RadrootsNostrEventRef" +"RadrootsNostrEventPtr" = "RadrootsNostrEventPtr" +"RadrootsListingAddress" = "ListingAddress" +"RadrootsProfile" = "RadrootsProfile" +"RadrootsFarm" = "RadrootsFarm" +"RadrootsListing" = "RadrootsListing" +"RadrootsPost" = "RadrootsPost" +"RadrootsComment" = "RadrootsComment" +"RadrootsReaction" = "RadrootsReaction" +"RadrootsArticle" = "RadrootsArticle" +"RadrootsFileMetadata" = "RadrootsFileMetadata" +"RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent" +"RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent" +"RadrootsOrderEnvelope" = "OrderEnvelope" +"RadrootsOrderEventType" = "OrderEventType" +"RadrootsOrderItem" = "OrderItem" +"RadrootsOrderPricingBasis" = "OrderPricingBasis" +"RadrootsOrderEconomicLineKind" = "OrderEconomicLineKind" +"RadrootsOrderEconomicActor" = "OrderEconomicActor" +"RadrootsOrderEconomicEffect" = "OrderEconomicEffect" +"RadrootsOrderEconomicItem" = "OrderEconomicItem" +"RadrootsOrderEconomicLine" = "OrderEconomicLine" +"RadrootsOrderEconomicTotals" = "OrderEconomicTotals" +"RadrootsOrderEconomics" = "OrderEconomics" +"RadrootsOrderRequest" = "OrderRequest" +"RadrootsOrderInventoryCommitment" = "OrderInventoryCommitment" +"RadrootsOrderDecisionOutcome" = "OrderDecisionOutcome" +"RadrootsOrderDecision" = "OrderDecision" diff --git a/contracts/packages/kotlin.toml b/contracts/packages/kotlin.toml @@ -0,0 +1,67 @@ +[language] +id = "kotlin" +repository = "sdk-kotlin" + +[sdk] +package = "radroots.sdk" +deterministic_codec = "native_or_wasm" +signing = "native" +networking = "native" + +[rollout] +stage = "next" +order = 2 + +[operations] +"profile.build_draft" = "profile.buildDraft" +"farm.build_draft" = "farm.buildDraft" +"listing.build_tags" = "listing.buildTags" +"listing.build_draft" = "listing.buildDraft" +"listing.parse_event" = "listing.parseEvent" +"social.post.build_tags" = "social.post.buildTags" +"social.comment.build_tags" = "social.comment.buildTags" +"social.reaction.build_tags" = "social.reaction.buildTags" +"social.article.build_tags" = "social.article.buildTags" +"social.file_metadata.build_tags" = "social.fileMetadata.buildTags" +"social.calendar_date_event.build_tags" = "social.calendarDateEvent.buildTags" +"social.calendar_time_event.build_tags" = "social.calendarTimeEvent.buildTags" +"order.build_order_request_draft" = "order.buildOrderRequestDraft" +"order.build_order_decision_draft" = "order.buildOrderDecisionDraft" +"order.parse_order_request" = "order.parseOrderRequest" +"order.parse_order_decision" = "order.parseOrderDecision" +"order.parse_listing_address" = "order.parseListingAddress" +"trade_validation.validate_listing_event" = "tradeValidation.validateListingEvent" + +[shared_types] +"WireEventParts" = "WireEventParts" +"RadrootsFrozenEventDraft" = "RadrootsFrozenEventDraft" +"RadrootsSignedNostrEvent" = "RadrootsSignedNostrEvent" +"RadrootsNostrEvent" = "RadrootsNostrEvent" +"RadrootsNostrEventRef" = "RadrootsNostrEventRef" +"RadrootsNostrEventPtr" = "RadrootsNostrEventPtr" +"RadrootsListingAddress" = "ListingAddress" +"RadrootsProfile" = "RadrootsProfile" +"RadrootsFarm" = "RadrootsFarm" +"RadrootsListing" = "RadrootsListing" +"RadrootsPost" = "RadrootsPost" +"RadrootsComment" = "RadrootsComment" +"RadrootsReaction" = "RadrootsReaction" +"RadrootsArticle" = "RadrootsArticle" +"RadrootsFileMetadata" = "RadrootsFileMetadata" +"RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent" +"RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent" +"RadrootsOrderEnvelope" = "OrderEnvelope" +"RadrootsOrderEventType" = "OrderEventType" +"RadrootsOrderItem" = "OrderItem" +"RadrootsOrderPricingBasis" = "OrderPricingBasis" +"RadrootsOrderEconomicLineKind" = "OrderEconomicLineKind" +"RadrootsOrderEconomicActor" = "OrderEconomicActor" +"RadrootsOrderEconomicEffect" = "OrderEconomicEffect" +"RadrootsOrderEconomicItem" = "OrderEconomicItem" +"RadrootsOrderEconomicLine" = "OrderEconomicLine" +"RadrootsOrderEconomicTotals" = "OrderEconomicTotals" +"RadrootsOrderEconomics" = "OrderEconomics" +"RadrootsOrderRequest" = "OrderRequest" +"RadrootsOrderInventoryCommitment" = "OrderInventoryCommitment" +"RadrootsOrderDecisionOutcome" = "OrderDecisionOutcome" +"RadrootsOrderDecision" = "OrderDecision" diff --git a/contracts/packages/py.toml b/contracts/packages/py.toml @@ -0,0 +1,67 @@ +[language] +id = "py" +repository = "sdk-python" + +[sdk] +package = "radroots_sdk" +deterministic_codec = "native_or_wasm" +signing = "native" +networking = "native" + +[rollout] +stage = "deferred" +order = 3 + +[operations] +"profile.build_draft" = "profile_build_draft" +"farm.build_draft" = "farm_build_draft" +"listing.build_tags" = "listing_build_tags" +"listing.build_draft" = "listing_build_draft" +"listing.parse_event" = "listing_parse_event" +"social.post.build_tags" = "social_post_build_tags" +"social.comment.build_tags" = "social_comment_build_tags" +"social.reaction.build_tags" = "social_reaction_build_tags" +"social.article.build_tags" = "social_article_build_tags" +"social.file_metadata.build_tags" = "social_file_metadata_build_tags" +"social.calendar_date_event.build_tags" = "social_calendar_date_event_build_tags" +"social.calendar_time_event.build_tags" = "social_calendar_time_event_build_tags" +"order.build_order_request_draft" = "order_build_order_request_draft" +"order.build_order_decision_draft" = "order_build_order_decision_draft" +"order.parse_order_request" = "order_parse_order_request" +"order.parse_order_decision" = "order_parse_order_decision" +"order.parse_listing_address" = "order_parse_listing_address" +"trade_validation.validate_listing_event" = "trade_validation_validate_listing_event" + +[shared_types] +"WireEventParts" = "WireEventParts" +"RadrootsFrozenEventDraft" = "RadrootsFrozenEventDraft" +"RadrootsSignedNostrEvent" = "RadrootsSignedNostrEvent" +"RadrootsNostrEvent" = "RadrootsNostrEvent" +"RadrootsNostrEventRef" = "RadrootsNostrEventRef" +"RadrootsNostrEventPtr" = "RadrootsNostrEventPtr" +"RadrootsListingAddress" = "ListingAddress" +"RadrootsProfile" = "RadrootsProfile" +"RadrootsFarm" = "RadrootsFarm" +"RadrootsListing" = "RadrootsListing" +"RadrootsPost" = "RadrootsPost" +"RadrootsComment" = "RadrootsComment" +"RadrootsReaction" = "RadrootsReaction" +"RadrootsArticle" = "RadrootsArticle" +"RadrootsFileMetadata" = "RadrootsFileMetadata" +"RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent" +"RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent" +"RadrootsOrderEnvelope" = "OrderEnvelope" +"RadrootsOrderEventType" = "OrderEventType" +"RadrootsOrderItem" = "OrderItem" +"RadrootsOrderPricingBasis" = "OrderPricingBasis" +"RadrootsOrderEconomicLineKind" = "OrderEconomicLineKind" +"RadrootsOrderEconomicActor" = "OrderEconomicActor" +"RadrootsOrderEconomicEffect" = "OrderEconomicEffect" +"RadrootsOrderEconomicItem" = "OrderEconomicItem" +"RadrootsOrderEconomicLine" = "OrderEconomicLine" +"RadrootsOrderEconomicTotals" = "OrderEconomicTotals" +"RadrootsOrderEconomics" = "OrderEconomics" +"RadrootsOrderRequest" = "OrderRequest" +"RadrootsOrderInventoryCommitment" = "OrderInventoryCommitment" +"RadrootsOrderDecisionOutcome" = "OrderDecisionOutcome" +"RadrootsOrderDecision" = "OrderDecision" diff --git a/contracts/packages/swift.toml b/contracts/packages/swift.toml @@ -0,0 +1,67 @@ +[language] +id = "swift" +repository = "sdk-swift" + +[sdk] +package = "RadrootsSDK" +deterministic_codec = "native_or_wasm" +signing = "native" +networking = "native" + +[rollout] +stage = "next" +order = 2 + +[operations] +"profile.build_draft" = "profile.buildDraft" +"farm.build_draft" = "farm.buildDraft" +"listing.build_tags" = "listing.buildTags" +"listing.build_draft" = "listing.buildDraft" +"listing.parse_event" = "listing.parseEvent" +"social.post.build_tags" = "social.post.buildTags" +"social.comment.build_tags" = "social.comment.buildTags" +"social.reaction.build_tags" = "social.reaction.buildTags" +"social.article.build_tags" = "social.article.buildTags" +"social.file_metadata.build_tags" = "social.fileMetadata.buildTags" +"social.calendar_date_event.build_tags" = "social.calendarDateEvent.buildTags" +"social.calendar_time_event.build_tags" = "social.calendarTimeEvent.buildTags" +"order.build_order_request_draft" = "order.buildOrderRequestDraft" +"order.build_order_decision_draft" = "order.buildOrderDecisionDraft" +"order.parse_order_request" = "order.parseOrderRequest" +"order.parse_order_decision" = "order.parseOrderDecision" +"order.parse_listing_address" = "order.parseListingAddress" +"trade_validation.validate_listing_event" = "tradeValidation.validateListingEvent" + +[shared_types] +"WireEventParts" = "WireEventParts" +"RadrootsFrozenEventDraft" = "RadrootsFrozenEventDraft" +"RadrootsSignedNostrEvent" = "RadrootsSignedNostrEvent" +"RadrootsNostrEvent" = "RadrootsNostrEvent" +"RadrootsNostrEventRef" = "RadrootsNostrEventRef" +"RadrootsNostrEventPtr" = "RadrootsNostrEventPtr" +"RadrootsListingAddress" = "ListingAddress" +"RadrootsProfile" = "RadrootsProfile" +"RadrootsFarm" = "RadrootsFarm" +"RadrootsListing" = "RadrootsListing" +"RadrootsPost" = "RadrootsPost" +"RadrootsComment" = "RadrootsComment" +"RadrootsReaction" = "RadrootsReaction" +"RadrootsArticle" = "RadrootsArticle" +"RadrootsFileMetadata" = "RadrootsFileMetadata" +"RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent" +"RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent" +"RadrootsOrderEnvelope" = "OrderEnvelope" +"RadrootsOrderEventType" = "OrderEventType" +"RadrootsOrderItem" = "OrderItem" +"RadrootsOrderPricingBasis" = "OrderPricingBasis" +"RadrootsOrderEconomicLineKind" = "OrderEconomicLineKind" +"RadrootsOrderEconomicActor" = "OrderEconomicActor" +"RadrootsOrderEconomicEffect" = "OrderEconomicEffect" +"RadrootsOrderEconomicItem" = "OrderEconomicItem" +"RadrootsOrderEconomicLine" = "OrderEconomicLine" +"RadrootsOrderEconomicTotals" = "OrderEconomicTotals" +"RadrootsOrderEconomics" = "OrderEconomics" +"RadrootsOrderRequest" = "OrderRequest" +"RadrootsOrderInventoryCommitment" = "OrderInventoryCommitment" +"RadrootsOrderDecisionOutcome" = "OrderDecisionOutcome" +"RadrootsOrderDecision" = "OrderDecision" diff --git a/contracts/packages/ts.toml b/contracts/packages/ts.toml @@ -0,0 +1,74 @@ +[language] +id = "ts" +repository = "sdk-typescript" + +[sdk] +package = "@radroots/sdk" +module_format = "esm" +deterministic_codec = "wasm" +signing = "native" +networking = "native" + +[rollout] +stage = "active" +order = 1 + +[operations] +"profile.build_draft" = "profile.buildDraft" +"farm.build_draft" = "farm.buildDraft" +"listing.build_tags" = "listing.buildTags" +"listing.build_draft" = "listing.buildDraft" +"listing.parse_event" = "listing.parseEvent" +"social.post.build_tags" = "social.post.buildTags" +"social.comment.build_tags" = "social.comment.buildTags" +"social.reaction.build_tags" = "social.reaction.buildTags" +"social.article.build_tags" = "social.article.buildTags" +"social.file_metadata.build_tags" = "social.fileMetadata.buildTags" +"social.calendar_date_event.build_tags" = "social.calendarDateEvent.buildTags" +"social.calendar_time_event.build_tags" = "social.calendarTimeEvent.buildTags" +"order.build_order_request_draft" = "order.buildOrderRequestDraft" +"order.build_order_decision_draft" = "order.buildOrderDecisionDraft" +"order.parse_order_request" = "order.parseOrderRequest" +"order.parse_order_decision" = "order.parseOrderDecision" +"order.parse_listing_address" = "order.parseListingAddress" +"trade_validation.validate_listing_event" = "tradeValidation.validateListingEvent" + +[shared_types] +"WireEventParts" = "WireEventParts" +"RadrootsFrozenEventDraft" = "RadrootsFrozenEventDraft" +"RadrootsSignedNostrEvent" = "RadrootsSignedNostrEvent" +"RadrootsNostrEvent" = "RadrootsNostrEvent" +"RadrootsNostrEventRef" = "RadrootsNostrEventRef" +"RadrootsNostrEventPtr" = "RadrootsNostrEventPtr" +"RadrootsListingAddress" = "ListingAddress" +"RadrootsProfile" = "RadrootsProfile" +"RadrootsFarm" = "RadrootsFarm" +"RadrootsListing" = "RadrootsListing" +"RadrootsPost" = "RadrootsPost" +"RadrootsComment" = "RadrootsComment" +"RadrootsReaction" = "RadrootsReaction" +"RadrootsArticle" = "RadrootsArticle" +"RadrootsFileMetadata" = "RadrootsFileMetadata" +"RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent" +"RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent" +"RadrootsOrderEnvelope" = "OrderEnvelope" +"RadrootsOrderEventType" = "OrderEventType" +"RadrootsOrderItem" = "OrderItem" +"RadrootsOrderPricingBasis" = "OrderPricingBasis" +"RadrootsOrderEconomicLineKind" = "OrderEconomicLineKind" +"RadrootsOrderEconomicActor" = "OrderEconomicActor" +"RadrootsOrderEconomicEffect" = "OrderEconomicEffect" +"RadrootsOrderEconomicItem" = "OrderEconomicItem" +"RadrootsOrderEconomicLine" = "OrderEconomicLine" +"RadrootsOrderEconomicTotals" = "OrderEconomicTotals" +"RadrootsOrderEconomics" = "OrderEconomics" +"RadrootsOrderRequest" = "OrderRequest" +"RadrootsOrderInventoryCommitment" = "OrderInventoryCommitment" +"RadrootsOrderDecisionOutcome" = "OrderDecisionOutcome" +"RadrootsOrderDecision" = "OrderDecision" + +[artifacts] +models_dir = "src/generated" +runtime_dir = "src/runtime" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml @@ -22,3 +22,5 @@ radroots_replica_db_schema_bindings = { path = "../replica_db_schema_bindings" } radroots_types_bindings = { path = "../types_bindings" } radroots_trade_bindings = { path = "../trade_bindings" } serde_json = "1" +serde = { workspace = true, features = ["derive"] } +toml = "0.8" diff --git a/crates/xtask/src/check.rs b/crates/xtask/src/check.rs @@ -1,6 +1,7 @@ use std::{collections::BTreeSet, fs, path::Path}; use crate::{ + contracts::validate_sdk_contracts, fs::workspace_root, output::package_outputs, package_matrix::{ @@ -12,6 +13,7 @@ use crate::{ pub fn check() -> Result<(), String> { validate_package_matrix()?; let root = workspace_root()?; + validate_sdk_contracts(&root)?; check_forbidden_packages(&root)?; check_binding_crate_sources(&root)?; for spec in package_specs() { diff --git a/crates/xtask/src/contracts.rs b/crates/xtask/src/contracts.rs @@ -0,0 +1,411 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::{Path, PathBuf}, +}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExportContract { + language: LanguageContract, + packages: BTreeMap<String, String>, + artifacts: Option<ExportArtifacts>, + runtime: RuntimeContract, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct PackageContract { + language: LanguageContract, + sdk: SdkPackageContract, + rollout: RolloutContract, + operations: BTreeMap<String, String>, + shared_types: BTreeMap<String, String>, + artifacts: Option<SdkArtifacts>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct LanguageContract { + id: String, + repository: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RuntimeContract { + networking: String, + signing: String, + deterministic_codec: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExportArtifacts { + models_dir: String, + constants_dir: String, + wasm_dist_dir: Option<String>, + manifest_file: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SdkPackageContract { + package: String, + module_format: Option<String>, + deterministic_codec: String, + signing: String, + networking: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RolloutContract { + stage: String, + order: u32, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SdkArtifacts { + models_dir: String, + runtime_dir: String, + wasm_dist_dir: String, + manifest_file: String, +} + +pub fn validate_sdk_contracts(root: &Path) -> Result<(), String> { + let exports = load_contract_dir::<ExportContract>(&root.join("contracts").join("exports"))?; + let packages = load_contract_dir::<PackageContract>(&root.join("contracts").join("packages"))?; + if exports.is_empty() { + return Err("contracts/exports must define at least one language".to_owned()); + } + if packages.is_empty() { + return Err("contracts/packages must define at least one language".to_owned()); + } + + let mut export_packages = BTreeMap::new(); + let mut export_languages = BTreeSet::new(); + for export in &exports { + validate_language(&export.language, "exports")?; + validate_non_empty_map(&export.packages, "exports packages")?; + validate_runtime( + &export.runtime.networking, + &export.runtime.signing, + &export.runtime.deterministic_codec, + &format!("exports {}", export.language.id), + )?; + let artifacts = export + .artifacts + .as_ref() + .ok_or_else(|| format!("exports {} artifacts are required", export.language.id))?; + validate_non_empty(&artifacts.models_dir, "exports artifacts.models_dir")?; + validate_non_empty(&artifacts.constants_dir, "exports artifacts.constants_dir")?; + validate_non_empty(&artifacts.manifest_file, "exports artifacts.manifest_file")?; + if export.language.id == "ts" { + validate_non_empty( + artifacts.wasm_dist_dir.as_deref().unwrap_or(""), + "exports ts artifacts.wasm_dist_dir", + )?; + } + if !export_languages.insert(export.language.id.clone()) { + return Err(format!("duplicate exports language {}", export.language.id)); + } + let packages = export + .packages + .values() + .cloned() + .collect::<BTreeSet<String>>(); + if packages.len() != 1 { + return Err(format!( + "exports {} must resolve to one curated package", + export.language.id + )); + } + export_packages.insert(export.language.id.clone(), packages); + } + + let mut package_languages = BTreeSet::new(); + let mut operation_keys: Option<BTreeSet<String>> = None; + let mut shared_type_keys: Option<BTreeSet<String>> = None; + let mut rollout_orders = BTreeMap::new(); + for package in &packages { + validate_language(&package.language, "packages")?; + validate_non_empty(&package.sdk.package, "packages sdk.package")?; + validate_runtime( + &package.sdk.networking, + &package.sdk.signing, + &package.sdk.deterministic_codec, + &format!("packages {}", package.language.id), + )?; + if let Some(module_format) = package.sdk.module_format.as_deref() { + validate_non_empty(module_format, "packages sdk.module_format")?; + } + validate_rollout(&package.language.id, &package.rollout)?; + validate_non_empty_map(&package.operations, "packages operations")?; + validate_non_empty_map(&package.shared_types, "packages shared_types")?; + if package.language.id == "ts" { + let artifacts = package + .artifacts + .as_ref() + .ok_or_else(|| "packages ts artifacts are required".to_owned())?; + validate_non_empty(&artifacts.models_dir, "packages ts artifacts.models_dir")?; + validate_non_empty(&artifacts.runtime_dir, "packages ts artifacts.runtime_dir")?; + validate_non_empty( + &artifacts.wasm_dist_dir, + "packages ts artifacts.wasm_dist_dir", + )?; + validate_non_empty( + &artifacts.manifest_file, + "packages ts artifacts.manifest_file", + )?; + } + if !package_languages.insert(package.language.id.clone()) { + return Err(format!( + "duplicate packages language {}", + package.language.id + )); + } + let Some(packages_for_language) = export_packages.get(&package.language.id) else { + return Err(format!( + "packages {} is missing a matching export contract", + package.language.id + )); + }; + let expected = [package.sdk.package.clone()] + .into_iter() + .collect::<BTreeSet<_>>(); + if packages_for_language != &expected { + return Err(format!( + "exports {} must resolve to package {}", + package.language.id, package.sdk.package + )); + } + let current_operations = package.operations.keys().cloned().collect::<BTreeSet<_>>(); + match &operation_keys { + Some(expected) if expected != &current_operations => { + return Err(format!( + "packages {} operations must match the shared operation set", + package.language.id + )); + } + None => operation_keys = Some(current_operations), + _ => {} + } + let current_shared_types = package + .shared_types + .keys() + .cloned() + .collect::<BTreeSet<_>>(); + match &shared_type_keys { + Some(expected) if expected != &current_shared_types => { + return Err(format!( + "packages {} shared_types must match the shared type set", + package.language.id + )); + } + None => shared_type_keys = Some(current_shared_types), + _ => {} + } + rollout_orders.insert(package.language.id.clone(), package.rollout.order); + } + + if export_languages != package_languages { + return Err("contracts/exports and contracts/packages languages must match".to_owned()); + } + if rollout_orders.get("ts") != Some(&1) { + return Err("packages ts rollout.order must be 1".to_owned()); + } + Ok(()) +} + +fn load_contract_dir<T>(dir: &Path) -> Result<Vec<T>, String> +where + T: for<'de> Deserialize<'de>, +{ + let read_dir = + fs::read_dir(dir).map_err(|error| format!("failed to read {}: {error}", dir.display()))?; + let mut entries = read_dir + .collect::<Result<Vec<_>, _>>() + .map_err(|error| format!("failed to read {} entry: {error}", dir.display()))?; + entries.sort_by_key(|entry| entry.file_name()); + let mut contracts = Vec::new(); + for entry in entries { + let path = entry.path(); + if path.extension().and_then(|extension| extension.to_str()) != Some("toml") { + continue; + } + contracts.push(parse_toml(&path)?); + } + Ok(contracts) +} + +fn parse_toml<T>(path: &PathBuf) -> Result<T, String> +where + T: for<'de> Deserialize<'de>, +{ + let raw = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display())) +} + +fn validate_language(language: &LanguageContract, family: &str) -> Result<(), String> { + validate_non_empty(&language.id, &format!("{family} language.id"))?; + validate_non_empty( + &language.repository, + &format!("{family} language.repository"), + ) +} + +fn validate_runtime( + networking: &str, + signing: &str, + deterministic_codec: &str, + family: &str, +) -> Result<(), String> { + validate_non_empty(networking, &format!("{family} networking"))?; + validate_non_empty(signing, &format!("{family} signing"))?; + validate_non_empty( + deterministic_codec, + &format!("{family} deterministic_codec"), + ) +} + +fn validate_rollout(language: &str, rollout: &RolloutContract) -> Result<(), String> { + validate_non_empty(&rollout.stage, "packages rollout.stage")?; + if !matches!(rollout.stage.as_str(), "active" | "next" | "deferred") { + return Err(format!("packages {language} rollout.stage is invalid")); + } + if rollout.order == 0 { + return Err(format!( + "packages {language} rollout.order must be greater than zero" + )); + } + Ok(()) +} + +fn validate_non_empty(value: &str, field: &str) -> Result<(), String> { + if value.trim().is_empty() || value.trim() != value { + return Err(format!("{field} must be non-empty")); + } + Ok(()) +} + +fn validate_non_empty_map(map: &BTreeMap<String, String>, field: &str) -> Result<(), String> { + if map.is_empty() { + return Err(format!("{field} must not be empty")); + } + for (key, value) in map { + validate_non_empty(key, field)?; + validate_non_empty(value, field)?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::{ + fs, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::validate_sdk_contracts; + + #[test] + fn current_sdk_contracts_validate() { + let root = crate::fs::workspace_root().expect("workspace root"); + validate_sdk_contracts(&root).expect("sdk contracts validate"); + } + + #[test] + fn rejects_mismatched_language_sets() { + let root = test_root("language_mismatch"); + write_contract( + &root, + "contracts/exports/ts.toml", + EXPORT_TS.replace("@radroots/sdk", "@radroots/sdk").as_str(), + ); + let error = validate_sdk_contracts(&root).expect_err("missing packages should fail"); + assert!(error.contains("contracts/packages")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn rejects_package_export_mismatch() { + let root = test_root("package_mismatch"); + write_contract(&root, "contracts/exports/ts.toml", EXPORT_TS); + write_contract( + &root, + "contracts/packages/ts.toml", + PACKAGE_TS + .replace("@radroots/sdk", "@radroots/other") + .as_str(), + ); + let error = validate_sdk_contracts(&root).expect_err("mismatch should fail"); + assert!(error.contains("exports ts must resolve")); + let _ = fs::remove_dir_all(root); + } + + fn test_root(name: &str) -> std::path::PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time") + .as_nanos(); + std::env::temp_dir().join(format!("radroots_sdk_contracts_{name}_{stamp}")) + } + + fn write_contract(root: &std::path::Path, relative: &str, contents: &str) { + let path = root.join(relative); + fs::create_dir_all(path.parent().expect("parent")).expect("create parent"); + fs::write(path, contents).expect("write contract"); + } + + const EXPORT_TS: &str = r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots_core" = "@radroots/sdk" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" + +[runtime] +networking = "native" +signing = "native" +deterministic_codec = "wasm" +"#; + + const PACKAGE_TS: &str = r#"[language] +id = "ts" +repository = "sdk-typescript" + +[sdk] +package = "@radroots/sdk" +module_format = "esm" +deterministic_codec = "wasm" +signing = "native" +networking = "native" + +[rollout] +stage = "active" +order = 1 + +[operations] +"profile.build_draft" = "profile.buildDraft" + +[shared_types] +"WireEventParts" = "WireEventParts" + +[artifacts] +models_dir = "src/generated" +runtime_dir = "src/runtime" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#; +} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -1,4 +1,5 @@ mod check; +mod contracts; mod fs; mod generate; mod manifest; diff --git a/crates/xtask/src/output.rs b/crates/xtask/src/output.rs @@ -180,7 +180,6 @@ import type { RadrootsNostrEventPtr, RadrootsPlotRef, RadrootsResourceAreaRef, - RadrootsTradeFulfillmentStatus, RadrootsTradeMessagePayload, RadrootsTradeOrderEconomicLine, RadrootsTradeOrderItem, diff --git a/packages/trade-bindings/src/generated/types.ts b/packages/trade-bindings/src/generated/types.ts @@ -15,7 +15,6 @@ import type { RadrootsNostrEventPtr, RadrootsPlotRef, RadrootsResourceAreaRef, - RadrootsTradeFulfillmentStatus, RadrootsTradeMessagePayload, RadrootsTradeOrderEconomicLine, RadrootsTradeOrderItem,