diff --git a/pkg/bungee-interface/README.md b/pkg/bungee-interface/README.md index 1eb4cc2..a130e93 100644 --- a/pkg/bungee-interface/README.md +++ b/pkg/bungee-interface/README.md @@ -11,8 +11,8 @@ the Bungee integration: public request / response types, and the wire-stable client error enum used by Guild and wallet clients. - `bungee_interface::api` contains the upstream REST API calque, including the - `BungeeApi` trait, transport error surface, response wrapper, and per-endpoint - DTOs. + `BungeeV1Api` and `SocketSwapV3Api` traits, transport error surface, response + wrapper, and per-endpoint DTOs. ## Stack Layout @@ -21,7 +21,8 @@ The shipped Bungee integration is split across three crates: - `bungee-interface` is the leaf crate that owns the shared domain contract and upstream API calque. - `bungee-client-http` depends only on `bungee-interface` and implements - `bungee_interface::api::BungeeApi` with reqwest transport. + `bungee_interface::api::BungeeV1Api` and + `bungee_interface::api::SocketSwapV3Api` with reqwest transport. - `bungee` depends only on `bungee-interface` and implements `bungee_interface::client::BungeeClient` with quote-selection and response normalization logic. @@ -39,6 +40,10 @@ tests in this crate cover request / response encoding and the public error payloads so schema drift is caught before it reaches downstream clients that roll out independently. +The only additive quote-output field introduced for Socket Swap v3 is optional +`min_output_amount`. It defaults to `None` when old persisted rows or old Guild +responses omit it. + ## Testing The crate keeps wire-compat snapshot coverage for the public domain types and diff --git a/pkg/bungee-interface/src/api/endpoints/mod.rs b/pkg/bungee-interface/src/api/endpoints/mod.rs index 3fe9dfe..1457553 100644 --- a/pkg/bungee-interface/src/api/endpoints/mod.rs +++ b/pkg/bungee-interface/src/api/endpoints/mod.rs @@ -2,6 +2,12 @@ pub mod build_tx; /// Quote endpoint types. pub mod quote; +/// Socket Swap v3 quote endpoint types. +pub mod socket_swap_quote; +/// Socket Swap v3 status endpoint types. +pub mod socket_swap_status; +/// Socket Swap v3 token-list endpoint types. +pub mod socket_swap_tokens_list; /// Status endpoint types. pub mod status; /// Token-list endpoint types. diff --git a/pkg/bungee-interface/src/api/endpoints/socket_swap_quote.rs b/pkg/bungee-interface/src/api/endpoints/socket_swap_quote.rs new file mode 100644 index 0000000..dce9f94 --- /dev/null +++ b/pkg/bungee-interface/src/api/endpoints/socket_swap_quote.rs @@ -0,0 +1,194 @@ +// lint-long-file-override allow-max-lines=300 +use bytes::Bytes; +use serde::{Deserialize, Serialize}; + +/// Request headers for `GET /v3/swap/quote`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct Headers {} + +/// Query parameters for `GET /v3/swap/quote`. +#[derive(Debug, Clone, Serialize)] +pub struct Query { + /// `userOps` + #[serde(rename = "userOps")] + pub user_ops: String, + /// `originChainId` + #[serde(rename = "originChainId")] + pub origin_chain_id: String, + /// `destinationChainId` + #[serde(rename = "destinationChainId")] + pub destination_chain_id: String, + /// `inputToken` + #[serde(rename = "inputToken")] + pub input_token: String, + /// `outputToken` + #[serde(rename = "outputToken")] + pub output_token: String, + /// `inputAmount` + #[serde(rename = "inputAmount")] + pub input_amount: String, + /// `receiverAddress` + #[serde(rename = "receiverAddress")] + pub receiver_address: String, + /// `userAddress` + #[serde(rename = "userAddress")] + pub user_address: String, + /// `slippage` + pub slippage: String, +} + +/// Response body variants for `GET /v3/swap/quote`. +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone)] +pub enum ResponseEnum { + /// Successful 200 response. + Ok200(QuoteResponse), + /// Any non-200 response with raw body bytes. + Unknown(u16, Bytes), +} + +/// Wrapped Socket Swap v3 quote response. +#[derive(Debug, Clone, Deserialize)] +pub struct QuoteResponse { + /// Success flag returned by the API. + pub success: bool, + /// HTTP-style status code reported by Socket. + #[serde(rename = "statusCode")] + pub status_code: u64, + /// Wrapped quote payload. + pub result: QuoteResult, + /// Optional upstream message. + pub message: Option, +} + +/// Quote result payload. +#[derive(Debug, Clone, Deserialize)] +pub struct QuoteResult { + /// Origin chain id. + #[serde(rename = "originChainId")] + pub origin_chain_id: u128, + /// Destination chain id. + #[serde(rename = "destinationChainId")] + pub destination_chain_id: u128, + /// User wallet address on the source chain. + #[serde(rename = "userAddress")] + pub user_address: String, + /// Receiver wallet address on the destination chain. + #[serde(rename = "receiverAddress")] + pub receiver_address: String, + /// Input amount and token details echoed by Socket. + pub input: Input, + /// Candidate routes. + #[serde(default)] + pub routes: Vec, +} + +/// Input amount and token details echoed by Socket. +#[derive(Debug, Clone, Deserialize)] +pub struct Input { + /// Input token details. + pub token: Token, + /// Input token amount. + pub amount: String, + /// USD value of the input amount. + #[serde(rename = "valueInUsd")] + pub value_in_usd: Option, + /// Price per unit in USD. + #[serde(rename = "priceInUsd")] + pub price_in_usd: Option, +} + +/// Token chain and address details. +#[derive(Debug, Clone, Deserialize)] +pub struct Token { + /// Token chain id. + #[serde(rename = "chainId")] + pub chain_id: u128, + /// Token contract address. + pub address: String, +} + +/// Socket Swap v3 route candidate. +#[derive(Debug, Clone, Deserialize)] +pub struct Route { + /// Route user operation type. + #[serde(rename = "userOp")] + pub user_op: String, + /// Socket quote id. + #[serde(rename = "quoteId")] + pub quote_id: String, + /// Unix timestamp after which the route should not be submitted. + #[serde(rename = "expiresAt")] + pub expires_at: Option, + /// Output amount information. + pub output: Output, + /// Estimated time to complete, in seconds. Socket returns this as a JSON + /// number that may be fractional (e.g. `9.5`), so it must be `f64`. + #[serde(rename = "estimatedTime")] + pub estimated_time: Option, + /// Route tags used for tie-break ranking. + #[serde(rename = "routeTags", default)] + pub route_tags: Vec, + /// Optional approval details required before submitting txData. + pub approval: Option, + /// Transaction data for the Socket route. + #[serde(rename = "txData")] + pub tx_data: TxData, +} + +/// Output amount wrapper. +#[derive(Debug, Clone, Deserialize)] +pub struct Output { + /// Output token details. + pub token: Token, + /// Expected output amount. + pub amount: String, + /// Guaranteed minimum output amount. + #[serde(rename = "minAmountOut")] + pub min_amount_out: String, + /// Price per unit in USD. + #[serde(rename = "priceInUsd")] + pub price_in_usd: Option, + /// USD value of the output amount. + #[serde(rename = "valueInUsd")] + pub value_in_usd: Option, +} + +/// ERC-20 approval details. +#[derive(Debug, Clone, Deserialize)] +pub struct Approval { + /// Spender address to approve. + #[serde(rename = "spenderAddress")] + pub spender_address: String, + /// Allowance amount. + pub amount: String, + /// Approved token address. + #[serde(rename = "tokenAddress")] + pub token_address: String, + /// User wallet address. + #[serde(rename = "userAddress")] + pub user_address: String, +} + +/// Socket route transaction data container. +#[derive(Debug, Clone, Deserialize)] +pub struct TxData { + /// Transaction data kind. + pub kind: String, + /// EVM transaction object. + pub object: TxObject, +} + +/// EVM transaction object returned under `txData.object`. +#[derive(Debug, Clone, Deserialize)] +pub struct TxObject { + /// Source chain id. + #[serde(rename = "chainId")] + pub chain_id: u128, + /// Transaction target. + pub to: String, + /// Calldata. + pub data: String, + /// Native value. + pub value: String, +} diff --git a/pkg/bungee-interface/src/api/endpoints/socket_swap_status.rs b/pkg/bungee-interface/src/api/endpoints/socket_swap_status.rs new file mode 100644 index 0000000..3cc4aff --- /dev/null +++ b/pkg/bungee-interface/src/api/endpoints/socket_swap_status.rs @@ -0,0 +1,66 @@ +use bytes::Bytes; +use serde::{Deserialize, Serialize}; + +/// Request headers for `GET /v3/swap/status`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct Headers {} + +/// Query parameters for `GET /v3/swap/status`. +#[derive(Debug, Clone, Serialize)] +pub struct Query { + /// `quoteId` + #[serde(rename = "quoteId", skip_serializing_if = "Option::is_none")] + pub quote_id: Option, + /// `srcTxHash` + #[serde(rename = "srcTxHash", skip_serializing_if = "Option::is_none")] + pub src_tx_hash: Option, +} + +/// Response body variants for `GET /v3/swap/status`. +#[derive(Debug, Clone)] +pub enum ResponseEnum { + /// Successful 200 response. + Ok200(StatusResponse), + /// Any non-200 response with raw body bytes. + Unknown(u16, Bytes), +} + +/// Wrapped Socket Swap v3 status response. +#[derive(Debug, Clone, Deserialize)] +pub struct StatusResponse { + /// Success flag returned by the API. + pub success: bool, + /// HTTP-style status code reported by Socket. + #[serde(rename = "statusCode")] + pub status_code: u64, + /// Wrapped status payload. + pub result: StatusResult, + /// Optional upstream message. + pub message: Option, +} + +/// Socket status result payload. +#[derive(Debug, Clone, Deserialize)] +pub struct StatusResult { + /// Socket quote id. + #[serde(rename = "quoteId")] + pub quote_id: String, + /// Route user operation type. + #[serde(rename = "userOp")] + pub user_op: String, + /// Coarse status. + pub status: String, + /// Granular status code. + #[serde(rename = "statusCode")] + pub status_code: String, + /// Destination leg data. + pub destination: Option, +} + +/// Destination leg status data. +#[derive(Debug, Clone, Deserialize)] +pub struct StatusDestination { + /// Destination transaction hash, if broadcast. + #[serde(rename = "txHash")] + pub tx_hash: Option, +} diff --git a/pkg/bungee-interface/src/api/endpoints/socket_swap_tokens_list.rs b/pkg/bungee-interface/src/api/endpoints/socket_swap_tokens_list.rs new file mode 100644 index 0000000..559d016 --- /dev/null +++ b/pkg/bungee-interface/src/api/endpoints/socket_swap_tokens_list.rs @@ -0,0 +1,95 @@ +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// Request headers for `GET /v3/swap/tokens/list`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct Headers {} + +/// Query parameters for `GET /v3/swap/tokens/list`. +#[derive(Debug, Clone, Serialize)] +pub struct Query { + /// `userAddress` + #[serde(rename = "userAddress", skip_serializing_if = "Option::is_none")] + pub user_address: Option, + /// `chainIds` + #[serde(rename = "chainIds", skip_serializing_if = "Option::is_none")] + pub chain_ids: Option, + /// `list` + #[serde(skip_serializing_if = "Option::is_none")] + pub list: Option, +} + +/// Response body variants for `GET /v3/swap/tokens/list`. +#[derive(Debug, Clone)] +pub enum ResponseEnum { + /// Successful 200 response. + Ok200(TokenListResponse), + /// Any non-200 response with raw body bytes. + Unknown(u16, Bytes), +} + +/// Wrapped Socket Swap v3 token-list response. +#[derive(Debug, Clone, Deserialize)] +pub struct TokenListResponse { + /// Indicates whether the upstream request succeeded. + pub success: bool, + /// HTTP-style status code reported by Socket. + #[serde(rename = "statusCode")] + pub status_code: u64, + /// Wrapped token list payload. + pub result: TokenListResult, + /// Optional upstream message. + pub message: Option, +} + +/// Result wrapper containing tokens grouped by chain id. +#[derive(Debug, Clone, Deserialize)] +pub struct TokenListResult { + /// Map of chain id string to token entries. + #[serde(flatten)] + pub tokens: BTreeMap>, +} + +/// Token metadata entry as returned by the Socket Swap v3 API. +#[derive(Debug, Clone, Deserialize)] +pub struct TokenListToken { + /// Chain id reported in the entry. + #[serde(rename = "chainId")] + pub chain_id: u64, + /// Token contract address. + pub address: String, + /// Human-readable token name. + pub name: String, + /// Token symbol ticker. + pub symbol: String, + /// Token decimals. + pub decimals: u8, + /// Optional logo URI for the token. + #[serde(rename = "logoURI")] + pub logo_uri: Option, + /// Whether the token is shortlisted. + #[serde(rename = "isShortListed", default)] + pub is_short_listed: bool, + /// Optional trending rank for the token. + #[serde(rename = "trendingRank")] + pub trending_rank: Option, + /// Optional token market cap in USD. + #[serde(rename = "marketCap")] + pub market_cap: Option, + /// Optional total volume in USD. + #[serde(rename = "totalVolume")] + pub total_volume: Option, + /// Token balance string for the provided user address. + #[serde(default)] + pub balance: String, + /// USD balance for the provided user address. + #[serde(rename = "balanceInUsd", default)] + pub balance_in_usd: f64, + /// Metadata tags. + #[serde(default)] + pub tags: Vec, + /// Whether the token is verified. + #[serde(rename = "isVerified", default)] + pub is_verified: bool, +} diff --git a/pkg/bungee-interface/src/api/mod.rs b/pkg/bungee-interface/src/api/mod.rs index 2fa9d81..6712a91 100644 --- a/pkg/bungee-interface/src/api/mod.rs +++ b/pkg/bungee-interface/src/api/mod.rs @@ -8,10 +8,10 @@ mod response; pub use error::TransportError; pub use response::{Response, ResponseHeaders}; -/// Typed calque trait for the upstream Bungee REST API. -#[unimock::unimock(api = BungeeApiMock)] +/// Typed calque trait for the upstream Bungee v1 REST API. +#[unimock::unimock(api = BungeeV1ApiMock)] #[async_trait] -pub trait BungeeApi: Send + Sync + 'static { +pub trait BungeeV1Api: Send + Sync + 'static { /// Call `GET /api/v1/bungee/quote`. async fn get_api_v1_bungee_quote( &self, @@ -40,3 +40,29 @@ pub trait BungeeApi: Send + Sync + 'static { query: Option, ) -> Result, TransportError>; } + +/// Typed calque trait for the Socket Swap v3 REST API. +#[unimock::unimock(api = SocketSwapV3ApiMock)] +#[async_trait] +pub trait SocketSwapV3Api: Send + Sync + 'static { + /// Call `GET /v3/swap/quote`. + async fn get_v3_swap_quote( + &self, + headers: Option, + query: Option, + ) -> Result, TransportError>; + + /// Call `GET /v3/swap/tokens/list`. + async fn get_v3_swap_tokens_list( + &self, + headers: Option, + query: Option, + ) -> Result, TransportError>; + + /// Call `GET /v3/swap/status`. + async fn get_v3_swap_status( + &self, + headers: Option, + query: Option, + ) -> Result, TransportError>; +} diff --git a/pkg/bungee-interface/src/client/quote.rs b/pkg/bungee-interface/src/client/quote.rs index 67e51b6..846b6ab 100644 --- a/pkg/bungee-interface/src/client/quote.rs +++ b/pkg/bungee-interface/src/client/quote.rs @@ -26,6 +26,9 @@ pub struct GetQuoteInput { pub struct GetQuoteOutput { /// Expected output amount. pub output_amount: U256, + /// Guaranteed minimum output amount. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_output_amount: Option, /// Inbox transaction target. pub tx_to: Address, /// Inbox transaction value in wei. diff --git a/pkg/bungee-interface/src/client/snapshots/bungee_interface__client__tests__quote_output.snap b/pkg/bungee-interface/src/client/snapshots/bungee_interface__client__tests__quote_output.snap index b5cad69..991cc57 100644 --- a/pkg/bungee-interface/src/client/snapshots/bungee_interface__client__tests__quote_output.snap +++ b/pkg/bungee-interface/src/client/snapshots/bungee_interface__client__tests__quote_output.snap @@ -5,6 +5,7 @@ expression: value { "approval_amount": "0x3e7", "approval_spender": "0x0000000000000000000000000000000000000006", + "min_output_amount": "0x190", "output_amount": "0x1c8", "quote_id": "qid-1", "request_hash": "rh-1", diff --git a/pkg/bungee-interface/src/client/status.rs b/pkg/bungee-interface/src/client/status.rs index c0dbf04..5b9a3d5 100644 --- a/pkg/bungee-interface/src/client/status.rs +++ b/pkg/bungee-interface/src/client/status.rs @@ -54,16 +54,22 @@ impl StatusIdentifier { } /// Input payload for checking the status of a submitted bridge. +/// +/// `tx_hash` (the source-chain submit tx) is the identifier used by the **v3** and +/// **v3+compat** backends (sent as `srcTxHash`) and is also a valid v1 identifier. +/// `request_hash`/`id` are **v1-mode-only** legacy identifiers: the v3/v3+compat status +/// path resolves by `tx_hash` only and ignores them. #[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct GetStatusInput { - /// Request hash returned by Bungee. + /// Request hash / quote id (v1-mode identifier; not used by v3/v3+compat). #[serde(skip_serializing_if = "Option::is_none")] pub request_hash: Option, - /// Manual route source chain transaction hash. + /// Source-chain submit transaction hash. The identifier for v3/v3+compat (as + /// `srcTxHash`) and a valid v1 identifier. #[serde(skip_serializing_if = "Option::is_none")] pub tx_hash: Option, - /// Alternate identifier accepted by the public API. + /// Alternate v1-mode identifier (not used by v3/v3+compat). #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, } diff --git a/pkg/bungee-interface/src/client/tests.rs b/pkg/bungee-interface/src/client/tests.rs index f700c97..4a20836 100644 --- a/pkg/bungee-interface/src/client/tests.rs +++ b/pkg/bungee-interface/src/client/tests.rs @@ -98,6 +98,7 @@ fn quote_input_round_trips_as_json() { fn quote_output_round_trips_as_json() { let output = GetQuoteOutput { output_amount: U256::from(456u64), + min_output_amount: Some(U256::from(400u64)), tx_to: sample_address(5), tx_value: U256::from(789u64), tx_data: vec![0xde, 0xad, 0xbe, 0xef], @@ -113,6 +114,24 @@ fn quote_output_round_trips_as_json() { assert_eq!(decoded, output); } +#[test] +fn quote_output_deserializes_without_min_output_amount() { + let value = json!({ + "output_amount": "0x1c8", + "tx_to": sample_address(5), + "tx_value": "0x315", + "tx_data": "0xdeadbeef", + "approval_spender": sample_address(6), + "approval_amount": "0x3e7", + "quote_id": "qid-1", + "request_hash": "rh-1" + }); + let decoded = + serde_json::from_value::(value).expect("deserialize legacy quote output"); + + assert_eq!(decoded.min_output_amount, None); +} + #[test] fn token_list_output_round_trips_as_json() { let output = GetTokenListOutput {