Mural Pay integration (#4520)

* wip: muralpay integration

* Basic Mural Pay API bindings

* Fix clippy

* use dotenvy in muralpay example

* Refactor payout creation code

* wip: muralpay payout requests

* Mural Pay payouts work

* Fix clippy

* add mural pay fees API

* Work on payout fee API

* Fees API for more payment methods

* Fix CI

* Temporarily disable Venmo and PayPal methods from frontend

* wip: counterparties

* Start on counterparties and payment methods API

* Mural Pay multiple methods when fetching

* Don't send supported_countries to frontend

* Add countries to muralpay fiat methods

* Compile fix

* Add exchange rate info to fees endpoint

* Add fees to premium Tremendous options

* Add delivery email field to Tremendous payouts

* Add Tremendous product category to payout methods

* Add bank details API to muralpay

* Fix CI

* Fix CI

* Remove prepaid visa, compute fees properly for Tremendous methods

* Add more details to Tremendous errors

* Add fees to Mural

* Payout history route and bank details

* Re-add legacy PayPal/Venmo options for US

* move the mural bank details route

* Add utoipa support to payout endpoints

* address some PR comments

* add CORS to new utoipa routes

* Immediately approve mural payouts

* Add currency support to Tremendous payouts

* Currency forex

* add forex to tremendous fee request

* Add Mural balance to bank balance info

* Add more Tremendous currencies support

* Transaction payouts available use the correct date

* Address my own review comment

* Address PR comments

* Change Mural withdrawal limit to 3k

* maybe fix tremendous gift cards

* Change how Mural minimum withdrawals are calculated

* Tweak min/max withdrawal values

---------

Co-authored-by: Calum H. <contact@cal.engineer>
Co-authored-by: Alejandro González <me@alegon.dev>
This commit is contained in:
aecsocket
2025-11-03 14:19:46 -08:00
committed by GitHub
parent b11934054d
commit 17f395ee55
34 changed files with 4381 additions and 690 deletions

View File

@@ -0,0 +1,43 @@
[package]
name = "muralpay"
version = "0.1.0"
edition.workspace = true
description = "Mural Pay API"
repository = "https://github.com/modrinth/code/"
license = "MIT"
keywords = []
categories = ["api-bindings"]
[dependencies]
bytes = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
derive_more = { workspace = true, features = [
"deref",
"display",
"error",
"from",
] }
reqwest = { workspace = true, features = ["default-tls", "http2", "json"] }
rust_decimal = { workspace = true, features = ["macros"] }
rust_iso3166 = { workspace = true }
secrecy = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_with = { workspace = true }
strum = { workspace = true, features = ["derive"] }
utoipa = { workspace = true, features = ["uuid"], optional = true }
uuid = { workspace = true, features = ["serde"] }
[dev-dependencies]
clap = { workspace = true, features = ["derive"] }
color-eyre = { workspace = true }
dotenvy = { workspace = true }
eyre = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tracing-subscriber = { workspace = true }
[features]
utoipa = ["dep:utoipa"]
[lints]
workspace = true

View File

@@ -0,0 +1,5 @@
Rust API bindings for the [Mural Pay API](https://developers.muralpay.com/docs/getting-started).
# Useful links
- [Mural Pay API Reference](https://developers.muralpay.com/reference/)

View File

@@ -0,0 +1,321 @@
use std::{env, fmt::Debug, io};
use eyre::{Result, WrapErr, eyre};
use muralpay::{
AccountId, CounterpartyId, CreatePayout, CreatePayoutDetails, Dob,
FiatAccountType, FiatAndRailCode, FiatAndRailDetails, FiatFeeRequest,
FiatPayoutFee, MuralPay, PayoutMethodId, PayoutRecipientInfo,
PhysicalAddress, TokenAmount, TokenFeeRequest, TokenPayoutFee, UsdSymbol,
};
use rust_decimal::{Decimal, dec};
use serde::Serialize;
#[derive(Debug, clap::Parser)]
struct Args {
#[arg(short, long)]
output: Option<OutputFormat>,
#[clap(subcommand)]
command: Command,
}
#[derive(Debug, clap::Subcommand)]
enum Command {
/// Account listing and management
Account {
#[command(subcommand)]
command: AccountCommand,
},
/// Payouts and payout requests
Payout {
#[command(subcommand)]
command: PayoutCommand,
},
/// Counterparty management
Counterparty {
#[command(subcommand)]
command: CounterpartyCommand,
},
/// Payout method management
PayoutMethod {
#[command(subcommand)]
command: PayoutMethodCommand,
},
}
#[derive(Debug, clap::Subcommand)]
enum AccountCommand {
/// List all accounts
#[clap(alias = "ls")]
List,
}
#[derive(Debug, clap::Subcommand)]
enum PayoutCommand {
/// List all payout requests
#[clap(alias = "ls")]
List,
/// Create a payout request
Create {
/// ID of the Mural account to send from
source_account_id: AccountId,
/// Description for this payout request
memo: Option<String>,
},
/// Get fees for a transaction
Fees {
#[command(subcommand)]
command: PayoutFeesCommand,
},
/// Get bank details for a fiat and rail code
BankDetails {
/// Fiat and rail code to fetch bank details for
fiat_and_rail_code: FiatAndRailCode,
},
}
#[derive(Debug, clap::Subcommand)]
enum PayoutFeesCommand {
/// Get fees for a token-to-fiat transaction
Token {
amount: Decimal,
fiat_and_rail_code: FiatAndRailCode,
},
/// Get fees for a fiat-to-token transaction
Fiat {
amount: Decimal,
fiat_and_rail_code: FiatAndRailCode,
},
}
#[derive(Debug, clap::Subcommand)]
enum CounterpartyCommand {
/// List all counterparties
#[clap(alias = "ls")]
List,
}
#[derive(Debug, clap::Subcommand)]
enum PayoutMethodCommand {
/// List payout methods for a counterparty
#[clap(alias = "ls")]
List {
/// ID of the counterparty
counterparty_id: CounterpartyId,
},
/// Delete a payout method
Delete {
/// ID of the counterparty
counterparty_id: CounterpartyId,
/// ID of the payout method to delete
payout_method_id: PayoutMethodId,
},
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum OutputFormat {
Json,
JsonMin,
}
#[tokio::main]
async fn main() -> Result<()> {
_ = dotenvy::dotenv();
color_eyre::install().expect("failed to install `color-eyre`");
tracing_subscriber::fmt().init();
let args = <Args as clap::Parser>::parse();
let of = args.output;
let api_url = env::var("MURALPAY_API_URL")
.unwrap_or_else(|_| muralpay::SANDBOX_API_URL.to_string());
let api_key = env::var("MURALPAY_API_KEY").wrap_err("no API key")?;
let transfer_api_key = env::var("MURALPAY_TRANSFER_API_KEY").ok();
let muralpay = MuralPay::new(api_url, api_key, transfer_api_key);
match args.command {
Command::Account {
command: AccountCommand::List,
} => run(of, muralpay.get_all_accounts().await?),
Command::Payout {
command: PayoutCommand::List,
} => run(of, muralpay.search_payout_requests(None, None).await?),
Command::Payout {
command:
PayoutCommand::Create {
source_account_id,
memo,
},
} => run(
of,
create_payout_request(
&muralpay,
source_account_id,
memo.as_deref(),
)
.await?,
),
Command::Payout {
command:
PayoutCommand::Fees {
command:
PayoutFeesCommand::Token {
amount,
fiat_and_rail_code,
},
},
} => run(
of,
get_fees_for_token_amount(&muralpay, amount, fiat_and_rail_code)
.await?,
),
Command::Payout {
command:
PayoutCommand::Fees {
command:
PayoutFeesCommand::Fiat {
amount,
fiat_and_rail_code,
},
},
} => run(
of,
get_fees_for_fiat_amount(&muralpay, amount, fiat_and_rail_code)
.await?,
),
Command::Payout {
command: PayoutCommand::BankDetails { fiat_and_rail_code },
} => run(of, muralpay.get_bank_details(&[fiat_and_rail_code]).await?),
Command::Counterparty {
command: CounterpartyCommand::List,
} => run(of, list_counterparties(&muralpay).await?),
Command::PayoutMethod {
command: PayoutMethodCommand::List { counterparty_id },
} => run(
of,
muralpay
.search_payout_methods(counterparty_id, None)
.await?,
),
Command::PayoutMethod {
command:
PayoutMethodCommand::Delete {
counterparty_id,
payout_method_id,
},
} => run(
of,
muralpay
.delete_payout_method(counterparty_id, payout_method_id)
.await?,
),
}
Ok(())
}
async fn create_payout_request(
muralpay: &MuralPay,
source_account_id: AccountId,
memo: Option<&str>,
) -> Result<()> {
muralpay
.create_payout_request(
source_account_id,
memo,
&[CreatePayout {
amount: TokenAmount {
token_amount: dec!(2.00),
token_symbol: muralpay::USDC.into(),
},
payout_details: CreatePayoutDetails::Fiat {
bank_name: "Foo Bank".into(),
bank_account_owner: "John Smith".into(),
developer_fee: None,
fiat_and_rail_details: FiatAndRailDetails::Usd {
symbol: UsdSymbol::Usd,
account_type: FiatAccountType::Checking,
bank_account_number: "123456789".into(),
// idk what the format is, https://wise.com/us/routing-number/bank/us-bank
bank_routing_number: "071004200".into(),
},
},
recipient_info: PayoutRecipientInfo::Individual {
first_name: "John".into(),
last_name: "Smith".into(),
email: "john.smith@example.com".into(),
date_of_birth: Dob::new(1970, 1, 1).unwrap(),
physical_address: PhysicalAddress {
address1: "1234 Elm Street".into(),
address2: Some("Apt 56B".into()),
country: rust_iso3166::US,
state: "CA".into(),
city: "Springfield".into(),
zip: "90001".into(),
},
},
supporting_details: None,
}],
)
.await?;
Ok(())
}
async fn get_fees_for_token_amount(
muralpay: &MuralPay,
amount: Decimal,
fiat_and_rail_code: FiatAndRailCode,
) -> Result<TokenPayoutFee> {
let fees = muralpay
.get_fees_for_token_amount(&[TokenFeeRequest {
amount: TokenAmount {
token_amount: amount,
token_symbol: muralpay::USDC.into(),
},
fiat_and_rail_code,
}])
.await?;
let fee = fees
.into_iter()
.next()
.ok_or_else(|| eyre!("no fee results returned"))?;
Ok(fee)
}
async fn get_fees_for_fiat_amount(
muralpay: &MuralPay,
amount: Decimal,
fiat_and_rail_code: FiatAndRailCode,
) -> Result<FiatPayoutFee> {
let fees = muralpay
.get_fees_for_fiat_amount(&[FiatFeeRequest {
fiat_amount: amount,
token_symbol: muralpay::USDC.into(),
fiat_and_rail_code,
}])
.await?;
let fee = fees
.into_iter()
.next()
.ok_or_else(|| eyre!("no fee results returned"))?;
Ok(fee)
}
async fn list_counterparties(muralpay: &MuralPay) -> Result<()> {
let _counterparties = muralpay.search_counterparties(None).await?;
Ok(())
}
fn run<T: Debug + Serialize>(output_format: Option<OutputFormat>, value: T) {
match output_format {
None => {
println!("{value:#?}");
}
Some(OutputFormat::Json) => {
_ = serde_json::to_writer_pretty(io::stdout(), &value)
}
Some(OutputFormat::JsonMin) => {
_ = serde_json::to_writer(io::stdout(), &value);
}
}
}

View File

@@ -0,0 +1,236 @@
use std::str::FromStr;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Display};
use rust_decimal::Decimal;
use secrecy::ExposeSecret;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
Blockchain, FiatAmount, MuralError, MuralPay, TokenAmount, WalletDetails,
util::RequestExt,
};
impl MuralPay {
pub async fn get_all_accounts(&self) -> Result<Vec<Account>, MuralError> {
self.http_get(|base| format!("{base}/api/accounts"))
.send_mural()
.await
}
pub async fn get_account(
&self,
id: AccountId,
) -> Result<Account, MuralError> {
self.http_get(|base| format!("{base}/api/accounts/{id}"))
.send_mural()
.await
}
pub async fn create_account(
&self,
name: impl AsRef<str>,
description: Option<impl AsRef<str>>,
) -> Result<Account, MuralError> {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
name: &'a str,
description: Option<&'a str>,
}
let body = Body {
name: name.as_ref(),
description: description.as_ref().map(|x| x.as_ref()),
};
self.http
.post(format!("{}/api/accounts", self.api_url))
.bearer_auth(self.api_key.expose_secret())
.json(&body)
.send_mural()
.await
}
}
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Deref,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{}", _0.hyphenated())]
pub struct AccountId(pub Uuid);
impl FromStr for AccountId {
type Err = <Uuid as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<Uuid>().map(Self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct Account {
pub id: AccountId,
pub name: String,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub is_api_enabled: bool,
pub status: AccountStatus,
pub account_details: Option<AccountDetails>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AccountStatus {
Initializing,
Active,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct AccountDetails {
pub wallet_details: WalletDetails,
pub balances: Vec<TokenAmount>,
pub payin_methods: Vec<PayinMethod>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct PayinMethod {
pub status: PayinMethodStatus,
pub supported_destination_tokens: Vec<DestinationToken>,
pub payin_rail_details: PayinRailDetails,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PayinMethodStatus {
Activated,
Deactivated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct DestinationToken {
pub fees: Fees,
pub token: Token,
pub transaction_minimum: Option<FiatAmount>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct Fees {
#[serde(with = "rust_decimal::serde::float")]
pub variable_fee_percentage: Decimal,
pub fixed_transaction_fee: Option<FiatAmount>,
#[serde(with = "rust_decimal::serde::float_option", default)]
pub developer_fee_percentage: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct Token {
pub symbol: String,
pub blockchain: Blockchain,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PayinRailDetails {
#[serde(rename_all = "camelCase")]
Usd {
currency: UsdCurrency,
payin_rails: Vec<String>,
bank_beneficiary_name: String,
bank_beneficiary_address: String,
bank_name: String,
bank_address: String,
bank_routing_number: String,
bank_account_number: String,
},
#[serde(rename_all = "camelCase")]
Eur {
currency: EurCurrency,
payin_rail: EurPayinRail,
bank_name: String,
bank_address: String,
account_holder_name: String,
iban: String,
bic: String,
},
#[serde(rename_all = "camelCase")]
Cop {
currency: CopCurrency,
payin_rail: CopPayinRail,
},
#[serde(rename_all = "camelCase")]
BlockchainDeposit {
deposit_token: DepositToken,
sender_address: Option<String>,
destination_address: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum UsdCurrency {
Usd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum EurCurrency {
Eur,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum EurPayinRail {
Sepa,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum CopCurrency {
Cop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum CopPayinRail {
Pse,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DepositToken {
#[serde(rename_all = "camelCase")]
UsdtTron { contract_address: String },
}

View File

@@ -0,0 +1,169 @@
use chrono::{DateTime, Utc};
use derive_more::{Deref, Display};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use uuid::Uuid;
use crate::{
MuralError, MuralPay, PhysicalAddress, SearchParams, SearchResponse,
util::RequestExt,
};
impl MuralPay {
pub async fn search_counterparties(
&self,
params: Option<SearchParams<CounterpartyId>>,
) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError> {
self.http_post(|base| format!("{base}/api/counterparties/search"))
.query(&params.map(|p| p.to_query()).unwrap_or_default())
.send_mural()
.await
}
pub async fn get_counterparty(
&self,
id: CounterpartyId,
) -> Result<Counterparty, MuralError> {
self.http_get(|base| {
format!("{base}/api/counterparties/counterparty/{id}")
})
.send_mural()
.await
}
pub async fn create_counterparty(
&self,
counterparty: &CreateCounterparty,
) -> Result<Counterparty, MuralError> {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
counterparty: &'a CreateCounterparty,
}
let body = Body { counterparty };
self.http_post(|base| format!("{base}/api/counterparties"))
.json(&body)
.send_mural()
.await
}
pub async fn update_counterparty(
&self,
id: CounterpartyId,
counterparty: &UpdateCounterparty,
) -> Result<Counterparty, MuralError> {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
counterparty: &'a UpdateCounterparty,
}
let body = Body { counterparty };
self.http_put(|base| {
format!("{base}/api/counterparties/counterparty/{id}")
})
.json(&body)
.send_mural()
.await
}
}
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Deref,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{}", _0.hyphenated())]
pub struct CounterpartyId(pub Uuid);
impl FromStr for CounterpartyId {
type Err = <Uuid as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<Uuid>().map(Self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct Counterparty {
pub id: CounterpartyId,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub alias: Option<String>,
#[serde(flatten)]
pub kind: CounterpartyKind,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum CounterpartyKind {
#[serde(rename_all = "camelCase")]
Individual {
first_name: String,
last_name: String,
email: String,
physical_address: PhysicalAddress,
},
#[serde(rename_all = "camelCase")]
Business {
name: String,
email: String,
physical_address: PhysicalAddress,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum CreateCounterparty {
#[serde(rename_all = "camelCase")]
Individual {
alias: Option<String>,
first_name: String,
last_name: String,
email: String,
physical_address: PhysicalAddress,
},
#[serde(rename_all = "camelCase")]
Business {
alias: Option<String>,
name: String,
email: String,
physical_address: PhysicalAddress,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum UpdateCounterparty {
#[serde(rename_all = "camelCase")]
Individual {
alias: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
email: Option<String>,
physical_address: Option<PhysicalAddress>,
},
#[serde(rename_all = "camelCase")]
Business {
alias: Option<String>,
name: Option<String>,
email: Option<String>,
physical_address: Option<PhysicalAddress>,
},
}

View File

@@ -0,0 +1,117 @@
use std::{collections::HashMap, fmt};
use bytes::Bytes;
use derive_more::{Display, Error, From};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Display, Error, From)]
pub enum MuralError {
#[display("API error")]
Api(ApiError),
#[display("request error")]
Request(reqwest::Error),
#[display("failed to decode response\n{json:?}")]
#[from(skip)]
Decode {
source: serde_json::Error,
json: Bytes,
},
#[display("failed to decode error response\n{json:?}")]
#[from(skip)]
DecodeError {
source: serde_json::Error,
json: Bytes,
},
}
pub type Result<T, E = MuralError> = std::result::Result<T, E>;
#[derive(Debug, Display, Error, From)]
pub enum TransferError {
#[display("no transfer API key")]
NoTransferKey,
#[display("API error")]
Api(Box<ApiError>),
#[display("request error")]
Request(reqwest::Error),
#[display("failed to decode response\n{json:?}")]
#[from(skip)]
Decode {
source: serde_json::Error,
json: Bytes,
},
#[display("failed to decode error response\n{json:?}")]
#[from(skip)]
DecodeError {
source: serde_json::Error,
json: Bytes,
},
}
impl From<MuralError> for TransferError {
fn from(value: MuralError) -> Self {
match value {
MuralError::Api(x) => Self::Api(Box::new(x)),
MuralError::Request(x) => Self::Request(x),
MuralError::Decode { source, json } => {
Self::Decode { source, json }
}
MuralError::DecodeError { source, json } => {
Self::DecodeError { source, json }
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
#[serde(rename_all = "camelCase")]
pub struct ApiError {
pub error_instance_id: Uuid,
pub name: String,
pub message: String,
#[serde(deserialize_with = "one_or_many")]
#[serde(default)]
pub details: Vec<String>,
#[serde(default)]
pub params: HashMap<String, serde_json::Value>,
}
fn one_or_many<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(String),
Many(Vec<String>),
}
match OneOrMany::deserialize(deserializer)? {
OneOrMany::One(s) => Ok(vec![s]),
OneOrMany::Many(v) => Ok(v),
}
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut lines = vec![self.message.to_string()];
if !self.details.is_empty() {
lines.push("details:".into());
lines.extend(self.details.iter().map(|s| format!("- {s}")));
}
if !self.params.is_empty() {
lines.push("params:".into());
lines
.extend(self.params.iter().map(|(k, v)| format!("- {k}: {v}")));
}
lines.push(format!("error name: {}", self.name));
lines.push(format!("error instance id: {}", self.error_instance_id));
write!(f, "{}", lines.join("\n"))
}
}

View File

@@ -1,22 +1,31 @@
#![doc = include_str!("../README.md")]
mod account;
mod counterparty;
mod error;
mod organization;
mod payout;
mod payout_method;
mod serde_iso3166;
mod util;
pub use {account::*, error::*, organization::*, payout::*};
pub use {
account::*, counterparty::*, error::*, organization::*, payout::*,
payout_method::*,
};
use rust_decimal::Decimal;
use secrecy::SecretString;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use std::{ops::Deref, str::FromStr};
use uuid::Uuid;
pub const API_URL: &str = "https://api.muralpay.com";
pub const SANDBOX_API_URL: &str = "https://api-staging.muralpay.com";
/// Default token symbol for [`TokenAmount::token_symbol`] values.
pub const USDC: &str = "USDC";
#[derive(Debug)]
pub struct MuralPay {
pub http: reqwest::Client,
@@ -41,6 +50,7 @@ impl MuralPay {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Blockchain {
Ethereum,
@@ -49,7 +59,10 @@ pub enum Blockchain {
Celo,
}
crate::util::display_as_serialize!(Blockchain);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum CurrencyCode {
Usd,
@@ -65,7 +78,20 @@ pub enum CurrencyCode {
Zar,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
crate::util::display_as_serialize!(CurrencyCode);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum FiatAccountType {
Checking,
Savings,
}
crate::util::display_as_serialize!(FiatAccountType);
#[derive(Debug, Clone, Copy, Serialize, Deserialize, strum::EnumIter)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "kebab-case")]
pub enum FiatAndRailCode {
Usd,
@@ -84,7 +110,18 @@ pub enum FiatAndRailCode {
UsdPanama,
}
crate::util::display_as_serialize!(FiatAndRailCode);
impl FromStr for FiatAndRailCode {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_value(serde_json::Value::String(s.to_owned()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct WalletDetails {
pub blockchain: Blockchain,
@@ -92,15 +129,19 @@ pub struct WalletDetails {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct TokenAmount {
#[serde(with = "rust_decimal::serde::float")]
pub token_amount: Decimal,
pub token_symbol: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct FiatAmount {
#[serde(with = "rust_decimal::serde::float")]
pub fiat_amount: Decimal,
pub fiat_currency_code: CurrencyCode,
}
@@ -126,6 +167,7 @@ impl<Id: Deref<Target = Uuid> + Clone> SearchParams<Id> {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct SearchResponse<Id, T> {
pub total: u64,

View File

@@ -0,0 +1,277 @@
use std::str::FromStr;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Display};
use secrecy::ExposeSecret;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
CurrencyCode, MuralError, MuralPay, SearchResponse, util::RequestExt,
};
impl MuralPay {
pub async fn search_organizations(
&self,
req: SearchRequest,
) -> Result<SearchResponse<OrganizationId, Organization>, MuralError> {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body {
#[serde(skip_serializing_if = "Option::is_none")]
filter: Option<Filter>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Filter {
#[serde(rename = "type")]
ty: FilterType,
name: String,
}
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FilterType {
Name,
}
let query = [
req.limit.map(|limit| ("limit", limit.to_string())),
req.next_id
.map(|next_id| ("nextId", next_id.hyphenated().to_string())),
]
.into_iter()
.flatten()
.collect::<Vec<_>>();
let body = Body {
filter: req.name.map(|name| Filter {
ty: FilterType::Name,
name,
}),
};
self.http_post(|base| format!("{base}/api/organizations/search"))
.bearer_auth(self.api_key.expose_secret())
.query(&query)
.json(&body)
.send_mural()
.await
}
pub async fn get_organization(
&self,
id: OrganizationId,
) -> Result<Organization, MuralError> {
self.http_post(|base| format!("{base}/api/organizations/{id}"))
.send_mural()
.await
}
}
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Deref,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{}", _0.hyphenated())]
pub struct OrganizationId(pub Uuid);
impl FromStr for OrganizationId {
type Err = <Uuid as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<Uuid>().map(Self)
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct SearchRequest {
pub limit: Option<u64>,
pub next_id: Option<Uuid>,
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Organization {
Individual(Individual),
Business(Business),
EndUserCustodialIndividual(EndUserCustodialIndividual),
EndUserCustodialBusiness(EndUserCustodialBusiness),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct Individual {
pub id: OrganizationId,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub first_name: String,
pub last_name: String,
pub tos_status: TosStatus,
pub kyc_status: KycStatus,
pub currency_capabilities: Vec<CurrencyCapability>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct Business {
pub id: OrganizationId,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub name: String,
pub tos_status: TosStatus,
pub kyc_status: KycStatus,
pub currency_capabilities: Vec<CurrencyCapability>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct EndUserCustodialIndividual {
pub id: OrganizationId,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub first_name: String,
pub last_name: String,
pub approver: Approver,
pub tos_status: TosStatus,
pub kyc_status: KycStatus,
pub currency_capabilities: Vec<CurrencyCapability>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct EndUserCustodialBusiness {
pub id: OrganizationId,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub name: String,
pub approver: Approver,
pub tos_status: TosStatus,
pub kyc_status: KycStatus,
pub currency_capabilities: Vec<CurrencyCapability>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct Approver {
pub id: Uuid,
pub created_at: DateTime<Utc>,
pub name: String,
pub email: String,
pub auth_methods: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TosStatus {
NotAccepted,
NeedsReview,
Accepted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum KycStatus {
Inactive,
Pending,
Approved {
approved_at: DateTime<Utc>,
},
Errored {
details: String,
errored_at: DateTime<Utc>,
},
Rejected {
reason: String,
rejected_at: DateTime<Utc>,
},
PreValidationFailed {
failed_validation_reason: FailedValidationReason,
failed_validation_at: DateTime<Utc>,
},
NeedsUpdate {
needs_update_reason: String,
verification_status_updated_at: DateTime<Utc>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum FailedValidationReason {
DocumentPrevalidationFailed {
document_id: String,
failed_validation_reason: String,
},
UltimateBeneficialOwnerPrevalidationFailed {
ultimate_beneficial_owner_id: String,
failed_validation_reason: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct CurrencyCapability {
pub fiat_and_rail_code: String,
pub currency_code: CurrencyCode,
pub deposit_status: TransactionCapabilityStatus,
pub pay_out_status: TransactionCapabilityStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum TransactionCapabilityStatus {
TermsOfService {
details: String,
},
#[serde(rename = "awaitingKYC")]
AwaitingKyc {
details: String,
},
Enabled,
Rejected {
reason: RejectedReason,
details: String,
},
Disabled {
reason: DisabledReason,
details: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RejectedReason {
KycFailed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DisabledReason {
CapabilityUnavailable,
ProcessingError,
}

View File

@@ -0,0 +1,825 @@
#![cfg_attr(
feature = "utoipa",
expect(
clippy::large_stack_arrays,
reason = "due to `utoipa::ToSchema` derive"
)
)]
use std::str::FromStr;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Display, Error, From};
use rust_decimal::Decimal;
use rust_iso3166::CountryCode;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use uuid::Uuid;
use crate::{
AccountId, Blockchain, FiatAccountType, FiatAmount, FiatAndRailCode,
MuralError, MuralPay, SearchParams, SearchResponse, TokenAmount,
TransferError, WalletDetails, util::RequestExt,
};
impl MuralPay {
pub async fn search_payout_requests(
&self,
filter: Option<PayoutStatusFilter>,
params: Option<SearchParams<PayoutRequestId>>,
) -> Result<SearchResponse<PayoutRequestId, PayoutRequest>, MuralError>
{
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body {
filter: Option<PayoutStatusFilter>,
}
let body = Body { filter };
self.http_post(|base| format!("{base}/api/payouts/search"))
.query(&params.map(|p| p.to_query()).unwrap_or_default())
.json(&body)
.send_mural()
.await
}
pub async fn get_payout_request(
&self,
id: PayoutRequestId,
) -> Result<PayoutRequest, MuralError> {
self.http_get(|base| format!("{base}/api/payouts/{id}"))
.send_mural()
.await
}
pub async fn get_fees_for_token_amount(
&self,
token_fee_requests: &[TokenFeeRequest],
) -> Result<Vec<TokenPayoutFee>, MuralError> {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
token_fee_requests: &'a [TokenFeeRequest],
}
let body = Body { token_fee_requests };
self.http_post(|base| format!("{base}/api/payouts/fees/token-to-fiat"))
.json(&body)
.send_mural()
.await
}
pub async fn get_fees_for_fiat_amount(
&self,
fiat_fee_requests: &[FiatFeeRequest],
) -> Result<Vec<FiatPayoutFee>, MuralError> {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
fiat_fee_requests: &'a [FiatFeeRequest],
}
let body = Body { fiat_fee_requests };
self.http_post(|base| format!("{base}/api/payouts/fees/fiat-to-token"))
.json(&body)
.send_mural()
.await
}
pub async fn create_payout_request(
&self,
source_account_id: AccountId,
memo: Option<impl AsRef<str>>,
payouts: &[CreatePayout],
) -> Result<PayoutRequest, MuralError> {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
source_account_id: AccountId,
memo: Option<&'a str>,
payouts: &'a [CreatePayout],
}
let body = Body {
source_account_id,
memo: memo.as_ref().map(|x| x.as_ref()),
payouts,
};
self.http_post(|base| format!("{base}/api/payouts/payout"))
.json(&body)
.send_mural()
.await
}
pub async fn execute_payout_request(
&self,
id: PayoutRequestId,
) -> Result<PayoutRequest, TransferError> {
self.http_post(|base| format!("{base}/api/payouts/payout/{id}/execute"))
.transfer_auth(self)?
.send_mural()
.await
.map_err(From::from)
}
pub async fn cancel_payout_request(
&self,
id: PayoutRequestId,
) -> Result<PayoutRequest, TransferError> {
self.http_post(|base| format!("{base}/api/payouts/payout/{id}/cancel"))
.transfer_auth(self)?
.send_mural()
.await
.map_err(From::from)
}
pub async fn get_bank_details(
&self,
fiat_currency_and_rail: &[FiatAndRailCode],
) -> Result<BankDetailsResponse, MuralError> {
let query = fiat_currency_and_rail
.iter()
.map(|code| ("fiatCurrencyAndRail", code.to_string()))
.collect::<Vec<_>>();
self.http_get(|base| format!("{base}/api/payouts/bank-details"))
.query(&query)
.send_mural()
.await
}
}
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Deref,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{}", _0.hyphenated())]
pub struct PayoutRequestId(pub Uuid);
impl FromStr for PayoutRequestId {
type Err = <Uuid as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<Uuid>().map(Self)
}
}
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Deref,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{}", _0.hyphenated())]
pub struct PayoutId(pub Uuid);
impl FromStr for PayoutId {
type Err = <Uuid as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<Uuid>().map(Self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PayoutStatusFilter {
PayoutStatus { statuses: Vec<String> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct PayoutRequest {
pub id: PayoutRequestId,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub source_account_id: AccountId,
pub transaction_hash: Option<String>,
pub memo: Option<String>,
pub status: PayoutStatus,
pub payouts: Vec<Payout>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PayoutStatus {
AwaitingExecution,
Canceled,
Pending,
Executed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct Payout {
pub id: PayoutId,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub amount: TokenAmount,
pub details: PayoutDetails,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PayoutDetails {
Fiat(FiatPayoutDetails),
Blockchain(BlockchainPayoutDetails),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct FiatPayoutDetails {
pub fiat_and_rail_code: FiatAndRailCode,
pub fiat_payout_status: FiatPayoutStatus,
pub fiat_amount: FiatAmount,
pub transaction_fee: TokenAmount,
#[serde(with = "rust_decimal::serde::float")]
pub exchange_fee_percentage: Decimal,
#[serde(with = "rust_decimal::serde::float")]
pub exchange_rate: Decimal,
pub fee_total: TokenAmount,
pub developer_fee: Option<DeveloperFee>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum FiatPayoutStatus {
Created,
#[serde(rename_all = "camelCase")]
Pending {
initiated_at: DateTime<Utc>,
},
#[serde(rename_all = "camelCase")]
OnHold {
initiated_at: DateTime<Utc>,
},
#[serde(rename_all = "camelCase")]
Completed {
initiated_at: DateTime<Utc>,
completed_at: DateTime<Utc>,
},
#[serde(rename_all = "camelCase")]
Failed {
initiated_at: DateTime<Utc>,
reason: String,
error_code: FiatPayoutErrorCode,
},
Canceled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum FiatPayoutErrorCode {
Unknown,
AccountNumberIncorrect,
RejectedByBank,
AccountTypeIncorrect,
AccountClosed,
BeneficiaryDocumentationIncorrect,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct DeveloperFee {
#[serde(with = "rust_decimal::serde::float_option", default)]
pub developer_fee_percentage: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct BlockchainPayoutDetails {
pub wallet_address: String,
pub blockchain: Blockchain,
pub status: BlockchainPayoutStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum BlockchainPayoutStatus {
AwaitingExecution,
Pending,
Executed,
Failed,
Canceled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct CreatePayout {
pub amount: TokenAmount,
pub payout_details: CreatePayoutDetails,
pub recipient_info: PayoutRecipientInfo,
pub supporting_details: Option<SupportingDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum CreatePayoutDetails {
#[serde(rename_all = "camelCase")]
Fiat {
bank_name: String,
bank_account_owner: String,
developer_fee: Option<DeveloperFee>,
fiat_and_rail_details: FiatAndRailDetails,
},
#[serde(rename_all = "camelCase")]
Blockchain { wallet_details: WalletDetails },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum FiatAndRailDetails {
#[serde(rename_all = "camelCase")]
Usd {
symbol: UsdSymbol,
account_type: FiatAccountType,
bank_account_number: String,
bank_routing_number: String,
},
#[serde(rename_all = "camelCase")]
Cop {
symbol: CopSymbol,
phone_number: String,
account_type: FiatAccountType,
bank_account_number: String,
document_number: String,
document_type: DocumentType,
},
#[serde(rename_all = "camelCase")]
Ars {
symbol: ArsSymbol,
bank_account_number: String,
document_number: String,
bank_account_number_type: String,
},
#[serde(rename_all = "camelCase")]
Eur {
symbol: EurSymbol,
iban: String,
swift_bic: String,
#[serde(with = "crate::serde_iso3166")]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
country: CountryCode,
},
#[serde(rename_all = "camelCase")]
Mxn {
symbol: MxnSymbol,
bank_account_number: String,
},
#[serde(rename_all = "camelCase")]
Brl {
symbol: BrlSymbol,
pix_account_type: PixAccountType,
pix_email: String,
pix_phone: String,
branch_code: String,
document_number: String,
},
#[serde(rename_all = "camelCase")]
Clp {
symbol: ClpSymbol,
account_type: FiatAccountType,
bank_account_number: String,
document_type: DocumentType,
document_number: String,
},
#[serde(rename_all = "camelCase")]
Pen {
symbol: PenSymbol,
document_number: String,
document_type: DocumentType,
bank_account_number: String,
account_type: FiatAccountType,
},
#[serde(rename_all = "camelCase")]
Bob {
symbol: BobSymbol,
bank_account_number: String,
document_number: String,
document_type: DocumentType,
},
#[serde(rename_all = "camelCase")]
Crc {
symbol: CrcSymbol,
iban: String,
document_number: String,
document_type: DocumentType,
},
#[serde(rename_all = "camelCase")]
Zar {
symbol: ZarSymbol,
account_type: FiatAccountType,
bank_account_number: String,
},
#[serde(rename_all = "camelCase")]
UsdPeru {
symbol: UsdSymbol,
account_type: FiatAccountType,
bank_account_number: String,
document_number: String,
document_type: DocumentType,
},
#[serde(rename_all = "camelCase")]
UsdChina {
symbol: UsdSymbol,
bank_name: String,
account_type: FiatAccountType,
bank_account_number: String,
document_number: String,
document_type: DocumentType,
phone_number: String,
address: String,
swift_bic: String,
},
}
impl FiatAndRailDetails {
pub fn code(&self) -> FiatAndRailCode {
match self {
Self::Usd { .. } => FiatAndRailCode::Usd,
Self::Cop { .. } => FiatAndRailCode::Cop,
Self::Ars { .. } => FiatAndRailCode::Ars,
Self::Eur { .. } => FiatAndRailCode::Eur,
Self::Mxn { .. } => FiatAndRailCode::Mxn,
Self::Brl { .. } => FiatAndRailCode::Brl,
Self::Clp { .. } => FiatAndRailCode::Clp,
Self::Pen { .. } => FiatAndRailCode::Pen,
Self::Bob { .. } => FiatAndRailCode::Bob,
Self::Crc { .. } => FiatAndRailCode::Crc,
Self::Zar { .. } => FiatAndRailCode::Zar,
Self::UsdPeru { .. } => FiatAndRailCode::UsdPeru,
Self::UsdChina { .. } => FiatAndRailCode::UsdChina,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum UsdSymbol {
Usd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum CopSymbol {
Cop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum ArsSymbol {
Ars,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum EurSymbol {
Eur,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum MxnSymbol {
Mxn,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum BrlSymbol {
Brl,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum ClpSymbol {
Clp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum PenSymbol {
Pen,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum BobSymbol {
Bob,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum CrcSymbol {
Crc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum ZarSymbol {
Zar,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DocumentType {
NationalId,
Passport,
ResidentId,
Ruc,
TaxId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PixAccountType {
Phone,
Email,
Document,
BankAccount,
}
#[derive(Debug, Clone, Serialize, Deserialize, From)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PayoutRecipientInfo {
#[serde(rename_all = "camelCase")]
Individual {
first_name: String,
last_name: String,
email: String,
date_of_birth: Dob,
physical_address: PhysicalAddress,
},
#[serde(rename_all = "camelCase")]
Business {
name: String,
email: String,
physical_address: PhysicalAddress,
},
}
#[derive(Debug, Display, Clone, Copy, SerializeDisplay, DeserializeFromStr)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{year:04}-{month:02}-{day:02}")]
pub struct Dob {
year: u16,
month: u8,
day: u8,
}
#[derive(Debug, Display, Clone, Error)]
pub enum InvalidDob {
#[display("must be three segments separated by `-`")]
NotThreeSegments,
#[display("year is not an integer")]
YearNotInt,
#[display("month is not an integer")]
MonthNotInt,
#[display("day is not an integer")]
DayNotInt,
#[display("year out of range")]
YearRange,
#[display("month out of range")]
MonthRange,
#[display("day out of range")]
DayRange,
}
impl Dob {
pub fn new(year: u16, month: u8, day: u8) -> Result<Self, InvalidDob> {
if !(1000..10000).contains(&year) {
return Err(InvalidDob::YearRange);
}
if month > 12 {
return Err(InvalidDob::MonthRange);
}
if day > 31 {
return Err(InvalidDob::DayRange);
}
Ok(Self { year, month, day })
}
}
impl FromStr for Dob {
type Err = InvalidDob;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let [year, month, day] = s
.split('-')
.collect::<Vec<_>>()
.try_into()
.map_err(|_| InvalidDob::NotThreeSegments)?;
let year = year.parse::<u16>().map_err(|_| InvalidDob::YearNotInt)?;
let month = month.parse::<u8>().map_err(|_| InvalidDob::MonthNotInt)?;
let day = day.parse::<u8>().map_err(|_| InvalidDob::DayNotInt)?;
Self::new(year, month, day)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct PhysicalAddress {
pub address1: String,
pub address2: Option<String>,
#[serde(with = "crate::serde_iso3166")]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub country: CountryCode,
pub state: String,
pub city: String,
pub zip: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct SupportingDetails {
pub supporting_document: Option<String>, // data:image/jpeg;base64,...
pub payout_purpose: Option<PayoutPurpose>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PayoutPurpose {
VendorPayment,
Payroll,
TaxPayment,
RentLeasePayment,
SupplierPayment,
PersonalGift,
FamilySupport,
CharitableDonation,
ExpenseReimbursement,
BillUtilityPayment,
TravelExpenses,
InvestmentContribution,
CashWithdrawal,
RealEstatePurchase,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct TokenFeeRequest {
pub amount: TokenAmount,
pub fiat_and_rail_code: FiatAndRailCode,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum TokenPayoutFee {
#[serde(rename_all = "camelCase")]
Success {
#[serde(with = "rust_decimal::serde::float")]
exchange_rate: Decimal,
#[serde(with = "rust_decimal::serde::float")]
exchange_fee_percentage: Decimal,
fiat_and_rail_code: FiatAndRailCode,
transaction_fee: TokenAmount,
min_transaction_value: TokenAmount,
estimated_fiat_amount: FiatAmount,
token_amount: TokenAmount,
fee_total: TokenAmount,
},
#[serde(rename_all = "camelCase")]
Error {
token_amount: TokenAmount,
message: String,
fiat_and_rail_code: FiatAndRailCode,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct FiatFeeRequest {
#[serde(with = "rust_decimal::serde::float")]
pub fiat_amount: Decimal,
pub token_symbol: String,
pub fiat_and_rail_code: FiatAndRailCode,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum FiatPayoutFee {
#[serde(rename_all = "camelCase")]
Success {
token_symbol: String,
fiat_amount: FiatAmount,
#[serde(with = "rust_decimal::serde::float")]
exchange_rate: Decimal,
#[serde(with = "rust_decimal::serde::float")]
exchange_fee_percentage: Decimal,
fiat_and_rail_code: FiatAndRailCode,
transaction_fee: TokenAmount,
min_transaction_value: TokenAmount,
estimated_token_amount_required: TokenAmount,
fee_total: TokenAmount,
},
#[serde(rename_all = "camelCase")]
Error {
message: String,
fiat_and_rail_code: FiatAndRailCode,
token_symbol: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct BankDetailsResponse {
pub bank_details: CurrenciesBankDetails,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "kebab-case")]
pub struct CurrenciesBankDetails {
#[serde(default)]
pub usd: CurrencyBankDetails,
#[serde(default)]
pub cop: CurrencyBankDetails,
#[serde(default)]
pub ars: CurrencyBankDetails,
#[serde(default)]
pub eur: CurrencyBankDetails,
#[serde(default)]
pub mxn: CurrencyBankDetails,
#[serde(default)]
pub brl: CurrencyBankDetails,
#[serde(default)]
pub clp: CurrencyBankDetails,
#[serde(default)]
pub pen: CurrencyBankDetails,
#[serde(default)]
pub bob: CurrencyBankDetails,
#[serde(default)]
pub crc: CurrencyBankDetails,
#[serde(default)]
pub zar: CurrencyBankDetails,
#[serde(default)]
pub usd_peru: CurrencyBankDetails,
#[serde(default)]
pub usd_china: CurrencyBankDetails,
#[serde(default)]
pub usd_panama: CurrencyBankDetails,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct CurrencyBankDetails {
pub bank_names: Vec<String>,
}

View File

@@ -0,0 +1,439 @@
use std::str::FromStr;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Display, Error};
use serde::{Deserialize, Serialize};
use serde_with::DeserializeFromStr;
use uuid::Uuid;
use crate::{
ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId,
CrcSymbol, DocumentType, EurSymbol, FiatAccountType, MuralError, MuralPay,
MxnSymbol, PenSymbol, SearchParams, SearchResponse, UsdSymbol,
WalletDetails, ZarSymbol, util::RequestExt,
};
impl MuralPay {
pub async fn search_payout_methods(
&self,
counterparty_id: CounterpartyId,
params: Option<SearchParams<PayoutMethodId>>,
) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError> {
self.http_post(|base| {
format!(
"{base}/api/counterparties/{counterparty_id}/payout-methods/search"
)
})
.query(&params.map(|p| p.to_query()).unwrap_or_default())
.send_mural()
.await
}
pub async fn get_payout_method(
&self,
counterparty_id: CounterpartyId,
payout_method_id: PayoutMethodId,
) -> Result<PayoutMethod, MuralError> {
self.http_get(|base| format!("{base}/api/counterparties/{counterparty_id}/payout-methods/{payout_method_id}"))
.send_mural()
.await
}
pub async fn create_payout_method(
&self,
counterparty_id: CounterpartyId,
alias: impl AsRef<str>,
payout_method: &PayoutMethodDetails,
) -> Result<PayoutMethod, MuralError> {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
alias: &'a str,
payout_method: &'a PayoutMethodDetails,
}
let body = Body {
alias: alias.as_ref(),
payout_method,
};
self.http_post(|base| {
format!(
"{base}/api/counterparties/{counterparty_id}/payout-methods"
)
})
.json(&body)
.send_mural()
.await
}
pub async fn delete_payout_method(
&self,
counterparty_id: CounterpartyId,
payout_method_id: PayoutMethodId,
) -> Result<(), MuralError> {
self.http_delete(|base| format!("{base}/api/counterparties/{counterparty_id}/payout-methods/{payout_method_id}"))
.send_mural()
.await
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PayoutMethodDocumentType {
NationalId,
Passport,
ResidentId,
Ruc,
TaxId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PayoutMethodPixAccountType {
Phone,
Email,
Document,
BankAccount,
}
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Deref,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{}", _0.hyphenated())]
pub struct PayoutMethodId(pub Uuid);
impl FromStr for PayoutMethodId {
type Err = <Uuid as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<Uuid>().map(Self)
}
}
#[derive(Debug, Clone, Serialize, DeserializeFromStr)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct TruncatedString(String);
const TRUNCATED_LEN: usize = 4;
#[derive(Debug, Display, Error)]
#[display("expected {TRUNCATED_LEN} characters, got {num_chars}")]
pub struct InvalidTruncated {
pub num_chars: usize,
}
impl FromStr for TruncatedString {
type Err = InvalidTruncated;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let num_chars = s.chars().count();
if num_chars == TRUNCATED_LEN {
Ok(Self(s.to_string()))
} else {
Err(InvalidTruncated { num_chars })
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct PayoutMethod {
pub id: PayoutMethodId,
pub created_at: DateTime<Utc>,
pub counterparty_id: CounterpartyId,
pub alias: String,
pub payout_method: PayoutMethodDetails,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PayoutMethodDetails {
#[serde(rename_all = "camelCase")]
Usd { details: UsdPayoutDetails },
#[serde(rename_all = "camelCase")]
Ars { details: ArsPayoutDetails },
#[serde(rename_all = "camelCase")]
Brl { details: BrlPayoutDetails },
#[serde(rename_all = "camelCase")]
Cop { details: CopPayoutDetails },
#[serde(rename_all = "camelCase")]
Eur { details: EurPayoutDetails },
#[serde(rename_all = "camelCase")]
Mxn { details: MxnPayoutDetails },
#[serde(rename_all = "camelCase")]
Clp { details: ClpPayoutDetails },
#[serde(rename_all = "camelCase")]
Pen { details: PenPayoutDetails },
#[serde(rename_all = "camelCase")]
Bob { details: BobPayoutDetails },
#[serde(rename_all = "camelCase")]
Crc { details: CrcPayoutDetails },
#[serde(rename_all = "camelCase")]
Zar { details: ZarPayoutDetails },
#[serde(rename_all = "camelCase")]
BlockchainWallet { details: WalletDetails },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum UsdPayoutDetails {
#[serde(rename_all = "camelCase")]
UsdDomestic {
symbol: UsdSymbol,
account_type: FiatAccountType,
transfer_type: UsdTransferType,
bank_name: String,
bank_account_number_truncated: TruncatedString,
bank_routing_number_truncated: TruncatedString,
},
#[serde(rename_all = "camelCase")]
UsdPeru {
symbol: UsdSymbol,
account_type: FiatAccountType,
document_type: DocumentType,
bank_name: String,
bank_account_number_truncated: TruncatedString,
document_number_truncated: TruncatedString,
},
#[serde(rename_all = "camelCase")]
UsdChina {
symbol: UsdSymbol,
account_type: FiatAccountType,
document_type: DocumentType,
bank_name: String,
bank_account_number_truncated: TruncatedString,
document_number_truncated: TruncatedString,
swift_bic_truncated: TruncatedString,
phone_number_truncated: TruncatedString,
},
#[serde(rename_all = "camelCase")]
UsdPanama {
symbol: UsdSymbol,
account_type: FiatAccountType,
document_type: DocumentType,
bank_name: String,
bank_account_number_truncated: TruncatedString,
document_number_truncated: TruncatedString,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum UsdTransferType {
Ach,
Wire,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ArsPayoutDetails {
#[serde(rename_all = "camelCase")]
ArsAlias {
symbol: ArsSymbol,
bank_name: String,
alias_truncated: TruncatedString,
document_number_truncated: TruncatedString,
},
#[serde(rename_all = "camelCase")]
ArsAccountNumber {
symbol: ArsSymbol,
bank_account_number_type: ArsBankAccountNumberType,
bank_name: String,
bank_account_number_truncated: TruncatedString,
document_number_truncated: TruncatedString,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ArsBankAccountNumberType {
Cvu,
Cbu,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum BrlPayoutDetails {
#[serde(rename_all = "camelCase")]
PixPhone {
symbol: BrlSymbol,
full_legal_name: String,
bank_name: String,
phone_number_truncated: TruncatedString,
document_number_truncated: TruncatedString,
},
#[serde(rename_all = "camelCase")]
PixEmail {
symbol: BrlSymbol,
full_legal_name: String,
bank_name: String,
email_truncated: TruncatedString,
document_number_truncated: TruncatedString,
},
#[serde(rename_all = "camelCase")]
PixDocument {
symbol: BrlSymbol,
full_legal_name: String,
bank_name: String,
document_number_truncated: TruncatedString,
},
#[serde(rename_all = "camelCase")]
PixBankAccount {
symbol: BrlSymbol,
full_legal_name: String,
bank_name: String,
bank_account_number_truncated: TruncatedString,
document_number_truncated: TruncatedString,
},
#[serde(rename_all = "camelCase")]
Wire {
symbol: BrlSymbol,
account_type: FiatAccountType,
full_legal_name: String,
bank_name: String,
account_number_truncated: TruncatedString,
bank_branch_truncated: TruncatedString,
document_number_truncated: TruncatedString,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum CopPayoutDetails {
#[serde(rename_all = "camelCase")]
CopDomestic {
symbol: CopSymbol,
account_type: FiatAccountType,
document_type: DocumentType,
bank_name: String,
phone_number_truncated: TruncatedString,
document_number_truncated: TruncatedString,
bank_account_number_truncated: TruncatedString,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum EurPayoutDetails {
#[serde(rename_all = "camelCase")]
EurSepa {
symbol: EurSymbol,
country: String,
bank_name: String,
iban_truncated: TruncatedString,
swift_bic_truncated: TruncatedString,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum MxnPayoutDetails {
#[serde(rename_all = "camelCase")]
MxnDomestic {
symbol: MxnSymbol,
bank_name: String,
bank_account_number_truncated: TruncatedString,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ClpPayoutDetails {
#[serde(rename_all = "camelCase")]
ClpDomestic {
clp: ClpSymbol,
account_type: FiatAccountType,
document_type: DocumentType,
bank_name: String,
bank_account_number_truncated: TruncatedString,
document_number_truncated: TruncatedString,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PenPayoutDetails {
#[serde(rename_all = "camelCase")]
PenDomestic {
symbol: PenSymbol,
document_type: DocumentType,
account_type: FiatAccountType,
bank_name: String,
document_number_truncated: TruncatedString,
bank_account_number_truncated: TruncatedString,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum BobPayoutDetails {
#[serde(rename_all = "camelCase")]
BobDomestic {
symbol: BobSymbol,
document_type: DocumentType,
bank_name: String,
bank_account_number_truncated: TruncatedString,
document_number_truncated: TruncatedString,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum CrcPayoutDetails {
#[serde(rename_all = "camelCase")]
CrcDomestic {
symbol: CrcSymbol,
document_type: DocumentType,
bank_name: String,
iban_truncated: TruncatedString,
document_number_truncated: TruncatedString,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ZarPayoutDetails {
#[serde(rename_all = "camelCase")]
ZarDomestic {
symbol: ZarSymbol,
account_type: FiatAccountType,
bank_name: String,
bank_account_number_truncated: TruncatedString,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct CreatePayoutMethod {
pub alias: String,
pub payout_method: PayoutMethodDetails,
}

View File

@@ -0,0 +1,24 @@
use serde::{Deserialize, de::Error};
use std::borrow::Cow;
use rust_iso3166::CountryCode;
pub fn serialize<S: serde::Serializer>(
v: &CountryCode,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.serialize_str(v.alpha2)
}
pub fn deserialize<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<CountryCode, D::Error> {
<Cow<'_, str>>::deserialize(deserializer).and_then(|country_code| {
rust_iso3166::ALPHA2_MAP
.get(&country_code)
.copied()
.ok_or_else(|| {
D::Error::custom("invalid ISO 3166 alpha-2 country code")
})
})
}

View File

@@ -0,0 +1,100 @@
use reqwest::{IntoUrl, RequestBuilder};
use secrecy::ExposeSecret;
use serde::de::DeserializeOwned;
use crate::{ApiError, MuralError, MuralPay, TransferError};
impl MuralPay {
fn http_req(
&self,
make_req: impl FnOnce() -> RequestBuilder,
) -> RequestBuilder {
make_req()
.bearer_auth(self.api_key.expose_secret())
.header("accept", "application/json")
.header("content-type", "application/json")
}
pub(crate) fn http_get<U: IntoUrl>(
&self,
make_url: impl FnOnce(&str) -> U,
) -> RequestBuilder {
self.http_req(|| self.http.get(make_url(&self.api_url)))
}
pub(crate) fn http_post<U: IntoUrl>(
&self,
make_url: impl FnOnce(&str) -> U,
) -> RequestBuilder {
self.http_req(|| self.http.post(make_url(&self.api_url)))
}
pub(crate) fn http_put<U: IntoUrl>(
&self,
make_url: impl FnOnce(&str) -> U,
) -> RequestBuilder {
self.http_req(|| self.http.put(make_url(&self.api_url)))
}
pub(crate) fn http_delete<U: IntoUrl>(
&self,
make_url: impl FnOnce(&str) -> U,
) -> RequestBuilder {
self.http_req(|| self.http.delete(make_url(&self.api_url)))
}
}
pub trait RequestExt: Sized {
fn transfer_auth(self, client: &MuralPay) -> Result<Self, TransferError>;
async fn send_mural<T: DeserializeOwned>(self) -> crate::Result<T>;
}
const HEADER_TRANSFER_API_KEY: &str = "transfer-api-key";
impl RequestExt for reqwest::RequestBuilder {
fn transfer_auth(self, client: &MuralPay) -> Result<Self, TransferError> {
let transfer_api_key = client
.transfer_api_key
.as_ref()
.ok_or(TransferError::NoTransferKey)?;
Ok(self
.header(HEADER_TRANSFER_API_KEY, transfer_api_key.expose_secret()))
}
async fn send_mural<T: DeserializeOwned>(self) -> crate::Result<T> {
let resp = self.send().await?;
let status = resp.status();
if status.is_client_error() || status.is_server_error() {
let json = resp.bytes().await?;
let err = serde_json::from_slice::<ApiError>(&json)
.map_err(|source| MuralError::DecodeError { source, json })?;
Err(MuralError::Api(err))
} else {
let json = resp.bytes().await?;
let t = serde_json::from_slice::<T>(&json)
.map_err(|source| MuralError::Decode { source, json })?;
Ok(t)
}
}
}
macro_rules! display_as_serialize {
($T:ty) => {
const _: () = {
use std::fmt;
impl fmt::Display for $T {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value =
serde_json::to_value(self).map_err(|_| fmt::Error)?;
let value = value.as_str().ok_or(fmt::Error)?;
write!(f, "{value}")
}
}
};
};
}
pub(crate) use display_as_serialize;