From e3ce4e1e09cbbf9c0b52cd361ed8c980d06e6926 Mon Sep 17 00:00:00 2001 From: gravermistakes Date: Wed, 22 Jul 2026 02:16:22 -0700 Subject: [PATCH] Revise Trader-Wallet API surface designs Updated API surface design documentation to focus on Trader-Wallet structure and removed outdated sections. Signed-off-by: gravermistakes --- core/docs/plans/M1-api-shapes.md | 1258 +----------------------------- 1 file changed, 2 insertions(+), 1256 deletions(-) diff --git a/core/docs/plans/M1-api-shapes.md b/core/docs/plans/M1-api-shapes.md index 0989065..f0eaa1e 100644 --- a/core/docs/plans/M1-api-shapes.md +++ b/core/docs/plans/M1-api-shapes.md @@ -1,1258 +1,4 @@ -# Trader-Wallet-Marketplace API Surface Designs +# Trader-Wallet -## Overview -Three radically different API shapes for how traders submit actions, receive predictions, and interact with wallet binding. Each prioritizes different architectural values: **A** minimizes attack surface and dependencies; **B** emphasizes auditability and replay; **C** optimizes for type safety and live data. - ---- - -## Design A: Minimal Capability-Token Surface - -**Philosophy:** Stateless, HMAC-authenticated, two core endpoints. Trader auth is unforgeable capability tokens. Predictions and state are ephemeral — traders make decisions from momentary snapshots. Simplicity is security. - -### Method Signatures - -```rust -// Core entry point: submit action with signed token -fn submit_action( - token: CapabilityToken, // HMAC(trader_id + wallet_id + action_hash + timestamp) - action: MarketAction, - wallet_id: WalletId, -) -> Result - -// Single query endpoint: predictions + wallet state -fn query_market_snapshot( - token: CapabilityToken, -) -> Result -``` - -### Token Structure - -``` -CapabilityToken = { - trader_id: TradeId, - wallet_id: WalletId, - action_class: &str, // "buy" | "sell" | "mint" | etc. - nonce: u64, // prevents replay within TTL window - issued_at: Timestamp, - expires_at: Timestamp, // 30s TTL - signature: HmacSha256( - concat(trader_id, wallet_id, action_class, nonce, issued_at, expires_at), - shared_secret - ) -} -``` - -### Trader → Marketplace Flow - -``` -1. Trader generates CapabilityToken locally (has shared_secret, trader_id, wallet_id) -2. Trader calls query_market_snapshot(token) - → Marketplace verifies HMAC(token) - → Returns MarketSnapshot { predictions, wallet_balance, current_gas_price } -3. Trader formulates MarketAction offline -4. Trader generates new CapabilityToken(action_class = action.type()) -5. Trader calls submit_action(token, action, wallet_id) - → Marketplace verifies HMAC(token) and action.type matches token.action_class - → law_check(action) → if fail: return ViolationError - → veto_check(action, trader_id) → if fail: return VetoError - → wallet.sign() → execute() on chain → return ExecutionReceipt or ExecutionError -6. No polling. Action is fire-and-forget; trader checks balance later if desired. -``` - -### Error Handling - -```rust -pub enum MarketplaceError { - TokenExpired, // 30s window expired - TokenInvalid, // HMAC verification failed - WalletNotBound, // trader_id has no wallet - LawViolation { - rule_id: String, - reason: String, // e.g., "position_limit_exceeded" - }, - VetoedByConduct { - reason: String, // e.g., "risk_score_exceeded" - }, - WalletSignFailed, // key management error - ChainExecutionFailed { - tx_hash: Option, // may have been broadcast - reason: String, - }, - Timeout, // RPC didn't respond -} -``` - -### Law Script Invocation - -**In-process pure function.** Law script is loaded at startup as a compiled deterministic rule engine (no I/O, no loops). Invoked as: - -```rust -fn law_check(action: &MarketAction) -> Result<(), LawViolation> { - // Loaded once at startup; immutable at runtime - LAW_ENGINE.evaluate(&action) -} -``` - -Law script format (declarative, non-Turing): -``` -rule "position_limit" { - when action.type == "buy" && action.quantity > 10 - then violation("P001", "position exceeds 10 units") -} -``` - -### Extensibility - -**Adding new action types:** -1. Add variant to `MarketAction` enum -2. Update law script with new rules -3. Restart marketplace (law script is immutable; changes require restart + operator + Homunculus signatures) -4. Traders regenerate capability tokens with new `action_class` - -**Adding new constraint:** -1. Edit law script file -2. Restart marketplace with signatures -3. Constraint applies to all future actions automatically (law engine re-evaluates) - -**Limitations:** -- No runtime configuration -- Hard restart required for law changes -- Requires cryptographic coordination to restart with signatures - -### Testability - -```rust -#[cfg(test)] -mod tests { - #[test] - fn test_law_violation_blocks_action() { - let action = MarketAction::Buy { quantity: 100, ... }; - let result = law_check(&action); - assert!(matches!(result, Err(LawViolation { rule_id: "P001", ... }))); - } - - #[test] - fn test_valid_capability_token() { - let token = gen_token(trader_id, wallet_id, "buy", secret); - assert!(verify_hmac(&token, secret)); - } - - #[test] - fn test_expired_token_rejected() { - let token = gen_token_with_expiry(issued_at, expires_at - 60s); - assert!(matches!( - submit_action(&token, action, wallet), - Err(MarketplaceError::TokenExpired) - )); - } - - #[test] - fn test_veto_blocks_approved_action() { - let action = MarketAction::Buy { /* complies with law */ }; - let token = gen_token(...); - // Mock Conductor veto - conductor_mock.set_veto(true); - let result = submit_action(&token, action, wallet_id); - assert!(matches!(result, Err(MarketplaceError::VetoedByConduct))); - } - - #[test] - fn test_law_engine_deterministic() { - // Same action, same law script, same result always - for _ in 0..100 { - assert_eq!( - law_check(&action), - law_check(&action) - ); - } - } - - // Unit test law script rules in isolation - #[test] - fn test_rule_position_limit() { - let rule = law_engine.get_rule("position_limit"); - assert!(rule.evaluate(&big_action).is_err()); - assert!(rule.evaluate(&small_action).is_ok()); - } -} -``` - -**Strengths:** -- Minimal attack surface (2 endpoints) -- No session state → concurrent traders don't interfere -- HMAC is fast; verification is local -- Law engine is unit-testable in isolation -- Replay-protected by nonce + short TTL - ---- - -## Design B: Full Request-Reply with Event-Sourced Ledger - -**Philosophy:** Every action is an event. Traders submit, marketplace publishes, events are immutable. Replay and audit are first-class. Testability via event snapshots. - -### Method Signatures - -```rust -// Submit action, get action_id immediately -fn submit_action( - trader_id: TradeId, - wallet_id: WalletId, - action: MarketAction, -) -> Result - -// Poll for action status -fn get_action_status( - action_id: ActionId, - trader_id: TradeId, // verify trader owns this action -) -> Result - -// Stream events (trader sees own actions only) -fn subscribe_trader_events( - trader_id: TradeId, -) -> EventStream - -// Query predictions (snapshot at call time) -fn get_predictions( - sim_type: Option, -) -> Result, SimNotReady> - -// Query wallet state (snapshot) -fn get_wallet_state( - wallet_id: WalletId, - trader_id: TradeId, -) -> Result - -// Admin: replay events from ledger (for audit/reconstruction) -fn replay_events( - from_action_id: ActionId, - to_action_id: ActionId, -) -> Result, OutOfBounds> -``` - -### Event Ledger Structure - -```rust -pub enum ActionEvent { - Submitted { - action_id: ActionId, - trader_id: TradeId, - wallet_id: WalletId, - action: MarketAction, - timestamp: Timestamp, - }, - LawChecked { - action_id: ActionId, - passed: bool, - violations: Vec, - }, - VetoChecked { - action_id: ActionId, - approved: bool, - veto_reason: Option, - }, - WalletSigned { - action_id: ActionId, - tx_hash: Hash256, - }, - ExecutedOnChain { - action_id: ActionId, - tx_hash: Hash256, - block_number: u64, - status: TxStatus, // Confirmed | Failed | Pending - }, - ActionFailed { - action_id: ActionId, - reason: ActionFailureReason, - }, -} - -// Immutable append-only log -pub struct ActionLedger { - events: Vec, // written to durable store -} -``` - -### Trader → Marketplace Flow - -``` -1. Trader calls submit_action(trader_id, wallet_id, action) - → Marketplace creates ActionId, logs Submitted event - → Returns ActionId immediately (async processing begins) - -2. Background: Marketplace processes in pipeline - → law_check(action) → logs LawChecked event - → If law fails: logs ActionFailed, done - → veto_check(action, trader_id) → logs VetoChecked event - → If veto fails: logs ActionFailed, done - → wallet.sign() → logs WalletSigned event - → execute_on_chain() → logs ExecutedOnChain event - → All events appended to ActionLedger atomically per stage - -3. Trader polls get_action_status(action_id) - → Reads latest ActionEvent for action_id - → Returns current state: Submitted | LawFailed | VetoFailed | Signing | Executing | Confirmed | Failed - -4. Trader subscribes to subscribe_trader_events(trader_id) - → Receives stream of ActionEvents filtered to this trader - → Can react in real-time as pipeline progresses - -5. To reconstruct history: - → Admin calls replay_events(action_id_start, action_id_end) - → Ledger returns all events in order - → Can rebuild marketplace state at any point in history -``` - -### Error Handling - -```rust -pub enum ActionFailureReason { - WalletNotBound { trader_id: TradeId }, - LawViolated { - violations: Vec, - }, - Vetoed { - reason: String, - }, - WalletSignFailed { - reason: String, - }, - ChainExecutionFailed { - tx_hash: Hash256, - reason: String, - }, - Timeout, -} - -// Errors returned immediately from submit_action -pub enum SubmitError { - WalletNotBound, - InvalidAction, // serde fail, etc. -} -``` - -### Law Script Invocation - -**In-process, but with event logging.** Law engine is the same deterministic evaluator, but each check is wrapped: - -```rust -fn law_check_and_log( - action_id: ActionId, - action: &MarketAction, - ledger: &mut ActionLedger, -) -> Result<(), LawViolation> { - let result = LAW_ENGINE.evaluate(action); - ledger.append(ActionEvent::LawChecked { - action_id, - passed: result.is_ok(), - violations: result.err().unwrap_or_default(), - }); - result -} -``` - -### Extensibility - -**Adding new action types:** -1. Add variant to `MarketAction` enum -2. Update law script -3. Restart (law script immutable) -4. All existing event ledger still valid (events are self-describing) -5. New action_ids will log with new action type - -**Adding new pipeline stages:** -1. Define new `ActionEvent` variant -2. Insert processing stage in pipeline -3. Append event to ledger on completion -4. Replay logic automatically includes new stage - -**Adding constraints dynamically:** -1. Law script restart required (immutable at runtime) -2. BUT: can add inspection steps to pipeline without touching law script - - E.g., add `ActionInspected { reason: String }` event - - Pipeline can short-circuit on inspection without law violation - -### Testability - -```rust -#[cfg(test)] -mod tests { - #[test] - fn test_law_violation_logged() { - let action = MarketAction::Buy { quantity: 100, ... }; - let mut ledger = ActionLedger::new(); - let action_id = ActionId::new(); - - let result = law_check_and_log(action_id, &action, &mut ledger); - - assert!(result.is_err()); - let event = ledger.get(action_id); - assert!(matches!(event, ActionEvent::LawChecked { passed: false, ... })); - } - - #[test] - fn test_replay_reconstructs_state() { - let mut ledger = ActionLedger::new(); - // Simulate 10 actions - for i in 0..10 { - let action = MarketAction::Buy { quantity: i, ... }; - let action_id = ActionId::new(); - simulate_action_pipeline(action_id, action, &mut ledger); - } - - // Replay from action 3 to 7 - let replayed = replay_events(3, 7, &ledger); - assert_eq!(replayed.len(), 4); // events for actions 3,4,5,6,7 - } - - #[test] - fn test_action_pipeline_order() { - let mut ledger = ActionLedger::new(); - let action_id = ActionId::new(); - let action = MarketAction::Buy { quantity: 5, ... }; - - process_action(action_id, action, &mut ledger); - - let events = ledger.get_events_for(action_id); - // Verify order: Submitted → LawChecked → VetoChecked → WalletSigned → ExecutedOnChain - assert_eq!(events[0].variant(), "Submitted"); - assert_eq!(events[1].variant(), "LawChecked"); - assert_eq!(events[2].variant(), "VetoChecked"); - assert_eq!(events[3].variant(), "WalletSigned"); - assert_eq!(events[4].variant(), "ExecutedOnChain"); - } - - #[test] - fn test_subscribe_filters_by_trader() { - let mut marketplace = Marketplace::new(); - let trader_a = TradeId::new(); - let trader_b = TradeId::new(); - - marketplace.submit_action(trader_a, wallet_a, action_a); - marketplace.submit_action(trader_b, wallet_b, action_b); - - let events_a = marketplace.subscribe_trader_events(trader_a).collect(); - assert_eq!(events_a.len(), 1); - assert_eq!(events_a[0].trader_id, trader_a); - } - - #[test] - fn test_event_immutability() { - let mut ledger = ActionLedger::new(); - let action_id = ActionId::new(); - - let event1 = ActionEvent::Submitted { action_id, ... }; - ledger.append(event1.clone()); - - let retrieved = ledger.get(action_id); - assert_eq!(retrieved, event1); - - // Ledger is append-only; no mutation - // (Rust type system enforces this via &mut references) - } - - #[test] - fn test_concurrent_submits_order_preserved() { - let marketplace = Arc::new(Marketplace::new()); - let handles: Vec<_> = (0..100) - .map(|i| { - let mp = Arc::clone(&marketplace); - thread::spawn(move || { - mp.submit_action(trader_id, wallet_id, action) - }) - }) - .collect(); - - let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect(); - - // All ActionIds are unique - let ids: HashSet<_> = results.iter().map(|r| r.action_id).collect(); - assert_eq!(ids.len(), 100); - - // Ledger maintains order - for i in 0..100 { - assert!(ledger.get_events_for(results[i].action_id)[0].timestamp - <= ledger.get_events_for(results[i+1].action_id)[0].timestamp); - } - } - - #[test] - fn test_veto_blocks_at_veto_stage() { - let mut ledger = ActionLedger::new(); - let action = MarketAction::Buy { quantity: 5, ... }; - let action_id = ActionId::new(); - - // Mock Conductor to veto - conductor_mock.set_veto(true); - - process_action(action_id, action, &mut ledger); - - let events = ledger.get_events_for(action_id); - // Should have: Submitted, LawChecked (pass), VetoChecked (veto), ActionFailed - assert!(events.iter().any(|e| matches!(e, ActionEvent::VetoChecked { approved: false, .. }))); - assert!(events.iter().any(|e| matches!(e, ActionEvent::ActionFailed { .. }))); - // Should NOT have WalletSigned or ExecutedOnChain - assert!(!events.iter().any(|e| matches!(e, ActionEvent::WalletSigned { .. }))); - } -} -``` - -**Strengths:** -- Complete auditability via immutable ledger -- Replay enables reconstruction and testing -- Event streaming allows real-time trader feedback -- Pipeline stages are decoupled and testable in isolation -- Concurrent actions are ordered and non-interfering - ---- - -## Design C: GraphQL-Style Query/Mutation with Subscriptions - -**Philosophy:** Strong typing, introspectable schema, live data subscriptions. Traders issue mutations to submit actions, queries to read state, subscriptions to stream prediction updates in real-time. - -### GraphQL Schema - -```graphql -# Input Types -input MarketActionInput { - type: ActionType! # BUY | SELL | MINT | ... - asset: String! - quantity: Float! - price: Float - slippage: Float - chainId: Int -} - -input CapabilityCredential { - traderId: String! - walletId: String! - nonce: String! - signature: String! # Ed25519(concat(traderId, walletId, nonce, timestamp)) -} - -# Scalar Types -scalar Timestamp -scalar Hash256 -scalar TradeId -scalar WalletId -scalar ActionId - -# Enum Types -enum ActionType { - BUY - SELL - MINT - PROVIDE_LIQUIDITY - WITHDRAW_LIQUIDITY - CLAIM_REWARDS -} - -enum TxStatus { - PENDING - CONFIRMED - FAILED - TIMEOUT -} - -enum PredictionSource { - STATISTICAL_SIM - AMM_LIQUIDITY_SIM - MEV_ADVERSARIAL_SIM - TOKENOMICS_MACRO_SIM - SOCIOLOGICAL_SIM - CONSENSUS_STAKING_SIM - MICROSTRUCTURE_SIM -} - -# Object Types -type MarketAction { - id: ActionId! - traderId: TradeId! - walletId: WalletId! - type: ActionType! - asset: String! - quantity: Float! - price: Float - slippage: Float - submittedAt: Timestamp! - executedAt: Timestamp -} - -type ExecutionReceipt { - actionId: ActionId! - txHash: Hash256! - blockNumber: Int! - status: TxStatus! - gasUsed: Int - gasPrice: Float - confirmedAt: Timestamp -} - -type BoundedPrediction { - source: PredictionSource! - asset: String! - predictedPrice: Float! - confidence: Float! # 0.0 to 1.0 - interval: PredictionInterval! - generatedAt: Timestamp! -} - -type PredictionInterval { - lowerBound: Float! - upperBound: Float! - confidenceLevel: Float! # e.g., 0.95 for 95% -} - -type WalletState { - walletId: WalletId! - traderId: TradeId! - chains: [ChainBalance!]! - spendingLimitDaily: Float! - spendingLimitPerTx: Float! - usedTodayUSD: Float! - lastUsedAt: Timestamp -} - -type ChainBalance { - chainId: Int! - chainName: String! - assets: [AssetBalance!]! -} - -type AssetBalance { - symbol: String! - amount: Float! - usdValue: Float! -} - -type ActionValidationResult { - valid: Boolean! - lawViolations: [LawViolation!] - vetoReason: String -} - -type LawViolation { - ruleId: String! - description: String! - severity: String! # BLOCK | WARNING -} - -type MarketSnapshot { - timestamp: Timestamp! - predictions: [BoundedPrediction!]! - gasPrice: Float! - slippageEstimate: Float! -} - -# Root Query Type -type Query { - # Get current market snapshot - marketSnapshot: MarketSnapshot! - - # Get predictions for specific sim or all - predictions( - source: PredictionSource - asset: String - limit: Int = 10 - ): [BoundedPrediction!]! - - # Get wallet state for authenticated trader - walletState(walletId: WalletId!): WalletState! - - # Get specific action status - action(actionId: ActionId!): MarketAction - - # List trader's recent actions - traderActions( - traderId: TradeId! - limit: Int = 50 - offset: Int = 0 - status: TxStatus - ): [MarketAction!]! - - # Validate action before submission - validateAction( - action: MarketActionInput! - traderId: TradeId! - ): ActionValidationResult! - - # Check wallet binding - isWalletBound(traderId: TradeId!): Boolean! - - # Get execution history - executionHistory( - traderId: TradeId! - from: Timestamp - to: Timestamp - ): [ExecutionReceipt!]! -} - -# Root Mutation Type -type Mutation { - # Submit a market action - submitAction( - action: MarketActionInput! - walletId: WalletId! - credential: CapabilityCredential! - ): SubmitActionResult! - - # Bind wallet to trader (admin/initialization only) - bindWallet( - traderId: TradeId! - walletId: WalletId! - adminSignature: String! - ): BindWalletResult! - - # Pause trader (Conductor only) - pauseTrader( - traderId: TradeId! - reason: String! - adminSignature: String! - ): PauseTraderResult! - - # Resume trader (Conductor only) - resumeTrader( - traderId: TradeId! - adminSignature: String! - ): ResumeTraderResult! -} - -union SubmitActionResult = ExecutionReceipt | ActionError -union BindWalletResult = BindSuccess | BindError - -type ActionError { - code: String! # LAW_VIOLATION | VETO | WALLET_ERROR | TIMEOUT - message: String! - violations: [LawViolation!] -} - -type BindSuccess { - traderId: TradeId! - walletId: WalletId! - boundAt: Timestamp! -} - -type BindError { - code: String! - message: String! -} - -type PauseTraderResult { - traderId: TradeId! - pausedAt: Timestamp! - reason: String! -} - -type ResumeTraderResult { - traderId: TradeId! - resumedAt: Timestamp! -} - -# Root Subscription Type -type Subscription { - # Live predictions for asset (push to traders) - predictionUpdates( - source: PredictionSource - asset: String! - ): BoundedPrediction! - - # Action status changes (push to trader) - actionStatus( - actionId: ActionId! - ): MarketAction! - - # Wallet balance changes (push to trader) - walletBalanceChanged( - walletId: WalletId! - ): WalletState! - - # Marketplace events (admin only) - marketplaceEvents( - adminToken: String! - ): MarketplaceEvent! -} - -type MarketplaceEvent { - timestamp: Timestamp! - type: String! # ActionSubmitted | LawViolation | Veto | ExecutionFailed - actionId: ActionId - details: String! -} -``` - -### Trader → Marketplace Flow (Example) - -```graphql -# Step 1: Query current market state and predictions -query GetMarketSnapshot { - marketSnapshot { - timestamp - predictions(asset: "ETH") { - source - predictedPrice - confidence - interval { - lowerBound - upperBound - } - } - gasPrice - slippageEstimate - } - walletState(walletId: "wallet_123") { - chains { - chainName - assets { - symbol - amount - usdValue - } - } - spendingLimitDaily - usedTodayUSD - } -} - -# Step 2: Validate action before submission -query ValidateAction { - validateAction( - action: { - type: BUY - asset: "ETH" - quantity: 1.5 - price: 2500 - slippage: 0.01 - chainId: 1 - } - traderId: "trader_456" - ) { - valid - lawViolations { - ruleId - description - severity - } - vetoReason - } -} - -# Step 3: Submit action (if validated) -mutation SubmitBuyAction { - submitAction( - action: { - type: BUY - asset: "ETH" - quantity: 1.5 - price: 2500 - slippage: 0.01 - chainId: 1 - } - walletId: "wallet_123" - credential: { - traderId: "trader_456" - walletId: "wallet_123" - nonce: "uuid_12345" - signature: "ed25519_sig_..." - } - ) { - ... on ExecutionReceipt { - actionId - txHash - blockNumber - status - confirmedAt - } - ... on ActionError { - code - message - violations { - ruleId - description - } - } - } -} - -# Step 4: Subscribe to live updates -subscription MonitorPredictions { - predictionUpdates(asset: "ETH") { - source - predictedPrice - confidence - generatedAt - } -} - -# Step 5: Subscribe to action completion -subscription MonitorAction { - actionStatus(actionId: "action_789") { - id - status - executedAt - } -} -``` - -### Error Handling - -Errors are represented in the GraphQL schema: - -```graphql -# Example error response to submitAction -{ - "data": { - "submitAction": { - "__typename": "ActionError", - "code": "LAW_VIOLATION", - "message": "Action violates marketplace law", - "violations": [ - { - "ruleId": "P001", - "description": "position_limit_exceeded", - "severity": "BLOCK" - } - ] - } - } -} - -# Example for timeout -{ - "errors": [ - { - "message": "Chain RPC timeout after 30s", - "extensions": { - "code": "RPC_TIMEOUT" - } - } - ] -} - -# Example for veto -{ - "data": { - "submitAction": { - "__typename": "ActionError", - "code": "VETO", - "message": "Action was vetoed by Conductor", - "violations": null - } - } -} -``` - -### Law Script Invocation - -**In-process + lazy validation.** When `validateAction` query is called: - -```rust -fn validate_action_graphql( - action: &MarketActionInput, - trader_id: &TradeId, -) -> ActionValidationResult { - let market_action = action.to_market_action(); - - let law_result = LAW_ENGINE.evaluate(&market_action); - let violations = match law_result { - Ok(_) => vec![], - Err(v) => v, - }; - - let conductor_result = conductor.check_veto(&market_action, trader_id).await; - let veto_reason = match conductor_result { - Ok(_) => None, - Err(reason) => Some(reason), - }; - - ActionValidationResult { - valid: violations.is_empty() && veto_reason.is_none(), - lawViolations: violations, - vetoReason: veto_reason, - } -} -``` - -Then in `submitAction` mutation, law and veto checks are re-run (cannot bypass by skipping validation). - -### Extensibility - -**Adding new action types:** -1. Add variant to `ActionType` enum in GraphQL schema -2. Add to `MarketAction` enum in code -3. Update law script -4. GraphQL schema is versioned; can provide schema migration path -5. Clients are type-safe; old clients get schema validation error if they use removed types - -**Adding new simulation:** -1. Add variant to `PredictionSource` enum -2. Sim hub publishes predictions via data feeds (M2) -3. Marketplace collects in memory -4. `predictions` query automatically includes new source -5. Subscriptions automatically include new source via existing `predictionUpdates` subscription - -**Adding new constraints:** -1. Update law script -2. Restart (law immutable) -3. `validateAction` and `submitAction` automatically enforce -4. Optionally add new fields to `ActionValidationResult` for detailed reporting - -### Testability - -```rust -#[cfg(test)] -mod tests { - use crate::graphql::*; - - #[tokio::test] - async fn test_validate_action_law_violation() { - let client = setup_test_client().await; - - let query = r#" - query { - validateAction( - action: { type: BUY, asset: "ETH", quantity: 100, slippage: 0.01, chainId: 1 } - traderId: "trader_1" - ) { - valid - lawViolations { ruleId, description } - } - } - "#; - - let response = client.query(query).await; - assert_eq!(response.data.validate_action.valid, false); - assert!(response.data.validate_action.law_violations.len() > 0); - assert_eq!(response.data.validate_action.law_violations[0].rule_id, "P001"); - } - - #[tokio::test] - async fn test_submit_action_success() { - let client = setup_test_client().await; - let credential = gen_credential("trader_1", "wallet_1"); - - let mutation = r#" - mutation { - submitAction( - action: { type: BUY, asset: "ETH", quantity: 1, slippage: 0.01, chainId: 1 } - walletId: "wallet_1" - credential: { ... } - ) { - ... on ExecutionReceipt { - actionId - txHash - status - } - ... on ActionError { - code - message - } - } - } - "#; - - let response = client.mutation(mutation, credential).await; - assert!(matches!(response.data.submit_action, ExecutionReceipt { .. })); - } - - #[tokio::test] - async fn test_veto_blocks_submission() { - let client = setup_test_client().await; - conductor_mock.set_veto(true); - - let response = client.mutation(submit_action_mutation, credential).await; - - assert!(matches!(response.data.submit_action, ActionError { code: "VETO", .. })); - } - - #[tokio::test] - async fn test_subscription_prediction_updates() { - let client = setup_test_client().await; - - let subscription = r#" - subscription { - predictionUpdates(asset: "ETH") { - source - predictedPrice - confidence - } - } - "#; - - let mut stream = client.subscribe(subscription).await; - - // Sim hub publishes new prediction - sim_hub.publish_prediction(Prediction { asset: "ETH", ... }); - - // Marketplace forwards via subscription - let event = stream.next().await.unwrap(); - assert_eq!(event.prediction_updates.asset, "ETH"); - } - - #[tokio::test] - async fn test_subscription_action_status() { - let client = setup_test_client().await; - - let subscription = r#" - subscription { - actionStatus(actionId: "action_1") { - id - status - executedAt - } - } - "#; - - let mut stream = client.subscribe(subscription).await; - - // Submit action (in parallel) - client.mutation(submit_action_mutation, credential).await; - - // Subscription pushes updates as action progresses - let update1 = stream.next().await.unwrap(); - assert_eq!(update1.action_status.status, "PENDING"); - - let update2 = stream.next().await.unwrap(); - assert_eq!(update2.action_status.status, "CONFIRMED"); - } - - #[tokio::test] - async fn test_wallet_not_bound_error() { - let client = setup_test_client().await; - - let query = r#" - query { - isWalletBound(traderId: "unbound_trader") - } - "#; - - let response = client.query(query).await; - assert_eq!(response.data.is_wallet_bound, false); - } - - #[tokio::test] - async fn test_schema_introspection() { - let client = setup_test_client().await; - let schema = client.introspect().await; - - // Verify schema contains expected types - assert!(schema.types.iter().any(|t| t.name == "ActionType")); - assert!(schema.types.iter().any(|t| t.name == "BoundedPrediction")); - assert!(schema.types.iter().any(|t| t.name == "MarketAction")); - } - - #[tokio::test] - async fn test_concurrent_mutations_ordered() { - let client = setup_test_client().await; - - let futures = (0..10) - .map(|i| { - let credential = gen_credential(&format!("trader_{}", i), "wallet_1"); - client.mutation(submit_action_mutation, credential) - }) - .collect::>(); - - let results = futures::future::join_all(futures).await; - - // All actions succeed and have unique actionIds - let ids: HashSet<_> = results - .iter() - .filter_map(|r| match &r.data.submit_action { - ExecutionReceipt { action_id, .. } => Some(action_id.clone()), - _ => None, - }) - .collect(); - - assert_eq!(ids.len(), 10); - } -} -``` - -**Strengths:** -- Type-safe schema; introspectable -- Live subscriptions for real-time prediction + action feedback -- Separation of query (read), mutation (write), subscription (stream) is semantic -- GraphQL client libraries available for multiple languages -- Testable via standard GraphQL testing libraries -- Strong validation at schema level - ---- - -## Recommendation: Design B (Event-Sourced Ledger) - -### Reasoning - -**Design A** (capability tokens) is elegant for minimalist security: stateless, fast, HMAC-verified. However: -- No visibility into why actions fail during processing -- Traders must poll aggressively to learn outcome -- Audit trail is implicit; difficult for SAE (M7) to reconstruct behavior -- Hard to debug timing issues or predict when an action will complete - -**Design C** (GraphQL) is powerful for type safety and real-time UX: -- Strong introspection and schema evolution -- Live subscriptions are a genuine win for trader feedback -- However, GraphQL adds complexity (parsing, validation, multiple HTTP/WS layers) -- For a marketplace that prioritizes **determinism and auditability** (S99 vault, immutable law), the schema-freedom of GraphQL is actually a liability -- Harder to ensure all transactions follow the exact same pipeline order - -**Design B (Event-Sourced Ledger)** wins because: - -1. **Auditability (S3 / Invariant Provenance):** Every action is logged as an immutable event. SAE (M7) receives not just action outcomes but the exact pipeline stage at which decisions were made. - -2. **Replay & Reconstruction:** If a bug is discovered post-execution, the ledger can be replayed to understand state at any point. Critical for a marketplace handling real economic value. - -3. **Trader Transparency:** Traders can poll `get_action_status(action_id)` to see exactly where their action is in the pipeline (Submitted → LawChecked → VetoChecked → Signing → Executing → Confirmed). No guessing. - -4. **Concurrency-Safe:** Events are appended atomically. No race conditions on event order. Marketplace state can be reconstructed deterministically from the ledger. - -5. **Law Immutability (S99):** Law changes require restart + signatures. Event ledger makes it obvious when law version changed (historical events reference rule versions). - -6. **Extensibility via Events:** Adding new pipeline stages (e.g., "BalanceSnapshot" before veto, "GasEstimate" before signing) doesn't break existing API — new events just appear in the ledger. - -7. **Natural Fit for M7 (SAE):** SAE is designed to analyze actions for behavioral anomalies. Event ledger is a SAE-friendly input: immutable, ordered, complete. - -8. **Testability:** Each pipeline stage can be unit-tested by inspecting events. Concurrent submissions are serialized in the ledger for deterministic testing. - -**Trade-offs:** -- Slightly higher latency than Design A (events are async-logged) -- More complex than Design A (multiple event types to manage) -- No live subscriptions out-of-the-box like Design C (but easily added via WebSocket stream on top of ledger) - -**Hybrid option:** Add WebSocket subscriptions to Design B (stream ledger events) to get Design C's real-time feedback while keeping Design B's auditability. This is the "B+" approach. - -### Implementation Path - -1. **Phase 1 (MVP):** Implement Design B core: - - `submit_action()` creates ActionId, logs Submitted event - - Background pipeline processes synchronously for now - - `get_action_status()` queries ledger - - Ledger stored in-memory (persist to disk post-MVP) - -2. **Phase 2 (Robustness):** - - Persist ledger to durable store (RocksDB or SQLite) - - Implement `replay_events()` for audit - - Add admin replay tool - -3. **Phase 3 (UX):** - - Add WebSocket subscriptions on top (stream events as they arrive) - - Traders can subscribe to `traderEvents` for live updates - - Minimal API change; retroactively solves Design C's real-time requirement - ---- - -## Summary Table - -| Criterion | Design A (Tokens) | Design B (Ledger) | Design C (GraphQL) | -|-----------|-------------------|-------------------|--------------------| -| **Auditability** | Implicit | Explicit (ledger) | Query-based | -| **Replay** | None | Full (replay API) | None | -| **Latency** | Low (HMAC verify) | Medium (event log) | Medium (GQL parsing) | -| **Type Safety** | Weak (tokens are strings) | Medium (Rust enums) | Strong (GraphQL schema) | -| **Real-time UX** | Polling only | Polling + streams | Subscriptions (native) | -| **Concurrency Safety** | High (stateless) | High (atomic events) | Medium (query ordering) | -| **SAE Integration** | Hard (no trace) | Easy (event stream) | Medium (query state) | -| **Extensibility** | Restart required | Event types | Schema versioning | -| **Testability** | Unit-testable | Excellent (replay) | Schema-testable | -| **Fit with S3/S99** | Poor | **Excellent** | Good | -| **Recommendation** | Specialist use | **RECOMMENDED** | Alternative | - ---- +if only you fuckingnlistened