use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DefuseToken {
pub asset_id: String,
pub decimals: u8,
pub blockchain: String,
pub symbol: String,
pub price: f64,
pub price_updated_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contract_address: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum NearSwapType {
ExactInput,
FlexInput,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum NearDepositMode {
#[default]
Simple,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum NearDepositType {
#[default]
OriginChain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum NearRefundType {
#[default]
OriginChain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum NearRecipientType {
#[default]
DestinationChain,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NearAppFee {
pub recipient: String,
pub fee: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NearQuoteRequest {
pub dry: bool,
pub swap_type: NearSwapType,
pub deposit_mode: NearDepositMode,
pub slippage_tolerance: u32,
pub origin_asset: String,
pub deposit_type: NearDepositType,
pub destination_asset: String,
pub amount: String,
pub refund_to: String,
pub refund_type: NearRefundType,
pub recipient: String,
pub recipient_type: NearRecipientType,
pub deadline: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app_fees: Option<Vec<NearAppFee>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quote_waiting_time_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub referral: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub virtual_chain_recipient: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub virtual_chain_refund_recipient: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_recipient_msg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub connected_wallets: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NearQuote {
pub amount_in: String,
pub amount_in_formatted: String,
pub amount_in_usd: String,
pub min_amount_in: String,
pub amount_out: String,
pub amount_out_formatted: String,
pub amount_out_usd: String,
pub min_amount_out: String,
pub time_estimate: u64,
pub deadline: String,
pub time_when_inactive: String,
pub deposit_address: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NearQuoteResponse {
pub quote: NearQuote,
pub quote_request: NearQuoteRequest,
pub signature: String,
pub timestamp: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum NearExecutionStatus {
KnownDepositTx,
PendingDeposit,
IncompleteDeposit,
Processing,
Success,
Refunded,
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NearChainTxHash {
pub hash: String,
pub explorer_url: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NearSwapDetails {
pub intent_hashes: Vec<String>,
pub near_tx_hashes: Vec<String>,
pub amount_in: String,
pub amount_in_formatted: String,
pub amount_in_usd: String,
pub amount_out: String,
pub amount_out_formatted: String,
pub amount_out_usd: String,
pub slippage: f64,
pub refunded_amount: String,
pub refunded_amount_formatted: String,
pub refunded_amount_usd: String,
pub origin_chain_tx_hashes: Vec<NearChainTxHash>,
pub destination_chain_tx_hashes: Vec<NearChainTxHash>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NearExecutionStatusResponse {
pub status: NearExecutionStatus,
pub updated_at: String,
pub swap_details: NearSwapDetails,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quote_response: Option<NearQuoteResponse>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NearAttestationRequest {
pub deposit_address: String,
pub quote_hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NearAttestationResponse {
pub signature: String,
pub version: u32,
}
#[cfg(all(test, not(target_arch = "wasm32")))]
#[allow(clippy::tests_outside_test_module, reason = "inner module + cfg guard for WASM test skip")]
mod tests {
use super::*;
#[test]
fn defuse_token_optional_contract_address_deserializes() {
let sample = r#"{
"assetId": "nep141:eth",
"decimals": 18,
"blockchain": "eth",
"symbol": "ETH",
"price": 4463.25,
"priceUpdatedAt": "2025-09-05T12:00:38.695Z"
}"#;
let t: DefuseToken = serde_json::from_str(sample).unwrap();
assert_eq!(t.decimals, 18);
assert_eq!(t.blockchain, "eth");
assert!(t.contract_address.is_none());
}
#[test]
fn defuse_token_accepts_unknown_blockchain_key() {
let sample = r#"{
"assetId": "nep141:future",
"decimals": 18,
"blockchain": "some-new-chain",
"symbol": "FTR",
"price": 1.0,
"priceUpdatedAt": "2025-09-05T12:00:38.695Z"
}"#;
let t: DefuseToken = serde_json::from_str(sample).unwrap();
assert_eq!(t.blockchain, "some-new-chain");
}
#[test]
fn near_swap_type_serializes_as_screaming_snake_case() {
let t = NearSwapType::ExactInput;
assert_eq!(serde_json::to_string(&t).unwrap(), "\"EXACT_INPUT\"");
}
#[test]
fn near_execution_status_roundtrips_all_variants() {
for (v, expected) in [
(NearExecutionStatus::KnownDepositTx, "\"KNOWN_DEPOSIT_TX\""),
(NearExecutionStatus::PendingDeposit, "\"PENDING_DEPOSIT\""),
(NearExecutionStatus::IncompleteDeposit, "\"INCOMPLETE_DEPOSIT\""),
(NearExecutionStatus::Processing, "\"PROCESSING\""),
(NearExecutionStatus::Success, "\"SUCCESS\""),
(NearExecutionStatus::Refunded, "\"REFUNDED\""),
(NearExecutionStatus::Failed, "\"FAILED\""),
] {
let j = serde_json::to_string(&v).unwrap();
assert_eq!(j, expected);
let back: NearExecutionStatus = serde_json::from_str(&j).unwrap();
assert_eq!(back, v);
}
}
#[test]
fn attestation_request_response_roundtrip() {
let req = NearAttestationRequest {
deposit_address: "0xdead000000000000000000000000000000000000".into(),
quote_hash: "0xabc".into(),
};
let j = serde_json::to_string(&req).unwrap();
assert!(j.contains("depositAddress"));
assert!(j.contains("quoteHash"));
let back: NearAttestationRequest = serde_json::from_str(&j).unwrap();
assert_eq!(back, req);
let resp = NearAttestationResponse { signature: "0x1234".into(), version: 1 };
let j = serde_json::to_string(&resp).unwrap();
let back: NearAttestationResponse = serde_json::from_str(&j).unwrap();
assert_eq!(back, resp);
}
#[test]
fn near_quote_request_serializes_camel_case() {
let req = NearQuoteRequest {
dry: false,
swap_type: NearSwapType::ExactInput,
deposit_mode: NearDepositMode::Simple,
slippage_tolerance: 50,
origin_asset: "nep141:eth".into(),
deposit_type: NearDepositType::OriginChain,
destination_asset: "nep141:btc".into(),
amount: "1000000".into(),
refund_to: "0xabc".into(),
refund_type: NearRefundType::OriginChain,
recipient: "bc1q…".into(),
recipient_type: NearRecipientType::DestinationChain,
deadline: "2099-01-01T00:00:00.000Z".into(),
app_fees: None,
quote_waiting_time_ms: None,
referral: None,
virtual_chain_recipient: None,
virtual_chain_refund_recipient: None,
custom_recipient_msg: None,
session_id: None,
connected_wallets: None,
};
let j = serde_json::to_string(&req).unwrap();
assert!(j.contains("\"swapType\":\"EXACT_INPUT\""));
assert!(j.contains("\"slippageTolerance\":50"));
assert!(j.contains("\"originAsset\":\"nep141:eth\""));
assert!(!j.contains("appFees"));
assert!(!j.contains("sessionId"));
}
#[test]
fn defuse_token_with_contract_address_roundtrips() {
let sample = r#"{
"assetId": "nep141:usdc.e",
"decimals": 6,
"blockchain": "eth",
"symbol": "USDC",
"price": 1.0,
"priceUpdatedAt": "2025-09-05T12:00:38.695Z",
"contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}"#;
let t: DefuseToken = serde_json::from_str(sample).unwrap();
assert_eq!(
t.contract_address.as_deref(),
Some("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
);
let j = serde_json::to_string(&t).unwrap();
assert!(j.contains("contractAddress"));
}
}