/** * Public request and response models for the RailX V0 API. * * This file is the single source of truth for the wire contract. The * application re-exports it from `src/core/types.ts`, so services, route * handlers, the OpenAPI document, and the SDK all describe the same shapes. * It has no runtime imports so it can ship inside the SDK unchanged. */ export type Address = `0x${string}`; export type Hex = `0x${string}`; export const ROBINHOOD_CHAIN_ID = 4663; export type RobinhoodChainId = typeof ROBINHOOD_CHAIN_ID; export type ApiVersion = 'v0'; // --------------------------------------------------------------------------- // Envelopes // --------------------------------------------------------------------------- export interface ApiMeta { requestId: string; version: ApiVersion; generatedAt: string; } export interface ApiSuccess { data: T; meta: ApiMeta; } export interface ApiErrorBody { code: string; message: string; retryable: boolean; details: unknown; } export interface ApiFailure { error: ApiErrorBody; meta: ApiMeta; } // --------------------------------------------------------------------------- // Assets and prices // --------------------------------------------------------------------------- export type AssetStatus = 'active' | 'inactive' | 'unspecified'; export interface AssetDeployment { chainId: number; contractAddress: Address; } export interface CanonicalAsset { id: string; symbol: string; name: string; status: AssetStatus; currentMultiplier: string; pendingMultiplier: string | null; pendingMultiplierEffectiveTime: string | null; deployments: AssetDeployment[]; logoUrl: string | null; tradingCapabilities: Record | null; provenance: { source: 'robinhood'; sourceUrl: string; retrievedAt: string; }; } export interface AssetList { assets: CanonicalAsset[]; next: string | null; total: number; } export type CorporateActionType = | 'forward_split' | 'reverse_split' | 'cash_dividend' | 'stock_dividend' | 'spin_off' | 'cash_merger' | 'stock_merger' | 'stock_and_cash_merger' | 'redemption' | 'name_change' | 'worthless_removal' | 'rights_distribution' | 'unit_split' | 'unspecified'; export type CorporateActionStatus = 'in_progress' | 'completed' | 'unspecified'; export interface CorporateAction { id: string; type: CorporateActionType; /** Original Robinhood enum, retained for forward compatibility. */ sourceType: string; status: CorporateActionStatus; /** ISO calendar date (YYYY-MM-DD), or null when Robinhood has not scheduled it. */ processDate: string | null; asset: { symbol: string; deployments: AssetDeployment[]; }; /** Exactly one Robinhood-defined variant is present for known action types. */ details: Record; provenance: { source: 'robinhood'; sourceUrl: string; retrievedAt: string; }; } export interface CorporateActionList { actions: CorporateAction[]; total: number; } export interface SystemToken { kind: 'system'; symbol: string; name: string; address: Address; decimals: number; chainId: number; } export interface MarketToken { kind: 'market'; symbol: string; name: string; address: Address; decimals: number; chainId: number; asset: CanonicalAsset; } export type ResolvedToken = SystemToken | MarketToken; export interface PriceObservation { asset: Pick; currency: string; rawUnderlying: { bid: string; ask: string; mid: string; source: 'robinhood-rest'; }; tokenEquivalent: { bid: string; ask: string; mid: string; multiplier: string; calculation: 'underlying_price_x_current_multiplier'; }; isTradingHalt: boolean; dailyTradingVolume: string; generatedAt: string; retrievedAt: string; } // --------------------------------------------------------------------------- // Portfolios // --------------------------------------------------------------------------- export interface PortfolioPosition { asset: { kind: 'system' | 'market'; symbol: string; name: string; contractAddress: Address; decimals: number; }; balance: { raw: string; formatted: string }; /** Underlying quantity for market tokens (`balance × currentMultiplier`); null for system tokens. */ underlyingQuantity: string | null; } export interface Portfolio { wallet: Address; chainId: RobinhoodChainId; blockNumber: string; nativeBalance: { symbol: 'ETH'; raw: string; formatted: string }; positions: PortfolioPosition[]; scannedAssets: number; partial: boolean; warning: string | null; } // --------------------------------------------------------------------------- // Transactions and simulation // --------------------------------------------------------------------------- export interface UnsignedTransaction { chainId: number; from: Address; to: Address; data: Hex; value: string; gasLimit?: string; maxFeePerGas?: string; maxPriorityFeePerGas?: string; gasPrice?: string; } /** Client-supplied transaction for simulation; `from` may come from `wallet`. */ export interface SimulateTransactionInput { chainId?: number; from?: string; to: string; data?: string; value?: string; gasLimit?: string; gasPrice?: string; maxFeePerGas?: string; maxPriorityFeePerGas?: string; } export interface SimulateRequest { wallet?: string; transactions: SimulateTransactionInput[]; } export interface SimulationCallResult { index: number; status: 'passed' | 'failed' | 'not_run'; gasUsed: string | null; returnData: string | null; error: string | null; } export interface SimulationResult { status: 'passed' | 'failed' | 'partial'; mode: 'sequential' | 'independent'; blockNumber: string; calls: SimulationCallResult[]; warning: string | null; } // --------------------------------------------------------------------------- // Quotes and trade plans // --------------------------------------------------------------------------- export interface ExactInputTradeIntent { type: 'exact_input'; tokenIn?: string; tokenOut: string; amountIn: string; } export interface ExactOutputTradeIntent { type: 'exact_output'; tokenIn?: string; tokenOut: string; amountOut: string; } /** Preferred public request body for `POST /quotes` and `POST /trades/plan`. */ export interface IntentTradeRequest { wallet: string; intent: ExactInputTradeIntent | ExactOutputTradeIntent; /** Percent, 0.01–5. Defaults to 0.5. */ slippageTolerance?: number; } /** V0 compatibility shape. New integrations should use `IntentTradeRequest`. */ export interface LegacyTradeRequest { wallet: string; tokenOut: string; input: { /** Defaults to USDG. */ tokenIn?: string; /** Positive decimal string with at most 18 decimal places. */ amount: string; }; /** Percent, 0.01–5. Defaults to 0.5. */ slippageTolerance?: number; } export type TradeRequest = IntentTradeRequest | LegacyTradeRequest; export interface Quote { id: string; requestType: 'exact_input' | 'exact_output'; routing: string; tokenIn: ResolvedToken; tokenOut: ResolvedToken; amountIn: string; amountInBaseUnits: string; /** Maximum input the wallet can spend after applying the requested slippage tolerance. */ maximumAmountIn: string; maximumAmountInBaseUnits: string; amountOut: string | null; amountOutBaseUnits: string | null; /** Minimum output the transaction is required to deliver. */ minimumAmountOut: string; minimumAmountOutBaseUnits: string; slippageTolerance: number; receivedAt: string; expiresAt: string; provider: '0x-swap-api' | 'uniswap-trading-api'; providerRequestId: string | null; rawQuote: Record; permitData: Record | null; } export type TradePlanStepKind = 'cancel_approval' | 'approve' | 'swap'; export interface TradePlanStep { index: number; kind: TradePlanStepKind; transaction: UnsignedTransaction; } export type PolicyVerdict = 'allow' | 'warn' | 'block'; export interface PolicyCheck { source: 'rail_safety' | 'project_policy' | 'project_configuration'; verdict: PolicyVerdict; code: string; message: string; evidence: Record; } export interface TradePlanVerdict { outcome: PolicyVerdict; policyVersion: { id: string; version: number; schemaVersion: 1; } | null; checks: PolicyCheck[]; } export interface TokenTradeSizeLimit { token: string; warnAbove?: string; blockAbove?: string; } export interface PolicyThresholds { warnAbove?: number; blockAbove?: number; } export interface ProjectPolicyRulesV1 { schemaVersion: 1; allowedOutputAssets?: string[]; blockedOutputAssets?: string[]; allowedInputTokens?: string[]; tradeSizeLimits?: TokenTradeSizeLimit[]; slippageTolerance?: PolicyThresholds; } export interface ProjectPolicyVersion { id: string; projectId: string; version: number; schemaVersion: 1; rules: ProjectPolicyRulesV1; createdAt: number; active: boolean; } export interface TradePlan { id: string; status: 'ready' | 'review_required' | 'blocked'; chainId: RobinhoodChainId; wallet: Address; createdAt: string; expiresAt: string; intent: | { type: 'swap_exact_input'; tokenIn: string; tokenOut: string; amountIn: string; } | { type: 'swap_exact_output'; tokenIn: string; tokenOut: string; amountOut: string; }; quote: Quote; walletContext: { inputBalance: { raw: string; formatted: string; decimals: number }; sufficientBalance: true; }; steps: TradePlanStep[]; simulation: SimulationResult; verdict: TradePlanVerdict; signing: { mode: 'wallet_controlled'; submittedByRail: false; unsignedTransactions: UnsignedTransaction[]; }; } // --------------------------------------------------------------------------- // System // --------------------------------------------------------------------------- export interface ApiIndex { name: string; version: ApiVersion; chainId: RobinhoodChainId; documentation: string; endpoints: Record; } export interface HealthReport { status: 'ok' | 'degraded'; chainId: RobinhoodChainId; services: { robinhoodAssets: { configured: boolean }; robinhoodPrices: { configured: boolean }; robinhoodRpc: { configured: boolean; provider: string }; uniswapTradingApi: { configured: boolean }; zeroExSwapApi: { configured: boolean }; platformDatabase: { configured: boolean; schema: 'current' | 'behind' | 'not_configured'; }; railxApiAuthentication: { enabled: boolean; mode: 'project_keys' | 'disabled'; keyVerifierConfigured: boolean; }; }; } /** Scenario names accepted by `x-railx-scenario` on test-environment keys. */ export type SandboxScenario = | 'funded' | 'approved' | 'insufficient_balance' | 'zero_balance' | 'simulation_failed' | 'provider_degraded';