Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.

Commit dd4faec

Browse files
committed
banxa: add create-order flow & model cleanup
Add Banxa create-order support and simplify related models and client logic. - Introduce CreateOrderRequest and CheckoutOrder models and PAYMENT_METHOD_CARD constant (models/create_order.rs). - Implement BanxaClient::create_buy_order to POST create-order requests and return checkout info; remove build_quote_url and related URL-building logic (client.rs, provider.rs). - Add BUY_ORDER_TYPE constant and rename/get specific fiat methods to *_buy variants; use PAYMENT_METHOD_CARD in quote calls. - Remove obsolete Coin type and several order fields (crypto, fiat, crypto_amount, fees, country) and export adjustments (models/asset.rs, models/order.rs, models/mod.rs). - Update mapper tests to match the simplified Order shape (mapper.rs). - Minor cleanup: inline HMAC finalize/encode call (hmac_signature.rs). These changes move the buy-quote flow from generating redirect URLs locally to creating orders via Banxa's API and returning the checkout URL and provider transaction id.
1 parent e09f8f3 commit dd4faec

8 files changed

Lines changed: 82 additions & 62 deletions

File tree

crates/fiat/src/hmac_signature.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ fn generate_hmac_from_bytes(key_bytes: &[u8], message: &str) -> String {
66
type HmacSha256 = Hmac<Sha256>;
77
let mut mac = HmacSha256::new_from_slice(key_bytes).expect("HMAC can take key of any size");
88
mac.update(message.as_bytes());
9-
let result = mac.finalize();
10-
let signature = result.into_bytes();
11-
general_purpose::STANDARD.encode(signature)
9+
general_purpose::STANDARD.encode(mac.finalize().into_bytes())
1210
}
1311

