- M1 law script format decision doc recommends S-expressions with fixed combinators for non-Turing provability, auditability, and immutability at runtime (S99). Includes example constitution with all L1-L5 invariants. - M1 trader-wallet-marketplace API designs: compare 3 radically different shapes (minimal tokens, event-sourced ledger, GraphQL). Recommend Design B (ledger) for auditability, replay capability, and SAE integration. - Ada config: core_config.gpr with GNAT 2022 compiler flags. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
34 KiB
Trader-Wallet-Marketplace API Surface Designs
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
// 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<ExecutionReceipt, ViolationOrVeto>
// Single query endpoint: predictions + wallet state
fn query_market_snapshot(
token: CapabilityToken,
) -> Result<MarketSnapshot, TokenExpiredError>
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
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<Hash256>, // 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:
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:
- Add variant to
MarketActionenum - Update law script with new rules
- Restart marketplace (law script is immutable; changes require restart + operator + Homunculus signatures)
- Traders regenerate capability tokens with new
action_class
Adding new constraint:
- Edit law script file
- Restart marketplace with signatures
- 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
#[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
// Submit action, get action_id immediately
fn submit_action(
trader_id: TradeId,
wallet_id: WalletId,
action: MarketAction,
) -> Result<ActionId, WalletNotBound>
// Poll for action status
fn get_action_status(
action_id: ActionId,
trader_id: TradeId, // verify trader owns this action
) -> Result<ActionEvent, ActionNotFound>
// Stream events (trader sees own actions only)
fn subscribe_trader_events(
trader_id: TradeId,
) -> EventStream<ActionEvent>
// Query predictions (snapshot at call time)
fn get_predictions(
sim_type: Option<SimType>,
) -> Result<Vec<BoundedPrediction>, SimNotReady>
// Query wallet state (snapshot)
fn get_wallet_state(
wallet_id: WalletId,
trader_id: TradeId,
) -> Result<WalletState, WalletNotBound>
// Admin: replay events from ledger (for audit/reconstruction)
fn replay_events(
from_action_id: ActionId,
to_action_id: ActionId,
) -> Result<Vec<ActionEvent>, OutOfBounds>
Event Ledger Structure
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<LawViolation>,
},
VetoChecked {
action_id: ActionId,
approved: bool,
veto_reason: Option<String>,
},
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<ActionEvent>, // 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
pub enum ActionFailureReason {
WalletNotBound { trader_id: TradeId },
LawViolated {
violations: Vec<LawViolation>,
},
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:
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:
- Add variant to
MarketActionenum - Update law script
- Restart (law script immutable)
- All existing event ledger still valid (events are self-describing)
- New action_ids will log with new action type
Adding new pipeline stages:
- Define new
ActionEventvariant - Insert processing stage in pipeline
- Append event to ledger on completion
- Replay logic automatically includes new stage
Adding constraints dynamically:
- Law script restart required (immutable at runtime)
- 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
- E.g., add
Testability
#[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
# 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)
# 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:
# 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:
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:
- Add variant to
ActionTypeenum in GraphQL schema - Add to
MarketActionenum in code - Update law script
- GraphQL schema is versioned; can provide schema migration path
- Clients are type-safe; old clients get schema validation error if they use removed types
Adding new simulation:
- Add variant to
PredictionSourceenum - Sim hub publishes predictions via data feeds (M2)
- Marketplace collects in memory
predictionsquery automatically includes new source- Subscriptions automatically include new source via existing
predictionUpdatessubscription
Adding new constraints:
- Update law script
- Restart (law immutable)
validateActionandsubmitActionautomatically enforce- Optionally add new fields to
ActionValidationResultfor detailed reporting
Testability
#[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::<Vec<_>>();
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:
-
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.
-
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.
-
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. -
Concurrency-Safe: Events are appended atomically. No race conditions on event order. Marketplace state can be reconstructed deterministically from the ledger.
-
Law Immutability (S99): Law changes require restart + signatures. Event ledger makes it obvious when law version changed (historical events reference rule versions).
-
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.
-
Natural Fit for M7 (SAE): SAE is designed to analyze actions for behavioral anomalies. Event ledger is a SAE-friendly input: immutable, ordered, complete.
-
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
-
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)
-
Phase 2 (Robustness):
- Persist ledger to durable store (RocksDB or SQLite)
- Implement
replay_events()for audit - Add admin replay tool
-
Phase 3 (UX):
- Add WebSocket subscriptions on top (stream events as they arrive)
- Traders can subscribe to
traderEventsfor 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 |