commit aed7a1428859a2a2d180b2a7568e41f6601362ff
parent 66015806a147824bad4c29ef9ae8e395d7838595
Author: triesap <tyson@radroots.org>
Date: Mon, 22 Jun 2026 01:34:37 +0000
contracts: own sdk export metadata
Diffstat:
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 != ¤t_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 != ¤t_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,