1412
pub fn generate_hmac_signature(secret_key: &str, message: &str) -> String {

crates/fiat/src/providers/banxa/client.rs

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
use std::error::Error;
22

3-
use primitives::{FiatProviderName, FiatQuoteUrl};
3+
use primitives::FiatProviderName;
44
use reqwest::{
55
Client,
66
header::{HeaderMap, HeaderValue},
77
};
8-
use url::Url;
98

10-
use super::models::{Asset, Country, FiatCurrency, ORDER_TYPE_BUY, Order, Quote};
9+
use super::models::{Asset, CheckoutOrder, Country, CreateOrderRequest, FiatCurrency, Order, PAYMENT_METHOD_CARD, Quote};
1110

1211
const API_URL: &str = "https://api.banxa.com";
12+
const BUY_ORDER_TYPE: &str = "buy";
1313

1414
pub struct BanxaClient {
1515
pub client: Client,
@@ -37,11 +37,7 @@ impl BanxaClient {
3737
}
3838

3939
pub async fn get_assets_buy(&self) -> Result<Vec<Asset>, Box<dyn Error + Send + Sync>> {
40-
self.get_assets_by_order_type(ORDER_TYPE_BUY).await
41-
}
42-
43-
async fn get_assets_by_order_type(&self, order_type: &str) -> Result<Vec<Asset>, Box<dyn Error + Send + Sync>> {
44-
let url = format!("{API_URL}/{}/v2/crypto/{order_type}", self.merchant_key);
40+
let url = format!("{API_URL}/{}/v2/crypto/{BUY_ORDER_TYPE}", self.merchant_key);
4541
Ok(self.client.get(&url).headers(self.headers()?).send().await?.json().await?)
4642
}
4743

@@ -53,7 +49,7 @@ impl BanxaClient {
5349
pub async fn get_quote_buy(&self, symbol: &str, chain: &str, fiat_currency: &str, fiat_amount: f64) -> Result<Quote, Box<dyn Error + Send + Sync>> {
5450
let fiat_amount = fiat_amount.to_string();
5551
let query = vec![
56-
("paymentMethodId", "debit-credit-card"),
52+
("paymentMethodId", PAYMENT_METHOD_CARD),
5753
("crypto", symbol),
5854
("blockchain", chain),
5955
("fiat", fiat_currency),
@@ -68,24 +64,23 @@ impl BanxaClient {
6864
Ok(self.client.get(&url).headers(self.headers()?).send().await?.json().await?)
6965
}
7066

71-
pub async fn get_fiat_currencies(&self, order_type: &str) -> Result<Vec<FiatCurrency>, Box<dyn Error + Send + Sync>> {
72-
let url = format!("{API_URL}/{}/v2/fiats/{order_type}", self.merchant_key);
67+
pub async fn get_fiat_currencies_buy(&self) -> Result<Vec<FiatCurrency>, Box<dyn Error + Send + Sync>> {
68+
let url = format!("{API_URL}/{}/v2/fiats/{BUY_ORDER_TYPE}", self.merchant_key);
7369
Ok(self.client.get(&url).headers(self.headers()?).send().await?.json().await?)
7470
}
7571

76-
pub fn build_quote_url(&self, amount: f64, fiat_currency: &str, symbol: &str, network: &str, wallet_address: &str) -> Result<FiatQuoteUrl, Box<dyn Error + Send + Sync>> {
77-
let mut url = Url::parse(&self.url)?;
78-
url.query_pairs_mut()
79-
.append_pair("orderType", ORDER_TYPE_BUY)
80-
.append_pair("coinType", symbol)
81-
.append_pair("blockchain", network)
82-
.append_pair("fiatType", fiat_currency)
83-
.append_pair("fiatAmount", &amount.to_string())
84-
.append_pair("walletAddress", wallet_address);
85-
86-
Ok(FiatQuoteUrl {
87-
redirect_url: url.to_string(),
88-
provider_transaction_id: None,
89-
})
72+
pub async fn create_buy_order(
73+
&self,
74+
quote_id: String,
75+
fiat_amount: f64,
76+
fiat_currency: String,
77+
symbol: String,
78+
network: String,
79+
wallet_address: String,
80+
) -> Result<CheckoutOrder, Box<dyn Error + Send + Sync>> {
81+
let request = CreateOrderRequest::new(quote_id, symbol, fiat_currency, fiat_amount, network, wallet_address, self.url.clone());
82+
let url = format!("{API_URL}/{}/v2/buy", self.merchant_key);
83+
let response = self.client.post(&url).headers(self.headers()?).json(&request).send().await?.error_for_status()?;
84+
response.json().await.map_err(|e| e.into())
9085
}
9186
}

crates/fiat/src/providers/banxa/mapper.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ fn map_payment_type(payment_id: &str) -> Option<PaymentType> {
141141

142142
#[cfg(test)]
143143
mod tests {
144-
use crate::providers::banxa::models::{Coin, FiatCurrency, Order};
144+
use crate::providers::banxa::models::{FiatCurrency, Order};
145145
use primitives::currency::Currency;
146146
use primitives::{FiatTransactionStatus, PaymentType};
147147

@@ -166,19 +166,10 @@ mod tests {
166166
id: "banxa_order_123".to_string(),
167167
external_order_id: Some("quote_123".to_string()),
168168
status: "completed".to_string(),
169-
crypto: Coin {
170-
id: "BTC".to_string(),
171-
blockchain: "BTC".to_string(),
172-
},
173-
fiat: "USD".to_string(),
174169
fiat_amount: 100.0,
175-
crypto_amount: 0.001,
176170
wallet_address: "bc1qexample".to_string(),
177171
transaction_hash: Some("tx_hash".to_string()),
178-
processing_fee: None,
179-
network_fee: None,
180172
order_type: "BUY".to_string(),
181-
country: Some("US".to_string()),
182173
})
183174
.unwrap();
184175

crates/fiat/src/providers/banxa/models/asset.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
use serde::Deserialize;
22
use std::collections::HashMap;
33

4-
#[derive(Debug, Deserialize, Clone)]
5-
pub struct Coin {
6-
pub id: String,
7-
pub blockchain: String,
8-
}
9-
104
#[derive(Debug, Deserialize, Clone)]
115
pub struct Asset {
126
pub id: String,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
pub const PAYMENT_METHOD_CARD: &str = "debit-credit-card";
4+
5+
#[derive(Debug, Serialize)]
6+
#[serde(rename_all = "camelCase")]
7+
pub struct CreateOrderRequest {
8+
pub payment_method_id: String,
9+
pub crypto: String,
10+
pub blockchain: String,
11+
pub fiat: String,
12+
pub fiat_amount: String,
13+
pub wallet_address: String,
14+
pub redirect_url: String,
15+
pub external_customer_id: String,
16+
pub external_order_id: String,
17+
}
18+
19+
impl CreateOrderRequest {
20+
pub fn new(external_order_id: String, crypto: String, fiat: String, fiat_amount: f64, blockchain: String, wallet_address: String, redirect_url: String) -> Self {
21+
Self {
22+
payment_method_id: PAYMENT_METHOD_CARD.to_string(),
23+
crypto,
24+
blockchain,
25+
fiat,
26+
fiat_amount: fiat_amount.to_string(),
27+
external_customer_id: wallet_address.clone(),
28+
wallet_address,
29+
redirect_url,
30+
external_order_id,
31+
}
32+
}
33+
}
34+
35+
#[derive(Debug, Deserialize)]
36+
#[serde(rename_all = "camelCase")]
37+
pub struct CheckoutOrder {
38+
pub id: String,
39+
pub checkout_url: String,
40+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
pub mod asset;
22
pub mod country;
3+
pub mod create_order;
34
pub mod fiat_currencies;
45
pub mod order;
56
pub mod quote;
67
pub mod webhook;
78

89
pub use asset::*;
910
pub use country::*;
11+
pub use create_order::*;
1012
pub use fiat_currencies::*;
11-
pub use order::{ORDER_TYPE_BUY, ORDER_TYPE_SELL, Order};
13+
pub use order::Order;
1214
pub use quote::*;
1315
pub use webhook::*;
Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,15 @@
11
use serde::Deserialize;
2-
use serde_serializers::{deserialize_f64_from_str, deserialize_option_f64_from_str};
3-
4-
use super::asset::Coin;
5-
6-
pub const ORDER_TYPE_BUY: &str = "buy";
7-
pub const ORDER_TYPE_SELL: &str = "sell";
2+
use serde_serializers::deserialize_f64_from_str;
83

94
#[derive(Debug, Deserialize, Clone)]
105
#[serde(rename_all = "camelCase")]
116
pub struct Order {
127
pub id: String,
138
pub external_order_id: Option<String>,
149
pub status: String,
15-
pub crypto: Coin,
16-
pub fiat: String,
1710
#[serde(deserialize_with = "deserialize_f64_from_str")]
1811
pub fiat_amount: f64,
19-
#[serde(deserialize_with = "deserialize_f64_from_str")]
20-
pub crypto_amount: f64,
2112
pub wallet_address: String,
2213
pub transaction_hash: Option<String>,
23-
#[serde(deserialize_with = "deserialize_option_f64_from_str")]
24-
pub processing_fee: Option<f64>,
25-
#[serde(deserialize_with = "deserialize_option_f64_from_str")]
26-
pub network_fee: Option<f64>,
2714
pub order_type: String,
28-
pub country: Option<String>,
2915
}

crates/fiat/src/providers/banxa/provider.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ impl FiatProvider for BanxaClient {
1818
}
1919

2020
async fn get_assets(&self) -> Result<Vec<FiatProviderAsset>, Box<dyn std::error::Error + Send + Sync>> {
21-
let (assets, buy_fiat_currencies) = tokio::try_join!(self.get_assets_buy(), self.get_fiat_currencies("buy"))?;
21+
let (assets, buy_fiat_currencies) = tokio::try_join!(self.get_assets_buy(), self.get_fiat_currencies_buy())?;
2222
Ok(assets.into_iter().flat_map(|asset| map_asset_with_limits(asset, &buy_fiat_currencies, &[])).collect())
2323
}
2424

@@ -60,7 +60,21 @@ impl FiatProvider for BanxaClient {
6060
match data.quote.quote_type {
6161
FiatQuoteType::Buy => {
6262
let network = FiatMapping::get_network(data.asset_symbol.network)?;
63-
self.build_quote_url(data.quote.fiat_amount, &data.quote.fiat_currency, &data.asset_symbol.symbol, &network, &data.wallet_address)
63+
let order = self
64+
.create_buy_order(
65+
data.quote.id,
66+
data.quote.fiat_amount,
67+
data.quote.fiat_currency,
68+
data.asset_symbol.symbol,
69+
network,
70+
data.wallet_address,
71+
)
72+
.await?;
73+
74+
Ok(FiatQuoteUrl {
75+
redirect_url: order.checkout_url,
76+
provider_transaction_id: Some(order.id),
77+
})
6478
}
6579
FiatQuoteType::Sell => Err("not supported".into()),
6680
}

0 commit comments

Comments
 (0)