You've already forked AstralRinth
forked from didirus/AstralRinth
Fix Mural payout status syncing (#4853)
* Fix Mural payout status syncing * Make Mural payout code more resilient * prepare sqlx * fix test
This commit is contained in:
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -5147,11 +5147,7 @@ dependencies = [
|
|||||||
"arc-swap",
|
"arc-swap",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
|
||||||
"color-eyre",
|
|
||||||
"derive_more 2.0.1",
|
"derive_more 2.0.1",
|
||||||
"dotenvy",
|
|
||||||
"eyre",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"rust_iso3166",
|
"rust_iso3166",
|
||||||
@@ -5160,8 +5156,6 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"strum",
|
"strum",
|
||||||
"tokio",
|
|
||||||
"tracing-subscriber",
|
|
||||||
"utoipa",
|
"utoipa",
|
||||||
"uuid 1.18.1",
|
"uuid 1.18.1",
|
||||||
]
|
]
|
||||||
|
|||||||
15
apps/labrinth/.sqlx/query-76cf88116014e3151db2f85b0bd30dba70ae62f9f922ed711dbb85fcf4ec5f7c.json
generated
Normal file
15
apps/labrinth/.sqlx/query-76cf88116014e3151db2f85b0bd30dba70ae62f9f922ed711dbb85fcf4ec5f7c.json
generated
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n UPDATE payouts\n SET status = $1\n WHERE id = $2\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Varchar",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "76cf88116014e3151db2f85b0bd30dba70ae62f9f922ed711dbb85fcf4ec5f7c"
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ lettre = { workspace = true }
|
|||||||
meilisearch-sdk = { workspace = true, features = ["reqwest"] }
|
meilisearch-sdk = { workspace = true, features = ["reqwest"] }
|
||||||
modrinth-maxmind = { workspace = true }
|
modrinth-maxmind = { workspace = true }
|
||||||
modrinth-util = { workspace = true, features = ["decimal", "utoipa"] }
|
modrinth-util = { workspace = true, features = ["decimal", "utoipa"] }
|
||||||
muralpay = { workspace = true, features = ["mock", "utoipa"] }
|
muralpay = { workspace = true, features = ["client", "mock", "utoipa"] }
|
||||||
murmur2 = { workspace = true }
|
murmur2 = { workspace = true }
|
||||||
paste = { workspace = true }
|
paste = { workspace = true }
|
||||||
path-util = { workspace = true }
|
path-util = { workspace = true }
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use crate::search::indexing::index_projects;
|
|||||||
use crate::util::anrok;
|
use crate::util::anrok;
|
||||||
use crate::{database, search};
|
use crate::{database, search};
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use muralpay::MuralPay;
|
|
||||||
use sqlx::Postgres;
|
use sqlx::Postgres;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ impl BackgroundTask {
|
|||||||
stripe_client: stripe::Client,
|
stripe_client: stripe::Client,
|
||||||
anrok_client: anrok::Client,
|
anrok_client: anrok::Client,
|
||||||
email_queue: EmailQueue,
|
email_queue: EmailQueue,
|
||||||
mural_client: MuralPay,
|
mural_client: muralpay::Client,
|
||||||
) {
|
) {
|
||||||
use BackgroundTask::*;
|
use BackgroundTask::*;
|
||||||
match self {
|
match self {
|
||||||
@@ -207,7 +206,10 @@ pub async fn payouts(
|
|||||||
info!("Done running payouts");
|
info!("Done running payouts");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sync_payout_statuses(pool: sqlx::Pool<Postgres>, mural: MuralPay) {
|
pub async fn sync_payout_statuses(
|
||||||
|
pool: sqlx::Pool<Postgres>,
|
||||||
|
mural: muralpay::Client,
|
||||||
|
) {
|
||||||
// Mural sets a max limit of 100 for search payouts endpoint
|
// Mural sets a max limit of 100 for search payouts endpoint
|
||||||
const LIMIT: u32 = 100;
|
const LIMIT: u32 = 100;
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ pub struct TremendousForexResponse {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct MuralPayDetails {
|
pub struct MuralPayDetails {
|
||||||
pub payout_details: MuralPayoutRequest,
|
pub payout_details: MuralPayoutRequest,
|
||||||
pub recipient_info: muralpay::PayoutRecipientInfo,
|
pub recipient_info: muralpay::CreatePayoutRecipientInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PayoutMethodType {
|
impl PayoutMethodType {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ use dashmap::DashMap;
|
|||||||
use eyre::{Result, eyre};
|
use eyre::{Result, eyre};
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use modrinth_util::decimal::Decimal2dp;
|
use modrinth_util::decimal::Decimal2dp;
|
||||||
use muralpay::MuralPay;
|
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use rust_decimal::prelude::ToPrimitive;
|
use rust_decimal::prelude::ToPrimitive;
|
||||||
use rust_decimal::{Decimal, RoundingStrategy, dec};
|
use rust_decimal::{Decimal, RoundingStrategy, dec};
|
||||||
@@ -48,7 +47,7 @@ pub struct PayoutsQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct MuralPayConfig {
|
pub struct MuralPayConfig {
|
||||||
pub client: MuralPay,
|
pub client: muralpay::Client,
|
||||||
pub source_account_id: muralpay::AccountId,
|
pub source_account_id: muralpay::AccountId,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,11 +76,11 @@ impl Default for PayoutsQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_muralpay_client() -> Result<MuralPay> {
|
pub fn create_muralpay_client() -> Result<muralpay::Client> {
|
||||||
let api_url = env_var("MURALPAY_API_URL")?;
|
let api_url = env_var("MURALPAY_API_URL")?;
|
||||||
let api_key = env_var("MURALPAY_API_KEY")?;
|
let api_key = env_var("MURALPAY_API_KEY")?;
|
||||||
let transfer_api_key = env_var("MURALPAY_TRANSFER_API_KEY")?;
|
let transfer_api_key = env_var("MURALPAY_TRANSFER_API_KEY")?;
|
||||||
Ok(MuralPay::new(api_url, api_key, Some(transfer_api_key)))
|
Ok(muralpay::Client::new(api_url, api_key, transfer_api_key))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_muralpay() -> Result<MuralPayConfig> {
|
pub fn create_muralpay() -> Result<MuralPayConfig> {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use chrono::Utc;
|
|||||||
use eyre::{Result, eyre};
|
use eyre::{Result, eyre};
|
||||||
use futures::{StreamExt, TryFutureExt, stream::FuturesUnordered};
|
use futures::{StreamExt, TryFutureExt, stream::FuturesUnordered};
|
||||||
use modrinth_util::decimal::Decimal2dp;
|
use modrinth_util::decimal::Decimal2dp;
|
||||||
use muralpay::{MuralError, MuralPay, TokenFeeRequest};
|
use muralpay::{MuralError, TokenFeeRequest};
|
||||||
use rust_decimal::{Decimal, prelude::ToPrimitive};
|
use rust_decimal::{Decimal, prelude::ToPrimitive};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@@ -69,7 +69,7 @@ impl PayoutsQueue {
|
|||||||
gross_amount: Decimal2dp,
|
gross_amount: Decimal2dp,
|
||||||
fees: PayoutFees,
|
fees: PayoutFees,
|
||||||
payout_details: MuralPayoutRequest,
|
payout_details: MuralPayoutRequest,
|
||||||
recipient_info: muralpay::PayoutRecipientInfo,
|
recipient_info: muralpay::CreatePayoutRecipientInfo,
|
||||||
gotenberg: &GotenbergClient,
|
gotenberg: &GotenbergClient,
|
||||||
) -> Result<muralpay::PayoutRequest, ApiError> {
|
) -> Result<muralpay::PayoutRequest, ApiError> {
|
||||||
let muralpay = self.muralpay.load();
|
let muralpay = self.muralpay.load();
|
||||||
@@ -183,36 +183,27 @@ impl PayoutsQueue {
|
|||||||
),
|
),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// try to immediately execute the payout request...
|
|
||||||
// use a poor man's try/catch block using this `async move {}`
|
|
||||||
// to catch any errors within this block
|
|
||||||
let result = async move {
|
|
||||||
muralpay
|
|
||||||
.client
|
|
||||||
.execute_payout_request(payout_request.id)
|
|
||||||
.await
|
|
||||||
.wrap_internal_err("failed to execute payout request")?;
|
|
||||||
eyre::Ok(())
|
|
||||||
}
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// and if it fails, make sure to immediately cancel it -
|
|
||||||
// we don't want floating payout requests
|
|
||||||
if let Err(err) = result {
|
|
||||||
muralpay
|
|
||||||
.client
|
|
||||||
.cancel_payout_request(payout_request.id)
|
|
||||||
.await
|
|
||||||
.wrap_internal_err_with(|| {
|
|
||||||
eyre!("failed to cancel unexecuted payout request\noriginal error: {err:#?}")
|
|
||||||
})?;
|
|
||||||
return Err(ApiError::Internal(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(payout_request)
|
Ok(payout_request)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cancel_muralpay_payout_request(
|
pub async fn execute_mural_payout_request(
|
||||||
|
&self,
|
||||||
|
id: muralpay::PayoutRequestId,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
let muralpay = self.muralpay.load();
|
||||||
|
let muralpay = muralpay
|
||||||
|
.as_ref()
|
||||||
|
.wrap_internal_err("Mural Pay client not available")?;
|
||||||
|
|
||||||
|
muralpay
|
||||||
|
.client
|
||||||
|
.execute_payout_request(id)
|
||||||
|
.await
|
||||||
|
.wrap_internal_err("failed to execute payout request")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cancel_mural_payout_request(
|
||||||
&self,
|
&self,
|
||||||
id: muralpay::PayoutRequestId,
|
id: muralpay::PayoutRequestId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -263,7 +254,7 @@ impl PayoutsQueue {
|
|||||||
/// Mural state, and updates the payout status.
|
/// Mural state, and updates the payout status.
|
||||||
pub async fn sync_pending_payouts_from_mural(
|
pub async fn sync_pending_payouts_from_mural(
|
||||||
db: &PgPool,
|
db: &PgPool,
|
||||||
mural: &MuralPay,
|
mural: &muralpay::Client,
|
||||||
limit: u32,
|
limit: u32,
|
||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<()> {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -369,9 +360,13 @@ pub async fn sync_pending_payouts_from_mural(
|
|||||||
|
|
||||||
/// Queries Mural for canceled or failed payouts, and updates the corresponding
|
/// Queries Mural for canceled or failed payouts, and updates the corresponding
|
||||||
/// Labrinth payouts' statuses.
|
/// Labrinth payouts' statuses.
|
||||||
|
///
|
||||||
|
/// This will update:
|
||||||
|
/// - Mural payout requests which are failed or canceled
|
||||||
|
/// - Mural payout requests where all of the payouts are failed or canceled
|
||||||
pub async fn sync_failed_mural_payouts_to_labrinth(
|
pub async fn sync_failed_mural_payouts_to_labrinth(
|
||||||
db: &PgPool,
|
db: &PgPool,
|
||||||
mural: &MuralPay,
|
mural: &muralpay::Client,
|
||||||
limit: u32,
|
limit: u32,
|
||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<()> {
|
||||||
info!("Syncing failed Mural payouts to Labrinth");
|
info!("Syncing failed Mural payouts to Labrinth");
|
||||||
@@ -380,12 +375,7 @@ pub async fn sync_failed_mural_payouts_to_labrinth(
|
|||||||
loop {
|
loop {
|
||||||
let search_resp = mural
|
let search_resp = mural
|
||||||
.search_payout_requests(
|
.search_payout_requests(
|
||||||
Some(muralpay::PayoutStatusFilter::PayoutStatus {
|
None,
|
||||||
statuses: vec![
|
|
||||||
muralpay::PayoutStatus::Canceled,
|
|
||||||
muralpay::PayoutStatus::Failed,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Some(muralpay::SearchParams {
|
Some(muralpay::SearchParams {
|
||||||
limit: Some(u64::from(limit)),
|
limit: Some(u64::from(limit)),
|
||||||
next_id,
|
next_id,
|
||||||
@@ -395,48 +385,51 @@ pub async fn sync_failed_mural_payouts_to_labrinth(
|
|||||||
.wrap_internal_err(
|
.wrap_internal_err(
|
||||||
"failed to fetch failed payout requests from Mural",
|
"failed to fetch failed payout requests from Mural",
|
||||||
)?;
|
)?;
|
||||||
next_id = search_resp.next_id;
|
|
||||||
if search_resp.results.is_empty() {
|
if search_resp.results.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
next_id = search_resp.next_id;
|
||||||
let num_canceled = search_resp
|
|
||||||
.results
|
|
||||||
.iter()
|
|
||||||
.filter(|p| p.status == muralpay::PayoutStatus::Canceled)
|
|
||||||
.count();
|
|
||||||
let num_failed = search_resp
|
|
||||||
.results
|
|
||||||
.iter()
|
|
||||||
.filter(|p| p.status == muralpay::PayoutStatus::Failed)
|
|
||||||
.count();
|
|
||||||
info!(
|
|
||||||
"Found {num_canceled} canceled and {num_failed} failed Mural payouts"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut payout_platform_ids = Vec::<String>::new();
|
let mut payout_platform_ids = Vec::<String>::new();
|
||||||
let mut payout_new_statuses = Vec::<String>::new();
|
let mut payout_new_statuses = Vec::<String>::new();
|
||||||
|
|
||||||
for payout_req in search_resp.results {
|
for payout_request in search_resp.results {
|
||||||
let new_payout_status = match payout_req.status {
|
let payout_platform_id = payout_request.id;
|
||||||
muralpay::PayoutStatus::Canceled => PayoutStatus::Cancelled,
|
|
||||||
muralpay::PayoutStatus::Failed => PayoutStatus::Failed,
|
let new_payout_status = match payout_request.status {
|
||||||
_ => {
|
muralpay::PayoutStatus::Canceled => {
|
||||||
warn!(
|
trace!(
|
||||||
"Found payout {} with status {:?}, which should have been filtered out by our Mural request - Mural bug",
|
"- Payout request {payout_platform_id} set to {} because it is cancelled in Mural",
|
||||||
payout_req.id, payout_req.status
|
PayoutStatus::Cancelled
|
||||||
);
|
);
|
||||||
continue;
|
Some(PayoutStatus::Cancelled)
|
||||||
}
|
}
|
||||||
|
muralpay::PayoutStatus::Failed => {
|
||||||
|
trace!(
|
||||||
|
"- Payout request {payout_platform_id} set to {} because it is failed in Mural",
|
||||||
|
PayoutStatus::Failed
|
||||||
|
);
|
||||||
|
Some(PayoutStatus::Failed)
|
||||||
|
}
|
||||||
|
// this will also fail any payout request which has no payouts
|
||||||
|
_ if payout_request
|
||||||
|
.payouts
|
||||||
|
.iter()
|
||||||
|
.all(payout_should_be_failed) =>
|
||||||
|
{
|
||||||
|
trace!(
|
||||||
|
"- Payout request {payout_platform_id} set to {} because all of its payouts are failed",
|
||||||
|
PayoutStatus::Failed
|
||||||
|
);
|
||||||
|
Some(PayoutStatus::Failed)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
};
|
};
|
||||||
let payout_platform_id = payout_req.id;
|
|
||||||
|
|
||||||
trace!(
|
if let Some(new_payout_status) = new_payout_status {
|
||||||
"- Payout {payout_platform_id} set to {new_payout_status:?}",
|
payout_platform_ids.push(payout_platform_id.to_string());
|
||||||
);
|
payout_new_statuses.push(new_payout_status.to_string());
|
||||||
|
}
|
||||||
payout_platform_ids.push(payout_platform_id.to_string());
|
|
||||||
payout_new_statuses.push(new_payout_status.to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query!(
|
||||||
@@ -470,6 +463,17 @@ pub async fn sync_failed_mural_payouts_to_labrinth(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn payout_should_be_failed(payout: &muralpay::Payout) -> bool {
|
||||||
|
matches!(
|
||||||
|
payout.details,
|
||||||
|
muralpay::PayoutDetails::Fiat(muralpay::FiatPayoutDetails {
|
||||||
|
fiat_payout_status: muralpay::FiatPayoutStatus::Failed { .. }
|
||||||
|
| muralpay::FiatPayoutStatus::Refunded { .. },
|
||||||
|
..
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -477,8 +481,8 @@ mod tests {
|
|||||||
api_v3::ApiV3,
|
api_v3::ApiV3,
|
||||||
environment::{TestEnvironment, with_test_environment},
|
environment::{TestEnvironment, with_test_environment},
|
||||||
};
|
};
|
||||||
use muralpay::MuralPay;
|
use muralpay::MuralPayMock;
|
||||||
use muralpay::mock::MuralPayMock;
|
use rust_decimal::dec;
|
||||||
|
|
||||||
fn create_mock_payout_request(
|
fn create_mock_payout_request(
|
||||||
id: &str,
|
id: &str,
|
||||||
@@ -494,12 +498,51 @@ mod tests {
|
|||||||
transaction_hash: None,
|
transaction_hash: None,
|
||||||
memo: None,
|
memo: None,
|
||||||
status,
|
status,
|
||||||
payouts: vec![],
|
payouts: vec![Payout {
|
||||||
|
id: PayoutId(uuid::Uuid::new_v4()),
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
amount: TokenAmount {
|
||||||
|
token_amount: dec!(10.00),
|
||||||
|
token_symbol: "USDC".into(),
|
||||||
|
},
|
||||||
|
details: PayoutDetails::Fiat(FiatPayoutDetails {
|
||||||
|
fiat_and_rail_code: FiatAndRailCode::Usd,
|
||||||
|
fiat_payout_status: FiatPayoutStatus::Pending {
|
||||||
|
initiated_at: chrono::Utc::now(),
|
||||||
|
},
|
||||||
|
fiat_amount: FiatAmount {
|
||||||
|
fiat_amount: dec!(10.00),
|
||||||
|
fiat_currency_code: CurrencyCode::Usd,
|
||||||
|
},
|
||||||
|
transaction_fee: TokenAmount {
|
||||||
|
token_amount: dec!(1.00),
|
||||||
|
token_symbol: "USDC".into(),
|
||||||
|
},
|
||||||
|
exchange_fee_percentage: dec!(0.0),
|
||||||
|
exchange_rate: dec!(1.0),
|
||||||
|
fee_total: TokenAmount {
|
||||||
|
token_amount: dec!(1.00),
|
||||||
|
token_symbol: "USDC".into(),
|
||||||
|
},
|
||||||
|
developer_fee: None,
|
||||||
|
}),
|
||||||
|
recipient_info: PayoutRecipientInfo::Inline {
|
||||||
|
name: "John Smith".into(),
|
||||||
|
details: InlineRecipientDetails::Fiat {
|
||||||
|
details: InlineFiatRecipientDetails {
|
||||||
|
fiat_currency_code: CurrencyCode::Usd,
|
||||||
|
bank_name: "Foo Bank".into(),
|
||||||
|
truncated_bank_account_number: "1234".into(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_mock_muralpay() -> MuralPay {
|
fn create_mock_muralpay() -> muralpay::Client {
|
||||||
MuralPay::from_mock(MuralPayMock {
|
muralpay::Client::from_mock(MuralPayMock {
|
||||||
get_payout_request: Box::new(|_id| {
|
get_payout_request: Box::new(|_id| {
|
||||||
Err(muralpay::MuralError::Api(muralpay::ApiError {
|
Err(muralpay::MuralError::Api(muralpay::ApiError {
|
||||||
error_instance_id: uuid::Uuid::new_v4(),
|
error_instance_id: uuid::Uuid::new_v4(),
|
||||||
@@ -643,7 +686,7 @@ mod tests {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let mock_client = MuralPay::from_mock(mock);
|
let mock_client = muralpay::Client::from_mock(mock);
|
||||||
|
|
||||||
// Run the function
|
// Run the function
|
||||||
let result =
|
let result =
|
||||||
@@ -756,7 +799,7 @@ mod tests {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let mock_client = MuralPay::from_mock(mock);
|
let mock_client = muralpay::Client::from_mock(mock);
|
||||||
|
|
||||||
// Run the function
|
// Run the function
|
||||||
let result =
|
let result =
|
||||||
@@ -818,7 +861,7 @@ mod tests {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let mock_client = MuralPay::from_mock(mock);
|
let mock_client = muralpay::Client::from_mock(mock);
|
||||||
|
|
||||||
// Run the function - should handle this gracefully
|
// Run the function - should handle this gracefully
|
||||||
sync_failed_mural_payouts_to_labrinth(
|
sync_failed_mural_payouts_to_labrinth(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use rust_decimal::{Decimal, RoundingStrategy};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use sqlx::PgPool;
|
use sqlx::{PgPool, PgTransaction};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
@@ -623,29 +623,22 @@ pub async fn create_payout(
|
|||||||
total_fee: fees.total_fee(),
|
total_fee: fees.total_fee(),
|
||||||
sent_to_method,
|
sent_to_method,
|
||||||
payouts_queue: &payouts_queue,
|
payouts_queue: &payouts_queue,
|
||||||
|
db: PgPool::clone(&pool),
|
||||||
|
transaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
let payout_item = match &body.method {
|
match &body.method {
|
||||||
PayoutMethodRequest::PayPal | PayoutMethodRequest::Venmo => {
|
PayoutMethodRequest::PayPal | PayoutMethodRequest::Venmo => {
|
||||||
paypal_payout(payout_cx).await?
|
paypal_payout(payout_cx).await?;
|
||||||
}
|
}
|
||||||
PayoutMethodRequest::Tremendous { method_details } => {
|
PayoutMethodRequest::Tremendous { method_details } => {
|
||||||
tremendous_payout(payout_cx, method_details).await?
|
tremendous_payout(payout_cx, method_details).await?;
|
||||||
}
|
}
|
||||||
PayoutMethodRequest::MuralPay { method_details } => {
|
PayoutMethodRequest::MuralPay { method_details } => {
|
||||||
mural_pay_payout(payout_cx, method_details, &gotenberg).await?
|
mural_pay_payout(payout_cx, method_details, &gotenberg).await?;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
payout_item
|
|
||||||
.insert(&mut transaction)
|
|
||||||
.await
|
|
||||||
.wrap_internal_err("failed to insert payout")?;
|
|
||||||
|
|
||||||
transaction
|
|
||||||
.commit()
|
|
||||||
.await
|
|
||||||
.wrap_internal_err("failed to commit transaction")?;
|
|
||||||
crate::database::models::DBUser::clear_caches(&[(user.id, None)], &redis)
|
crate::database::models::DBUser::clear_caches(&[(user.id, None)], &redis)
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("failed to clear user caches")?;
|
.wrap_internal_err("failed to clear user caches")?;
|
||||||
@@ -653,7 +646,6 @@ pub async fn create_payout(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
struct PayoutContext<'a> {
|
struct PayoutContext<'a> {
|
||||||
body: &'a Withdrawal,
|
body: &'a Withdrawal,
|
||||||
user: &'a DBUser,
|
user: &'a DBUser,
|
||||||
@@ -666,6 +658,8 @@ struct PayoutContext<'a> {
|
|||||||
total_fee: Decimal2dp,
|
total_fee: Decimal2dp,
|
||||||
sent_to_method: Decimal2dp,
|
sent_to_method: Decimal2dp,
|
||||||
payouts_queue: &'a PayoutsQueue,
|
payouts_queue: &'a PayoutsQueue,
|
||||||
|
db: PgPool,
|
||||||
|
transaction: PgTransaction<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_verified_email(user: &DBUser) -> Result<&str, ApiError> {
|
fn get_verified_email(user: &DBUser) -> Result<&str, ApiError> {
|
||||||
@@ -692,12 +686,14 @@ async fn tremendous_payout(
|
|||||||
total_fee,
|
total_fee,
|
||||||
sent_to_method,
|
sent_to_method,
|
||||||
payouts_queue,
|
payouts_queue,
|
||||||
|
db: _,
|
||||||
|
mut transaction,
|
||||||
}: PayoutContext<'_>,
|
}: PayoutContext<'_>,
|
||||||
TremendousDetails {
|
TremendousDetails {
|
||||||
delivery_email,
|
delivery_email,
|
||||||
currency,
|
currency,
|
||||||
}: &TremendousDetails,
|
}: &TremendousDetails,
|
||||||
) -> Result<DBPayout, ApiError> {
|
) -> Result<(), ApiError> {
|
||||||
let user_email = get_verified_email(user)?;
|
let user_email = get_verified_email(user)?;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -773,7 +769,7 @@ async fn tremendous_payout(
|
|||||||
|
|
||||||
let platform_id = res.order.rewards.first().map(|reward| reward.id.clone());
|
let platform_id = res.order.rewards.first().map(|reward| reward.id.clone());
|
||||||
|
|
||||||
Ok(DBPayout {
|
DBPayout {
|
||||||
id: payout_id,
|
id: payout_id,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
created: Utc::now(),
|
created: Utc::now(),
|
||||||
@@ -784,7 +780,17 @@ async fn tremendous_payout(
|
|||||||
method_id: Some(body.method_id.clone()),
|
method_id: Some(body.method_id.clone()),
|
||||||
method_address: Some(user_email.to_string()),
|
method_address: Some(user_email.to_string()),
|
||||||
platform_id,
|
platform_id,
|
||||||
})
|
}
|
||||||
|
.insert(&mut transaction)
|
||||||
|
.await
|
||||||
|
.wrap_internal_err("failed to insert payout")?;
|
||||||
|
|
||||||
|
transaction
|
||||||
|
.commit()
|
||||||
|
.await
|
||||||
|
.wrap_internal_err("failed to commit transaction")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mural_pay_payout(
|
async fn mural_pay_payout(
|
||||||
@@ -798,12 +804,35 @@ async fn mural_pay_payout(
|
|||||||
total_fee,
|
total_fee,
|
||||||
sent_to_method: _,
|
sent_to_method: _,
|
||||||
payouts_queue,
|
payouts_queue,
|
||||||
|
db,
|
||||||
|
mut transaction,
|
||||||
}: PayoutContext<'_>,
|
}: PayoutContext<'_>,
|
||||||
details: &MuralPayDetails,
|
details: &MuralPayDetails,
|
||||||
gotenberg: &GotenbergClient,
|
gotenberg: &GotenbergClient,
|
||||||
) -> Result<DBPayout, ApiError> {
|
) -> Result<(), ApiError> {
|
||||||
let user_email = get_verified_email(user)?;
|
let user_email = get_verified_email(user)?;
|
||||||
|
|
||||||
|
let method_id = match &details.payout_details {
|
||||||
|
MuralPayoutRequest::Blockchain { .. } => {
|
||||||
|
"blockchain-usdc-polygon".to_string()
|
||||||
|
}
|
||||||
|
MuralPayoutRequest::Fiat {
|
||||||
|
fiat_and_rail_details,
|
||||||
|
..
|
||||||
|
} => fiat_and_rail_details.code().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Once the Mural payout request has been created successfully,
|
||||||
|
// then we *must* commit the payout into the DB,
|
||||||
|
// to link the Mural payout request to the `payout` row.
|
||||||
|
// Even if we can't execute the payout.
|
||||||
|
// For this, we immediately insert and commit the txn.
|
||||||
|
// Otherwise if we don't put it into the DB, we've got a ghost Mural
|
||||||
|
// payout with no related database entry.
|
||||||
|
//
|
||||||
|
// However, this doesn't mean that the payout will definitely go through.
|
||||||
|
// For this, we need to execute it, and handle errors.
|
||||||
|
|
||||||
let payout_request = payouts_queue
|
let payout_request = payouts_queue
|
||||||
.create_muralpay_payout_request(
|
.create_muralpay_payout_request(
|
||||||
payout_id,
|
payout_id,
|
||||||
@@ -816,22 +845,13 @@ async fn mural_pay_payout(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let method_id = match &details.payout_details {
|
let payout = DBPayout {
|
||||||
MuralPayoutRequest::Blockchain { .. } => {
|
|
||||||
"blockchain-usdc-polygon".to_string()
|
|
||||||
}
|
|
||||||
MuralPayoutRequest::Fiat {
|
|
||||||
fiat_and_rail_details,
|
|
||||||
..
|
|
||||||
} => fiat_and_rail_details.code().to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(DBPayout {
|
|
||||||
id: payout_id,
|
id: payout_id,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
created: Utc::now(),
|
created: Utc::now(),
|
||||||
// after the payout has been successfully executed,
|
// after the payout has been successfully executed,
|
||||||
// we wait for Mural's confirmation that the funds have been delivered
|
// we wait for Mural's confirmation that the funds have been delivered
|
||||||
|
// done in `SyncPayoutStatuses` background task
|
||||||
status: PayoutStatus::InTransit,
|
status: PayoutStatus::InTransit,
|
||||||
amount: amount_minus_fee.get(),
|
amount: amount_minus_fee.get(),
|
||||||
fee: Some(total_fee.get()),
|
fee: Some(total_fee.get()),
|
||||||
@@ -839,7 +859,61 @@ async fn mural_pay_payout(
|
|||||||
method_id: Some(method_id),
|
method_id: Some(method_id),
|
||||||
method_address: Some(user_email.to_string()),
|
method_address: Some(user_email.to_string()),
|
||||||
platform_id: Some(payout_request.id.to_string()),
|
platform_id: Some(payout_request.id.to_string()),
|
||||||
})
|
};
|
||||||
|
payout
|
||||||
|
.insert(&mut transaction)
|
||||||
|
.await
|
||||||
|
.wrap_internal_err("failed to insert payout")?;
|
||||||
|
|
||||||
|
transaction
|
||||||
|
.commit()
|
||||||
|
.await
|
||||||
|
.wrap_internal_err("failed to commit payout insert transaction")?;
|
||||||
|
|
||||||
|
// try to immediately execute the payout request...
|
||||||
|
// use a poor man's try/catch block using this `async move {}`
|
||||||
|
// to catch any errors within this block
|
||||||
|
let result = async move {
|
||||||
|
payouts_queue
|
||||||
|
.execute_mural_payout_request(payout_request.id)
|
||||||
|
.await
|
||||||
|
.wrap_internal_err("failed to execute payout request")?;
|
||||||
|
eyre::Ok(())
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// and if it fails, make sure to immediately cancel it -
|
||||||
|
// we don't want floating payout requests
|
||||||
|
if let Err(err) = result {
|
||||||
|
if let Err(err) = sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE payouts
|
||||||
|
SET status = $1
|
||||||
|
WHERE id = $2
|
||||||
|
",
|
||||||
|
PayoutStatus::Failed.as_str(),
|
||||||
|
payout.id as _,
|
||||||
|
)
|
||||||
|
.execute(&db)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!(
|
||||||
|
"Created a Mural payout request, but failed to execute it, \
|
||||||
|
and failed to mark the payout as failed: {err:#?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
payouts_queue
|
||||||
|
.cancel_mural_payout_request(payout_request.id)
|
||||||
|
.await
|
||||||
|
.wrap_internal_err_with(|| {
|
||||||
|
eyre!("failed to cancel unexecuted payout request\noriginal error: {err:#?}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return Err(ApiError::Internal(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn paypal_payout(
|
async fn paypal_payout(
|
||||||
@@ -853,8 +927,10 @@ async fn paypal_payout(
|
|||||||
total_fee,
|
total_fee,
|
||||||
sent_to_method,
|
sent_to_method,
|
||||||
payouts_queue,
|
payouts_queue,
|
||||||
|
db: _,
|
||||||
|
mut transaction,
|
||||||
}: PayoutContext<'_>,
|
}: PayoutContext<'_>,
|
||||||
) -> Result<DBPayout, ApiError> {
|
) -> Result<(), ApiError> {
|
||||||
let (wallet, wallet_type, address, display_address) =
|
let (wallet, wallet_type, address, display_address) =
|
||||||
if matches!(body.method, PayoutMethodRequest::Venmo) {
|
if matches!(body.method, PayoutMethodRequest::Venmo) {
|
||||||
if let Some(venmo) = &user.venmo_handle {
|
if let Some(venmo) = &user.venmo_handle {
|
||||||
@@ -965,7 +1041,7 @@ async fn paypal_payout(
|
|||||||
|
|
||||||
let platform_id = Some(data.payout_item_id.clone());
|
let platform_id = Some(data.payout_item_id.clone());
|
||||||
|
|
||||||
Ok(DBPayout {
|
DBPayout {
|
||||||
id: payout_id,
|
id: payout_id,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
created: Utc::now(),
|
created: Utc::now(),
|
||||||
@@ -976,7 +1052,17 @@ async fn paypal_payout(
|
|||||||
method_id: Some(body.method_id.clone()),
|
method_id: Some(body.method_id.clone()),
|
||||||
method_address: Some(display_address.clone()),
|
method_address: Some(display_address.clone()),
|
||||||
platform_id,
|
platform_id,
|
||||||
})
|
}
|
||||||
|
.insert(&mut transaction)
|
||||||
|
.await
|
||||||
|
.wrap_internal_err("failed to insert payout")?;
|
||||||
|
|
||||||
|
transaction
|
||||||
|
.commit()
|
||||||
|
.await
|
||||||
|
.wrap_internal_err("failed to commit transaction")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User performing a payout-related action.
|
/// User performing a payout-related action.
|
||||||
@@ -1201,7 +1287,7 @@ pub async fn cancel_payout(
|
|||||||
.parse::<muralpay::PayoutRequestId>()
|
.parse::<muralpay::PayoutRequestId>()
|
||||||
.wrap_request_err("invalid payout request ID")?;
|
.wrap_request_err("invalid payout request ID")?;
|
||||||
payouts
|
payouts
|
||||||
.cancel_muralpay_payout_request(payout_request_id)
|
.cancel_mural_payout_request(payout_request_id)
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err(
|
.wrap_internal_err(
|
||||||
"failed to cancel payout request",
|
"failed to cancel payout request",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ name = "muralpay"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description = "Mural Pay API"
|
description = "Mural Pay API"
|
||||||
repository = "https://github.com/modrinth/code/"
|
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
keywords = []
|
keywords = []
|
||||||
categories = ["api-bindings"]
|
categories = ["api-bindings"]
|
||||||
@@ -18,26 +17,19 @@ derive_more = { workspace = true, features = [
|
|||||||
"error",
|
"error",
|
||||||
"from",
|
"from",
|
||||||
] }
|
] }
|
||||||
reqwest = { workspace = true, features = ["default-tls", "http2", "json"] }
|
reqwest = { workspace = true, features = ["default-tls", "http2", "json"], optional = true }
|
||||||
rust_decimal = { workspace = true, features = ["macros"] }
|
rust_decimal = { workspace = true, features = ["macros", "serde-with-float"] }
|
||||||
rust_iso3166 = { workspace = true }
|
rust_iso3166 = { workspace = true }
|
||||||
secrecy = { workspace = true }
|
secrecy = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_with = { workspace = true }
|
serde_with = { workspace = true }
|
||||||
strum = { workspace = true, features = ["derive"] }
|
strum = { workspace = true, features = ["derive"] }
|
||||||
utoipa = { workspace = true, features = ["uuid"], optional = true }
|
utoipa = { workspace = true, features = ["chrono", "decimal", "uuid"], optional = true }
|
||||||
uuid = { workspace = true, features = ["serde"] }
|
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]
|
[features]
|
||||||
|
client = ["dep:reqwest", "dep:secrecy"]
|
||||||
mock = ["dep:arc-swap"]
|
mock = ["dep:arc-swap"]
|
||||||
utoipa = ["dep:utoipa"]
|
utoipa = ["dep:utoipa"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,330 +0,0 @@
|
|||||||
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,
|
|
||||||
PayoutRequestId, 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,
|
|
||||||
/// Get details for a single payout request
|
|
||||||
Get {
|
|
||||||
/// ID of the payout request
|
|
||||||
payout_request_id: PayoutRequestId,
|
|
||||||
},
|
|
||||||
/// 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::Get { payout_request_id },
|
|
||||||
} => run(of, muralpay.get_payout_request(payout_request_id).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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +1,65 @@
|
|||||||
use std::str::FromStr;
|
use {
|
||||||
|
crate::{Blockchain, FiatAmount, TokenAmount, WalletDetails},
|
||||||
use chrono::{DateTime, Utc};
|
chrono::{DateTime, Utc},
|
||||||
use derive_more::{Deref, Display};
|
derive_more::{Deref, Display},
|
||||||
use rust_decimal::Decimal;
|
rust_decimal::Decimal,
|
||||||
use secrecy::ExposeSecret;
|
serde::{Deserialize, Serialize},
|
||||||
use serde::{Deserialize, Serialize};
|
std::str::FromStr,
|
||||||
use uuid::Uuid;
|
uuid::Uuid,
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Blockchain, FiatAmount, MuralError, MuralPay, TokenAmount, WalletDetails,
|
|
||||||
util::RequestExt,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl MuralPay {
|
#[cfg(feature = "client")]
|
||||||
pub async fn get_all_accounts(&self) -> Result<Vec<Account>, MuralError> {
|
const _: () = {
|
||||||
mock!(self, get_all_accounts());
|
use crate::{MuralError, RequestExt};
|
||||||
|
|
||||||
self.http_get(|base| format!("{base}/api/accounts"))
|
impl crate::Client {
|
||||||
.send_mural()
|
pub async fn get_all_accounts(&self) -> Result<Vec<Account>, MuralError> {
|
||||||
.await
|
maybe_mock!(self, get_all_accounts());
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_account(
|
self.http_get(|base| format!("{base}/api/accounts"))
|
||||||
&self,
|
.send_mural()
|
||||||
id: AccountId,
|
.await
|
||||||
) -> Result<Account, MuralError> {
|
|
||||||
mock!(self, get_account(id));
|
|
||||||
|
|
||||||
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> {
|
|
||||||
mock!(
|
|
||||||
self,
|
|
||||||
create_account(
|
|
||||||
name.as_ref(),
|
|
||||||
description.as_ref().map(|x| x.as_ref()),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct Body<'a> {
|
|
||||||
name: &'a str,
|
|
||||||
description: Option<&'a str>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = Body {
|
pub async fn get_account(&self, id: AccountId) -> Result<Account, MuralError> {
|
||||||
name: name.as_ref(),
|
maybe_mock!(self, get_account(id));
|
||||||
description: description.as_ref().map(|x| x.as_ref()),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.http
|
self.http_get(|base| format!("{base}/api/accounts/{id}"))
|
||||||
.post(format!("{}/api/accounts", self.api_url))
|
.send_mural()
|
||||||
.bearer_auth(self.api_key.expose_secret())
|
.await
|
||||||
.json(&body)
|
}
|
||||||
.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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_mock!(
|
||||||
|
self,
|
||||||
|
create_account(name.as_ref(), description.as_ref().map(AsRef::as_ref))
|
||||||
|
);
|
||||||
|
|
||||||
|
let body = Body {
|
||||||
|
name: name.as_ref(),
|
||||||
|
description: description.as_ref().map(AsRef::as_ref),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.http_post(|base| format!("{base}/api/accounts"))
|
||||||
|
.json(&body)
|
||||||
|
.send_mural()
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)]
|
||||||
Debug,
|
|
||||||
Display,
|
|
||||||
Clone,
|
|
||||||
Copy,
|
|
||||||
PartialEq,
|
|
||||||
Eq,
|
|
||||||
Hash,
|
|
||||||
Deref,
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
)]
|
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[display("{}", _0.hyphenated())]
|
#[display("{}", _0.hyphenated())]
|
||||||
pub struct AccountId(pub Uuid);
|
pub struct AccountId(pub Uuid);
|
||||||
@@ -90,6 +72,12 @@ impl FromStr for AccountId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<AccountId> for Uuid {
|
||||||
|
fn from(value: AccountId) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use std::{collections::HashMap, fmt};
|
use {
|
||||||
|
bytes::Bytes,
|
||||||
use bytes::Bytes;
|
derive_more::{Display, Error, From},
|
||||||
use derive_more::{Display, Error, From};
|
serde::{Deserialize, Serialize},
|
||||||
use serde::{Deserialize, Serialize};
|
std::{collections::HashMap, fmt},
|
||||||
use uuid::Uuid;
|
uuid::Uuid,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Display, Error, From)]
|
#[derive(Debug, Display, Error, From)]
|
||||||
pub enum MuralError {
|
pub enum MuralError {
|
||||||
@@ -27,43 +28,6 @@ pub enum MuralError {
|
|||||||
|
|
||||||
pub type Result<T, E = MuralError> = std::result::Result<T, E>;
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ApiError {
|
pub struct ApiError {
|
||||||
@@ -96,7 +60,7 @@ where
|
|||||||
|
|
||||||
impl fmt::Display for ApiError {
|
impl fmt::Display for ApiError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let mut lines = vec![self.message.to_string()];
|
let mut lines = vec![self.message.clone()];
|
||||||
|
|
||||||
if !self.details.is_empty() {
|
if !self.details.is_empty() {
|
||||||
lines.push("details:".into());
|
lines.push("details:".into());
|
||||||
@@ -105,8 +69,7 @@ impl fmt::Display for ApiError {
|
|||||||
|
|
||||||
if !self.params.is_empty() {
|
if !self.params.is_empty() {
|
||||||
lines.push("params:".into());
|
lines.push("params:".into());
|
||||||
lines
|
lines.extend(self.params.iter().map(|(k, v)| format!("- {k}: {v}")));
|
||||||
.extend(self.params.iter().map(|(k, v)| format!("- {k}: {v}")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(format!("error name: {}", self.name));
|
lines.push(format!("error name: {}", self.name));
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
//! See [`MuralPayMock`].
|
//! See [`MuralPayMock`].
|
||||||
|
|
||||||
use std::fmt::{self, Debug};
|
use {
|
||||||
|
crate::{
|
||||||
use crate::{
|
Account, AccountId, BankDetailsResponse, Counterparty, CounterpartyId, CreateCounterparty,
|
||||||
Account, AccountId, BankDetailsResponse, Counterparty, CounterpartyId,
|
CreatePayout, FiatAndRailCode, FiatFeeRequest, FiatPayoutFee, MuralError, Organization,
|
||||||
CreateCounterparty, CreatePayout, FiatAndRailCode, FiatFeeRequest,
|
OrganizationId, PayoutMethod, PayoutMethodDetails, PayoutMethodId, PayoutRequest,
|
||||||
FiatPayoutFee, MuralError, Organization, OrganizationId, PayoutMethod,
|
PayoutRequestId, PayoutStatusFilter, SearchParams, SearchRequest, SearchResponse,
|
||||||
PayoutMethodDetails, PayoutMethodId, PayoutRequest, PayoutRequestId,
|
TokenFeeRequest, TokenPayoutFee, UpdateCounterparty,
|
||||||
PayoutStatusFilter, SearchParams, SearchRequest, SearchResponse,
|
transaction::{Transaction, TransactionId},
|
||||||
TokenFeeRequest, TokenPayoutFee, TransferError, UpdateCounterparty,
|
},
|
||||||
|
std::fmt::{self, Debug},
|
||||||
};
|
};
|
||||||
|
|
||||||
macro_rules! impl_mock {
|
macro_rules! impl_mock {
|
||||||
@@ -43,8 +44,8 @@ impl_mock! {
|
|||||||
fn get_fees_for_token_amount(&[TokenFeeRequest]) -> Result<Vec<TokenPayoutFee>, MuralError>;
|
fn get_fees_for_token_amount(&[TokenFeeRequest]) -> Result<Vec<TokenPayoutFee>, MuralError>;
|
||||||
fn get_fees_for_fiat_amount(&[FiatFeeRequest]) -> Result<Vec<FiatPayoutFee>, MuralError>;
|
fn get_fees_for_fiat_amount(&[FiatFeeRequest]) -> Result<Vec<FiatPayoutFee>, MuralError>;
|
||||||
fn create_payout_request(AccountId, Option<&str>, &[CreatePayout]) -> Result<PayoutRequest, MuralError>;
|
fn create_payout_request(AccountId, Option<&str>, &[CreatePayout]) -> Result<PayoutRequest, MuralError>;
|
||||||
fn execute_payout_request(PayoutRequestId) -> Result<PayoutRequest, TransferError>;
|
fn execute_payout_request(PayoutRequestId) -> Result<PayoutRequest, MuralError>;
|
||||||
fn cancel_payout_request(PayoutRequestId) -> Result<PayoutRequest, TransferError>;
|
fn cancel_payout_request(PayoutRequestId) -> Result<PayoutRequest, MuralError>;
|
||||||
fn get_bank_details(&[FiatAndRailCode]) -> Result<BankDetailsResponse, MuralError>;
|
fn get_bank_details(&[FiatAndRailCode]) -> Result<BankDetailsResponse, MuralError>;
|
||||||
fn search_payout_methods(CounterpartyId, Option<SearchParams<PayoutMethodId>>) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError>;
|
fn search_payout_methods(CounterpartyId, Option<SearchParams<PayoutMethodId>>) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError>;
|
||||||
fn get_payout_method(CounterpartyId, PayoutMethodId) -> Result<PayoutMethod, MuralError>;
|
fn get_payout_method(CounterpartyId, PayoutMethodId) -> Result<PayoutMethod, MuralError>;
|
||||||
@@ -56,6 +57,8 @@ impl_mock! {
|
|||||||
fn get_counterparty(CounterpartyId) -> Result<Counterparty, MuralError>;
|
fn get_counterparty(CounterpartyId) -> Result<Counterparty, MuralError>;
|
||||||
fn create_counterparty(&CreateCounterparty) -> Result<Counterparty, MuralError>;
|
fn create_counterparty(&CreateCounterparty) -> Result<Counterparty, MuralError>;
|
||||||
fn update_counterparty(CounterpartyId, &UpdateCounterparty) -> Result<Counterparty, MuralError>;
|
fn update_counterparty(CounterpartyId, &UpdateCounterparty) -> Result<Counterparty, MuralError>;
|
||||||
|
fn get_transaction(TransactionId) -> Result<Transaction, MuralError>;
|
||||||
|
fn search_transactions(AccountId, Option<SearchParams<AccountId>>) -> Result<SearchResponse<AccountId, Account>, MuralError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for MuralPayMock {
|
impl Debug for MuralPayMock {
|
||||||
122
packages/muralpay/src/client/mod.rs
Normal file
122
packages/muralpay/src/client/mod.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
mod error;
|
||||||
|
pub use error::*;
|
||||||
|
use {
|
||||||
|
reqwest::{IntoUrl, RequestBuilder},
|
||||||
|
secrecy::{ExposeSecret, SecretString},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "mock")]
|
||||||
|
mod mock;
|
||||||
|
#[cfg(feature = "mock")]
|
||||||
|
pub use mock::MuralPayMock;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Client {
|
||||||
|
pub http: reqwest::Client,
|
||||||
|
pub api_url: String,
|
||||||
|
pub api_key: SecretString,
|
||||||
|
pub transfer_api_key: SecretString,
|
||||||
|
#[cfg(feature = "mock")]
|
||||||
|
pub mock: std::sync::Arc<arc_swap::ArcSwapOption<mock::MuralPayMock>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn new(
|
||||||
|
api_url: impl Into<String>,
|
||||||
|
api_key: impl Into<SecretString>,
|
||||||
|
transfer_api_key: impl Into<SecretString>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
http: reqwest::Client::new(),
|
||||||
|
api_url: api_url.into(),
|
||||||
|
api_key: api_key.into(),
|
||||||
|
transfer_api_key: transfer_api_key.into(),
|
||||||
|
#[cfg(feature = "mock")]
|
||||||
|
mock: std::sync::Arc::new(arc_swap::ArcSwapOption::empty()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a client which mocks responses.
|
||||||
|
#[cfg(feature = "mock")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_mock(mock: mock::MuralPayMock) -> Self {
|
||||||
|
Self {
|
||||||
|
http: reqwest::Client::new(),
|
||||||
|
api_url: String::new(),
|
||||||
|
api_key: SecretString::from(String::new()),
|
||||||
|
transfer_api_key: SecretString::from(String::new()),
|
||||||
|
mock: std::sync::Arc::new(arc_swap::ArcSwapOption::from_pointee(mock)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 async fn health(&self) -> reqwest::Result<()> {
|
||||||
|
self.http_get(|base| format!("{base}/api/health"))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RequestExt: Sized {
|
||||||
|
#[must_use]
|
||||||
|
fn transfer_auth(self, client: &Client) -> Self;
|
||||||
|
|
||||||
|
fn send_mural<T: DeserializeOwned>(
|
||||||
|
self,
|
||||||
|
) -> impl Future<Output = crate::Result<T>> + Send + Sync;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEADER_TRANSFER_API_KEY: &str = "transfer-api-key";
|
||||||
|
|
||||||
|
impl RequestExt for reqwest::RequestBuilder {
|
||||||
|
fn transfer_auth(self, client: &Client) -> Self {
|
||||||
|
self.header(
|
||||||
|
HEADER_TRANSFER_API_KEY,
|
||||||
|
client.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,96 +1,84 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use {
|
||||||
use derive_more::{Deref, Display};
|
crate::PhysicalAddress,
|
||||||
use serde::{Deserialize, Serialize};
|
chrono::{DateTime, Utc},
|
||||||
use std::str::FromStr;
|
derive_more::{Deref, Display},
|
||||||
use uuid::Uuid;
|
serde::{Deserialize, Serialize},
|
||||||
|
std::str::FromStr,
|
||||||
use crate::{
|
uuid::Uuid,
|
||||||
MuralError, MuralPay, PhysicalAddress, SearchParams, SearchResponse,
|
|
||||||
util::RequestExt,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl MuralPay {
|
#[cfg(feature = "client")]
|
||||||
pub async fn search_counterparties(
|
const _: () = {
|
||||||
&self,
|
use crate::{MuralError, RequestExt, SearchParams, SearchResponse};
|
||||||
params: Option<SearchParams<CounterpartyId>>,
|
|
||||||
) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError> {
|
|
||||||
mock!(self, search_counterparties(params));
|
|
||||||
|
|
||||||
self.http_post(|base| format!("{base}/api/counterparties/search"))
|
impl crate::Client {
|
||||||
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
pub async fn search_counterparties(
|
||||||
.send_mural()
|
&self,
|
||||||
.await
|
params: Option<SearchParams<CounterpartyId>>,
|
||||||
}
|
) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError> {
|
||||||
|
maybe_mock!(self, search_counterparties(params));
|
||||||
|
|
||||||
pub async fn get_counterparty(
|
self.http_post(|base| format!("{base}/api/counterparties/search"))
|
||||||
&self,
|
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
||||||
id: CounterpartyId,
|
.send_mural()
|
||||||
) -> Result<Counterparty, MuralError> {
|
.await
|
||||||
mock!(self, get_counterparty(id));
|
|
||||||
|
|
||||||
self.http_get(|base| {
|
|
||||||
format!("{base}/api/counterparties/counterparty/{id}")
|
|
||||||
})
|
|
||||||
.send_mural()
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_counterparty(
|
|
||||||
&self,
|
|
||||||
counterparty: &CreateCounterparty,
|
|
||||||
) -> Result<Counterparty, MuralError> {
|
|
||||||
mock!(self, create_counterparty(counterparty));
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct Body<'a> {
|
|
||||||
counterparty: &'a CreateCounterparty,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = Body { counterparty };
|
pub async fn get_counterparty(
|
||||||
|
&self,
|
||||||
|
id: CounterpartyId,
|
||||||
|
) -> Result<Counterparty, MuralError> {
|
||||||
|
maybe_mock!(self, get_counterparty(id));
|
||||||
|
|
||||||
self.http_post(|base| format!("{base}/api/counterparties"))
|
self.http_get(|base| format!("{base}/api/counterparties/counterparty/{id}"))
|
||||||
.json(&body)
|
.send_mural()
|
||||||
.send_mural()
|
.await
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_counterparty(
|
|
||||||
&self,
|
|
||||||
id: CounterpartyId,
|
|
||||||
counterparty: &UpdateCounterparty,
|
|
||||||
) -> Result<Counterparty, MuralError> {
|
|
||||||
mock!(self, update_counterparty(id, counterparty));
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct Body<'a> {
|
|
||||||
counterparty: &'a UpdateCounterparty,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = Body { counterparty };
|
pub async fn create_counterparty(
|
||||||
|
&self,
|
||||||
|
counterparty: &CreateCounterparty,
|
||||||
|
) -> Result<Counterparty, MuralError> {
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Body<'a> {
|
||||||
|
counterparty: &'a CreateCounterparty,
|
||||||
|
}
|
||||||
|
|
||||||
self.http_put(|base| {
|
maybe_mock!(self, create_counterparty(counterparty));
|
||||||
format!("{base}/api/counterparties/counterparty/{id}")
|
|
||||||
})
|
let body = Body { counterparty };
|
||||||
.json(&body)
|
|
||||||
.send_mural()
|
self.http_post(|base| format!("{base}/api/counterparties"))
|
||||||
.await
|
.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_mock!(self, update_counterparty(id, counterparty));
|
||||||
|
|
||||||
|
let body = Body { counterparty };
|
||||||
|
|
||||||
|
self.http_put(|base| format!("{base}/api/counterparties/counterparty/{id}"))
|
||||||
|
.json(&body)
|
||||||
|
.send_mural()
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)]
|
||||||
Debug,
|
|
||||||
Display,
|
|
||||||
Clone,
|
|
||||||
Copy,
|
|
||||||
PartialEq,
|
|
||||||
Eq,
|
|
||||||
Hash,
|
|
||||||
Deref,
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
)]
|
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[display("{}", _0.hyphenated())]
|
#[display("{}", _0.hyphenated())]
|
||||||
pub struct CounterpartyId(pub Uuid);
|
pub struct CounterpartyId(pub Uuid);
|
||||||
@@ -103,6 +91,12 @@ impl FromStr for CounterpartyId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<CounterpartyId> for Uuid {
|
||||||
|
fn from(value: CounterpartyId) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
macro_rules! mock {
|
#[cfg(feature = "client")]
|
||||||
|
macro_rules! maybe_mock {
|
||||||
($self:expr, $fn:ident ( $($args:expr),* $(,)? )) => {
|
($self:expr, $fn:ident ( $($args:expr),* $(,)? )) => {
|
||||||
#[cfg(feature = "mock")]
|
#[cfg(feature = "mock")]
|
||||||
if let Some(mock) = &*($self).mock.load() {
|
if let Some(mock) = &*($self).mock.load() {
|
||||||
@@ -11,26 +12,28 @@ macro_rules! mock {
|
|||||||
|
|
||||||
mod account;
|
mod account;
|
||||||
mod counterparty;
|
mod counterparty;
|
||||||
mod error;
|
|
||||||
mod organization;
|
mod organization;
|
||||||
mod payout;
|
mod payout;
|
||||||
mod payout_method;
|
mod payout_method;
|
||||||
mod serde_iso3166;
|
mod serde_iso3166;
|
||||||
|
mod transaction;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
#[cfg(feature = "mock")]
|
|
||||||
pub mod mock;
|
|
||||||
|
|
||||||
pub use {
|
pub use {
|
||||||
account::*, counterparty::*, error::*, organization::*, payout::*,
|
account::*, counterparty::*, organization::*, payout::*, payout_method::*,
|
||||||
payout_method::*,
|
transaction::*,
|
||||||
|
};
|
||||||
|
use {
|
||||||
|
rust_decimal::Decimal,
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
std::{ops::Deref, str::FromStr},
|
||||||
|
uuid::Uuid,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rust_decimal::Decimal;
|
#[cfg(feature = "client")]
|
||||||
use secrecy::SecretString;
|
mod client;
|
||||||
use serde::{Deserialize, Serialize};
|
#[cfg(feature = "client")]
|
||||||
use std::{ops::Deref, str::FromStr};
|
pub use client::*;
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub const API_URL: &str = "https://api.muralpay.com";
|
pub const API_URL: &str = "https://api.muralpay.com";
|
||||||
pub const SANDBOX_API_URL: &str = "https://api-staging.muralpay.com";
|
pub const SANDBOX_API_URL: &str = "https://api-staging.muralpay.com";
|
||||||
@@ -38,46 +41,6 @@ pub const SANDBOX_API_URL: &str = "https://api-staging.muralpay.com";
|
|||||||
/// Default token symbol for [`TokenAmount::token_symbol`] values.
|
/// Default token symbol for [`TokenAmount::token_symbol`] values.
|
||||||
pub const USDC: &str = "USDC";
|
pub const USDC: &str = "USDC";
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MuralPay {
|
|
||||||
pub http: reqwest::Client,
|
|
||||||
pub api_url: String,
|
|
||||||
pub api_key: SecretString,
|
|
||||||
pub transfer_api_key: Option<SecretString>,
|
|
||||||
#[cfg(feature = "mock")]
|
|
||||||
mock: arc_swap::ArcSwapOption<mock::MuralPayMock>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MuralPay {
|
|
||||||
pub fn new(
|
|
||||||
api_url: impl Into<String>,
|
|
||||||
api_key: impl Into<SecretString>,
|
|
||||||
transfer_api_key: Option<impl Into<SecretString>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
http: reqwest::Client::new(),
|
|
||||||
api_url: api_url.into(),
|
|
||||||
api_key: api_key.into(),
|
|
||||||
transfer_api_key: transfer_api_key.map(Into::into),
|
|
||||||
#[cfg(feature = "mock")]
|
|
||||||
mock: arc_swap::ArcSwapOption::empty(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a client which mocks responses.
|
|
||||||
#[cfg(feature = "mock")]
|
|
||||||
#[must_use]
|
|
||||||
pub fn from_mock(mock: mock::MuralPayMock) -> Self {
|
|
||||||
Self {
|
|
||||||
http: reqwest::Client::new(),
|
|
||||||
api_url: "".into(),
|
|
||||||
api_key: SecretString::from(String::new()),
|
|
||||||
transfer_api_key: None,
|
|
||||||
mock: arc_swap::ArcSwapOption::from_pointee(mock),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
@@ -119,7 +82,9 @@ pub enum FiatAccountType {
|
|||||||
|
|
||||||
crate::util::display_as_serialize!(FiatAccountType);
|
crate::util::display_as_serialize!(FiatAccountType);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, strum::EnumIter)]
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::EnumIter,
|
||||||
|
)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum FiatAndRailCode {
|
pub enum FiatAndRailCode {
|
||||||
@@ -149,7 +114,7 @@ impl FromStr for FiatAndRailCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct WalletDetails {
|
pub struct WalletDetails {
|
||||||
@@ -157,7 +122,7 @@ pub struct WalletDetails {
|
|||||||
pub wallet_address: String,
|
pub wallet_address: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct TokenAmount {
|
pub struct TokenAmount {
|
||||||
@@ -166,7 +131,7 @@ pub struct TokenAmount {
|
|||||||
pub token_symbol: String,
|
pub token_symbol: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FiatAmount {
|
pub struct FiatAmount {
|
||||||
@@ -195,7 +160,7 @@ impl<Id: Deref<Target = Uuid> + Clone> SearchParams<Id> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SearchResponse<Id, T> {
|
pub struct SearchResponse<Id, T> {
|
||||||
|
|||||||
@@ -1,91 +1,81 @@
|
|||||||
use std::str::FromStr;
|
use {
|
||||||
|
crate::CurrencyCode,
|
||||||
use chrono::{DateTime, Utc};
|
chrono::{DateTime, Utc},
|
||||||
use derive_more::{Deref, Display};
|
derive_more::{Deref, Display},
|
||||||
use secrecy::ExposeSecret;
|
serde::{Deserialize, Serialize},
|
||||||
use serde::{Deserialize, Serialize};
|
std::str::FromStr,
|
||||||
use uuid::Uuid;
|
uuid::Uuid,
|
||||||
|
|
||||||
use crate::{
|
|
||||||
CurrencyCode, MuralError, MuralPay, SearchResponse, util::RequestExt,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl MuralPay {
|
#[cfg(feature = "client")]
|
||||||
pub async fn search_organizations(
|
const _: () = {
|
||||||
&self,
|
use crate::{MuralError, RequestExt, SearchResponse};
|
||||||
req: SearchRequest,
|
|
||||||
) -> Result<SearchResponse<OrganizationId, Organization>, MuralError> {
|
|
||||||
mock!(self, search_organizations(req.clone()));
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
impl crate::Client {
|
||||||
#[serde(rename_all = "camelCase")]
|
pub async fn search_organizations(
|
||||||
struct Body {
|
&self,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
req: SearchRequest,
|
||||||
filter: Option<Filter>,
|
) -> 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_mock!(self, search_organizations(req.clone()));
|
||||||
|
|
||||||
|
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"))
|
||||||
|
.query(&query)
|
||||||
|
.json(&body)
|
||||||
|
.send_mural()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
pub async fn get_organization(
|
||||||
#[serde(rename_all = "camelCase")]
|
&self,
|
||||||
struct Filter {
|
id: OrganizationId,
|
||||||
#[serde(rename = "type")]
|
) -> Result<Organization, MuralError> {
|
||||||
ty: FilterType,
|
maybe_mock!(self, get_organization(id));
|
||||||
name: String,
|
|
||||||
|
self.http_post(|base| format!("{base}/api/organizations/{id}"))
|
||||||
|
.send_mural()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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(
|
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)]
|
||||||
&self,
|
|
||||||
id: OrganizationId,
|
|
||||||
) -> Result<Organization, MuralError> {
|
|
||||||
mock!(self, get_organization(id));
|
|
||||||
|
|
||||||
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))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[display("{}", _0.hyphenated())]
|
#[display("{}", _0.hyphenated())]
|
||||||
pub struct OrganizationId(pub Uuid);
|
pub struct OrganizationId(pub Uuid);
|
||||||
@@ -98,6 +88,12 @@ impl FromStr for OrganizationId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<OrganizationId> for Uuid {
|
||||||
|
fn from(value: OrganizationId) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
pub struct SearchRequest {
|
pub struct SearchRequest {
|
||||||
|
|||||||
@@ -6,170 +6,188 @@
|
|||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use std::str::FromStr;
|
use {
|
||||||
|
crate::{
|
||||||
use chrono::{DateTime, Utc};
|
AccountId, Blockchain, CounterpartyId, CurrencyCode, FiatAccountType,
|
||||||
use derive_more::{Deref, Display, Error, From};
|
FiatAmount, FiatAndRailCode, PayoutMethodId, TokenAmount,
|
||||||
use rust_decimal::Decimal;
|
TransactionId, WalletDetails,
|
||||||
use rust_iso3166::CountryCode;
|
},
|
||||||
use serde::{Deserialize, Serialize};
|
chrono::{DateTime, Utc},
|
||||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
derive_more::{Deref, Display, Error, From},
|
||||||
use uuid::Uuid;
|
rust_decimal::Decimal,
|
||||||
|
rust_iso3166::CountryCode,
|
||||||
use crate::{
|
serde::{Deserialize, Serialize},
|
||||||
AccountId, Blockchain, FiatAccountType, FiatAmount, FiatAndRailCode,
|
serde_with::{DeserializeFromStr, SerializeDisplay},
|
||||||
MuralError, MuralPay, SearchParams, SearchResponse, TokenAmount,
|
std::str::FromStr,
|
||||||
TransferError, WalletDetails, util::RequestExt,
|
uuid::Uuid,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl MuralPay {
|
#[cfg(feature = "client")]
|
||||||
pub async fn search_payout_requests(
|
const _: () = {
|
||||||
&self,
|
use crate::{MuralError, RequestExt, SearchParams, SearchResponse};
|
||||||
filter: Option<PayoutStatusFilter>,
|
|
||||||
params: Option<SearchParams<PayoutRequestId>>,
|
|
||||||
) -> Result<SearchResponse<PayoutRequestId, PayoutRequest>, MuralError>
|
|
||||||
{
|
|
||||||
mock!(self, search_payout_requests(filter, params));
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
impl crate::Client {
|
||||||
#[serde(rename_all = "camelCase")]
|
pub async fn search_payout_requests(
|
||||||
struct Body {
|
&self,
|
||||||
// if we submit `null`, Mural errors; we have to explicitly exclude this field
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
filter: Option<PayoutStatusFilter>,
|
filter: Option<PayoutStatusFilter>,
|
||||||
|
params: Option<SearchParams<PayoutRequestId>>,
|
||||||
|
) -> Result<SearchResponse<PayoutRequestId, PayoutRequest>, MuralError>
|
||||||
|
{
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Body {
|
||||||
|
// if we submit `null`, Mural errors; we have to explicitly exclude this field
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
filter: Option<PayoutStatusFilter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_mock!(self, search_payout_requests(filter, params));
|
||||||
|
|
||||||
|
let body = Body { filter };
|
||||||
|
|
||||||
|
self.http_post(|base| format!("{base}/api/payouts/search"))
|
||||||
|
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
||||||
|
.json(&body)
|
||||||
|
.send_mural()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = Body { filter };
|
pub async fn get_payout_request(
|
||||||
|
&self,
|
||||||
|
id: PayoutRequestId,
|
||||||
|
) -> Result<PayoutRequest, MuralError> {
|
||||||
|
maybe_mock!(self, get_payout_request(id));
|
||||||
|
|
||||||
self.http_post(|base| format!("{base}/api/payouts/search"))
|
self.http_get(|base| format!("{base}/api/payouts/payout/{id}"))
|
||||||
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
.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],
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_mock!(self, get_fees_for_token_amount(token_fee_requests));
|
||||||
|
|
||||||
|
let body = Body { token_fee_requests };
|
||||||
|
|
||||||
|
self.http_post(|base| {
|
||||||
|
format!("{base}/api/payouts/fees/token-to-fiat")
|
||||||
|
})
|
||||||
.json(&body)
|
.json(&body)
|
||||||
.send_mural()
|
.send_mural()
|
||||||
.await
|
.await
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_payout_request(
|
|
||||||
&self,
|
|
||||||
id: PayoutRequestId,
|
|
||||||
) -> Result<PayoutRequest, MuralError> {
|
|
||||||
mock!(self, get_payout_request(id));
|
|
||||||
|
|
||||||
self.http_get(|base| format!("{base}/api/payouts/payout/{id}"))
|
|
||||||
.send_mural()
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_fees_for_token_amount(
|
|
||||||
&self,
|
|
||||||
token_fee_requests: &[TokenFeeRequest],
|
|
||||||
) -> Result<Vec<TokenPayoutFee>, MuralError> {
|
|
||||||
mock!(self, get_fees_for_token_amount(token_fee_requests));
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct Body<'a> {
|
|
||||||
token_fee_requests: &'a [TokenFeeRequest],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = Body { token_fee_requests };
|
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],
|
||||||
|
}
|
||||||
|
|
||||||
self.http_post(|base| format!("{base}/api/payouts/fees/token-to-fiat"))
|
maybe_mock!(self, get_fees_for_fiat_amount(fiat_fee_requests));
|
||||||
|
|
||||||
|
let body = Body { fiat_fee_requests };
|
||||||
|
|
||||||
|
self.http_post(|base| {
|
||||||
|
format!("{base}/api/payouts/fees/fiat-to-token")
|
||||||
|
})
|
||||||
.json(&body)
|
.json(&body)
|
||||||
.send_mural()
|
.send_mural()
|
||||||
.await
|
.await
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_fees_for_fiat_amount(
|
|
||||||
&self,
|
|
||||||
fiat_fee_requests: &[FiatFeeRequest],
|
|
||||||
) -> Result<Vec<FiatPayoutFee>, MuralError> {
|
|
||||||
mock!(self, get_fees_for_fiat_amount(fiat_fee_requests));
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct Body<'a> {
|
|
||||||
fiat_fee_requests: &'a [FiatFeeRequest],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = Body { fiat_fee_requests };
|
pub async fn create_payout_request(
|
||||||
|
&self,
|
||||||
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> {
|
|
||||||
mock!(self, create_payout_request(source_account_id, memo.as_ref().map(|x| x.as_ref()), payouts));
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct Body<'a> {
|
|
||||||
source_account_id: AccountId,
|
source_account_id: AccountId,
|
||||||
memo: Option<&'a str>,
|
memo: Option<impl AsRef<str>>,
|
||||||
payouts: &'a [CreatePayout],
|
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],
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_mock!(
|
||||||
|
self,
|
||||||
|
create_payout_request(
|
||||||
|
source_account_id,
|
||||||
|
memo.as_ref().map(AsRef::as_ref),
|
||||||
|
payouts
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let body = Body {
|
||||||
|
source_account_id,
|
||||||
|
memo: memo.as_ref().map(AsRef::as_ref),
|
||||||
|
payouts,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.http_post(|base| format!("{base}/api/payouts/payout"))
|
||||||
|
.json(&body)
|
||||||
|
.send_mural()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = Body {
|
pub async fn execute_payout_request(
|
||||||
source_account_id,
|
&self,
|
||||||
memo: memo.as_ref().map(|x| x.as_ref()),
|
id: PayoutRequestId,
|
||||||
payouts,
|
) -> Result<PayoutRequest, MuralError> {
|
||||||
};
|
maybe_mock!(self, execute_payout_request(id));
|
||||||
|
|
||||||
self.http_post(|base| format!("{base}/api/payouts/payout"))
|
self.http_post(|base| {
|
||||||
.json(&body)
|
format!("{base}/api/payouts/payout/{id}/execute")
|
||||||
|
})
|
||||||
|
.transfer_auth(self)
|
||||||
.send_mural()
|
.send_mural()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute_payout_request(
|
pub async fn cancel_payout_request(
|
||||||
&self,
|
&self,
|
||||||
id: PayoutRequestId,
|
id: PayoutRequestId,
|
||||||
) -> Result<PayoutRequest, TransferError> {
|
) -> Result<PayoutRequest, MuralError> {
|
||||||
mock!(self, execute_payout_request(id));
|
maybe_mock!(self, cancel_payout_request(id));
|
||||||
|
|
||||||
self.http_post(|base| format!("{base}/api/payouts/payout/{id}/execute"))
|
self.http_post(|base| {
|
||||||
.transfer_auth(self)?
|
format!("{base}/api/payouts/payout/{id}/cancel")
|
||||||
|
})
|
||||||
|
.transfer_auth(self)
|
||||||
.send_mural()
|
.send_mural()
|
||||||
.await
|
.await
|
||||||
.map_err(From::from)
|
}
|
||||||
|
|
||||||
|
pub async fn get_bank_details(
|
||||||
|
&self,
|
||||||
|
fiat_currency_and_rail: &[FiatAndRailCode],
|
||||||
|
) -> Result<BankDetailsResponse, MuralError> {
|
||||||
|
maybe_mock!(self, get_bank_details(fiat_currency_and_rail));
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
pub async fn cancel_payout_request(
|
|
||||||
&self,
|
|
||||||
id: PayoutRequestId,
|
|
||||||
) -> Result<PayoutRequest, TransferError> {
|
|
||||||
mock!(self, cancel_payout_request(id));
|
|
||||||
|
|
||||||
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> {
|
|
||||||
mock!(self, get_bank_details(fiat_currency_and_rail));
|
|
||||||
|
|
||||||
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(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
@@ -195,6 +213,12 @@ impl FromStr for PayoutRequestId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<PayoutRequestId> for Uuid {
|
||||||
|
fn from(value: PayoutRequestId) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
Display,
|
Display,
|
||||||
@@ -219,6 +243,12 @@ impl FromStr for PayoutId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<PayoutId> for Uuid {
|
||||||
|
fn from(value: PayoutId) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
@@ -226,7 +256,7 @@ pub enum PayoutStatusFilter {
|
|||||||
PayoutStatus { statuses: Vec<PayoutStatus> },
|
PayoutStatus { statuses: Vec<PayoutStatus> },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PayoutRequest {
|
pub struct PayoutRequest {
|
||||||
@@ -251,7 +281,7 @@ pub enum PayoutStatus {
|
|||||||
Failed,
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Payout {
|
pub struct Payout {
|
||||||
@@ -260,9 +290,10 @@ pub struct Payout {
|
|||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
pub amount: TokenAmount,
|
pub amount: TokenAmount,
|
||||||
pub details: PayoutDetails,
|
pub details: PayoutDetails,
|
||||||
|
pub recipient_info: PayoutRecipientInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
pub enum PayoutDetails {
|
pub enum PayoutDetails {
|
||||||
@@ -270,7 +301,7 @@ pub enum PayoutDetails {
|
|||||||
Blockchain(BlockchainPayoutDetails),
|
Blockchain(BlockchainPayoutDetails),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FiatPayoutDetails {
|
pub struct FiatPayoutDetails {
|
||||||
@@ -286,7 +317,7 @@ pub struct FiatPayoutDetails {
|
|||||||
pub developer_fee: Option<DeveloperFee>,
|
pub developer_fee: Option<DeveloperFee>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||||
pub enum FiatPayoutStatus {
|
pub enum FiatPayoutStatus {
|
||||||
@@ -310,7 +341,69 @@ pub enum FiatPayoutStatus {
|
|||||||
reason: String,
|
reason: String,
|
||||||
error_code: FiatPayoutErrorCode,
|
error_code: FiatPayoutErrorCode,
|
||||||
},
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
Canceled,
|
Canceled,
|
||||||
|
// since 1.31
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
RefundInProgress {
|
||||||
|
error_code: RefundErrorCode,
|
||||||
|
failure_reason: String,
|
||||||
|
refund_initiated_at: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
// since 1.31
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Refunded {
|
||||||
|
error_code: RefundErrorCode,
|
||||||
|
failure_reason: String,
|
||||||
|
refund_completed_at: DateTime<Utc>,
|
||||||
|
refund_initiated_at: DateTime<Utc>,
|
||||||
|
refund_transaction_id: TransactionId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// since 1.31
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum RefundErrorCode {
|
||||||
|
Unknown,
|
||||||
|
AccountNumberIncorrect,
|
||||||
|
RejectedByBank,
|
||||||
|
AccountTypeIncorrect,
|
||||||
|
AccountClosed,
|
||||||
|
BeneficiaryDocumentationIncorrect,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||||
|
pub enum FiatPayoutStatusKind {
|
||||||
|
Created,
|
||||||
|
Pending,
|
||||||
|
OnHold,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
Canceled,
|
||||||
|
RefundInProgress,
|
||||||
|
Refunded,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FiatPayoutStatus {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn kind(&self) -> FiatPayoutStatusKind {
|
||||||
|
match self {
|
||||||
|
Self::Created { .. } => FiatPayoutStatusKind::Created,
|
||||||
|
Self::Pending { .. } => FiatPayoutStatusKind::Pending,
|
||||||
|
Self::OnHold { .. } => FiatPayoutStatusKind::OnHold,
|
||||||
|
Self::Completed { .. } => FiatPayoutStatusKind::Completed,
|
||||||
|
Self::Failed { .. } => FiatPayoutStatusKind::Failed,
|
||||||
|
Self::Canceled { .. } => FiatPayoutStatusKind::Canceled,
|
||||||
|
Self::RefundInProgress { .. } => {
|
||||||
|
FiatPayoutStatusKind::RefundInProgress
|
||||||
|
}
|
||||||
|
Self::Refunded { .. } => FiatPayoutStatusKind::Refunded,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
@@ -325,7 +418,7 @@ pub enum FiatPayoutErrorCode {
|
|||||||
BeneficiaryDocumentationIncorrect,
|
BeneficiaryDocumentationIncorrect,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DeveloperFee {
|
pub struct DeveloperFee {
|
||||||
@@ -333,7 +426,7 @@ pub struct DeveloperFee {
|
|||||||
pub developer_fee_percentage: Option<Decimal>,
|
pub developer_fee_percentage: Option<Decimal>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct BlockchainPayoutDetails {
|
pub struct BlockchainPayoutDetails {
|
||||||
@@ -353,13 +446,51 @@ pub enum BlockchainPayoutStatus {
|
|||||||
Canceled,
|
Canceled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
pub enum PayoutRecipientInfo {
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Counterparty {
|
||||||
|
counterparty_id: CounterpartyId,
|
||||||
|
payout_method_id: PayoutMethodId,
|
||||||
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Inline {
|
||||||
|
name: String,
|
||||||
|
details: InlineRecipientDetails,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
pub enum InlineRecipientDetails {
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Fiat { details: InlineFiatRecipientDetails },
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Blockchain {
|
||||||
|
wallet_address: String,
|
||||||
|
blockchain: Blockchain,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct InlineFiatRecipientDetails {
|
||||||
|
pub fiat_currency_code: CurrencyCode,
|
||||||
|
pub bank_name: String,
|
||||||
|
pub truncated_bank_account_number: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CreatePayout {
|
pub struct CreatePayout {
|
||||||
pub amount: TokenAmount,
|
pub amount: TokenAmount,
|
||||||
pub payout_details: CreatePayoutDetails,
|
pub payout_details: CreatePayoutDetails,
|
||||||
pub recipient_info: PayoutRecipientInfo,
|
pub recipient_info: CreatePayoutRecipientInfo,
|
||||||
pub supporting_details: Option<SupportingDetails>,
|
pub supporting_details: Option<SupportingDetails>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,7 +618,8 @@ pub enum FiatAndRailDetails {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FiatAndRailDetails {
|
impl FiatAndRailDetails {
|
||||||
pub fn code(&self) -> FiatAndRailCode {
|
#[must_use]
|
||||||
|
pub const fn code(&self) -> FiatAndRailCode {
|
||||||
match self {
|
match self {
|
||||||
Self::Usd { .. } => FiatAndRailCode::Usd,
|
Self::Usd { .. } => FiatAndRailCode::Usd,
|
||||||
Self::Cop { .. } => FiatAndRailCode::Cop,
|
Self::Cop { .. } => FiatAndRailCode::Cop,
|
||||||
@@ -607,7 +739,7 @@ pub enum PixAccountType {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, From)]
|
#[derive(Debug, Clone, Serialize, Deserialize, From)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
pub enum PayoutRecipientInfo {
|
pub enum CreatePayoutRecipientInfo {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
Individual {
|
Individual {
|
||||||
first_name: String,
|
first_name: String,
|
||||||
@@ -624,20 +756,23 @@ pub enum PayoutRecipientInfo {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PayoutRecipientInfo {
|
impl CreatePayoutRecipientInfo {
|
||||||
|
#[must_use]
|
||||||
pub fn email(&self) -> &str {
|
pub fn email(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
PayoutRecipientInfo::Individual { email, .. } => email,
|
Self::Individual { email, .. } | Self::Business { email, .. } => {
|
||||||
PayoutRecipientInfo::Business { email, .. } => email,
|
email
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn physical_address(&self) -> &PhysicalAddress {
|
#[must_use]
|
||||||
|
pub const fn physical_address(&self) -> &PhysicalAddress {
|
||||||
match self {
|
match self {
|
||||||
PayoutRecipientInfo::Individual {
|
Self::Individual {
|
||||||
physical_address, ..
|
physical_address, ..
|
||||||
} => physical_address,
|
}
|
||||||
PayoutRecipientInfo::Business {
|
| Self::Business {
|
||||||
physical_address, ..
|
physical_address, ..
|
||||||
} => physical_address,
|
} => physical_address,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +1,104 @@
|
|||||||
use std::str::FromStr;
|
use {
|
||||||
|
crate::{
|
||||||
use chrono::{DateTime, Utc};
|
ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId, CrcSymbol,
|
||||||
use derive_more::{Deref, Display, Error};
|
DocumentType, EurSymbol, FiatAccountType, MxnSymbol, PenSymbol, UsdSymbol, WalletDetails,
|
||||||
use serde::{Deserialize, Serialize};
|
ZarSymbol,
|
||||||
use serde_with::DeserializeFromStr;
|
},
|
||||||
use uuid::Uuid;
|
chrono::{DateTime, Utc},
|
||||||
|
derive_more::{Deref, Display, Error},
|
||||||
use crate::{
|
serde::{Deserialize, Serialize},
|
||||||
ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId,
|
serde_with::DeserializeFromStr,
|
||||||
CrcSymbol, DocumentType, EurSymbol, FiatAccountType, MuralError, MuralPay,
|
std::str::FromStr,
|
||||||
MxnSymbol, PenSymbol, SearchParams, SearchResponse, UsdSymbol,
|
uuid::Uuid,
|
||||||
WalletDetails, ZarSymbol, util::RequestExt,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl MuralPay {
|
#[cfg(feature = "client")]
|
||||||
pub async fn search_payout_methods(
|
const _: () = {
|
||||||
&self,
|
use crate::{MuralError, RequestExt, SearchParams, SearchResponse};
|
||||||
counterparty_id: CounterpartyId,
|
|
||||||
params: Option<SearchParams<PayoutMethodId>>,
|
|
||||||
) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError> {
|
|
||||||
mock!(self, search_payout_methods(counterparty_id, params));
|
|
||||||
|
|
||||||
self.http_post(|base| {
|
impl crate::Client {
|
||||||
format!(
|
pub async fn search_payout_methods(
|
||||||
"{base}/api/counterparties/{counterparty_id}/payout-methods/search"
|
&self,
|
||||||
)
|
counterparty_id: CounterpartyId,
|
||||||
})
|
params: Option<SearchParams<PayoutMethodId>>,
|
||||||
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError> {
|
||||||
.send_mural()
|
maybe_mock!(self, search_payout_methods(counterparty_id, params));
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_payout_method(
|
self.http_post(|base| {
|
||||||
&self,
|
format!("{base}/api/counterparties/{counterparty_id}/payout-methods/search")
|
||||||
counterparty_id: CounterpartyId,
|
})
|
||||||
payout_method_id: PayoutMethodId,
|
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
||||||
) -> Result<PayoutMethod, MuralError> {
|
|
||||||
mock!(self, get_payout_method(counterparty_id, payout_method_id));
|
|
||||||
|
|
||||||
self.http_get(|base| format!("{base}/api/counterparties/{counterparty_id}/payout-methods/{payout_method_id}"))
|
|
||||||
.send_mural()
|
.send_mural()
|
||||||
.await
|
.await
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_payout_method(
|
|
||||||
&self,
|
|
||||||
counterparty_id: CounterpartyId,
|
|
||||||
alias: impl AsRef<str>,
|
|
||||||
payout_method: &PayoutMethodDetails,
|
|
||||||
) -> Result<PayoutMethod, MuralError> {
|
|
||||||
mock!(self, create_payout_method(counterparty_id, alias.as_ref(), payout_method));
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct Body<'a> {
|
|
||||||
alias: &'a str,
|
|
||||||
payout_method: &'a PayoutMethodDetails,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = Body {
|
pub async fn get_payout_method(
|
||||||
alias: alias.as_ref(),
|
&self,
|
||||||
payout_method,
|
counterparty_id: CounterpartyId,
|
||||||
};
|
payout_method_id: PayoutMethodId,
|
||||||
|
) -> Result<PayoutMethod, MuralError> {
|
||||||
|
maybe_mock!(self, get_payout_method(counterparty_id, payout_method_id));
|
||||||
|
|
||||||
self.http_post(|base| {
|
self.http_get(|base| {
|
||||||
format!(
|
format!(
|
||||||
"{base}/api/counterparties/{counterparty_id}/payout-methods"
|
"{base}/api/counterparties/{counterparty_id}/payout-methods/{payout_method_id}"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.json(&body)
|
|
||||||
.send_mural()
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_payout_method(
|
|
||||||
&self,
|
|
||||||
counterparty_id: CounterpartyId,
|
|
||||||
payout_method_id: PayoutMethodId,
|
|
||||||
) -> Result<(), MuralError> {
|
|
||||||
mock!(self, delete_payout_method(counterparty_id, payout_method_id));
|
|
||||||
|
|
||||||
self.http_delete(|base| format!("{base}/api/counterparties/{counterparty_id}/payout-methods/{payout_method_id}"))
|
|
||||||
.send_mural()
|
.send_mural()
|
||||||
.await
|
.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_mock!(
|
||||||
|
self,
|
||||||
|
create_payout_method(counterparty_id, alias.as_ref(), payout_method)
|
||||||
|
);
|
||||||
|
|
||||||
|
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> {
|
||||||
|
maybe_mock!(
|
||||||
|
self,
|
||||||
|
delete_payout_method(counterparty_id, payout_method_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
@@ -107,18 +121,7 @@ pub enum PayoutMethodPixAccountType {
|
|||||||
BankAccount,
|
BankAccount,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)]
|
||||||
Debug,
|
|
||||||
Display,
|
|
||||||
Clone,
|
|
||||||
Copy,
|
|
||||||
PartialEq,
|
|
||||||
Eq,
|
|
||||||
Hash,
|
|
||||||
Deref,
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
)]
|
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[display("{}", _0.hyphenated())]
|
#[display("{}", _0.hyphenated())]
|
||||||
pub struct PayoutMethodId(pub Uuid);
|
pub struct PayoutMethodId(pub Uuid);
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
use serde::{Deserialize, de::Error};
|
use {
|
||||||
use std::borrow::Cow;
|
rust_iso3166::CountryCode,
|
||||||
|
serde::{Deserialize, de::Error},
|
||||||
|
std::borrow::Cow,
|
||||||
|
};
|
||||||
|
|
||||||
use rust_iso3166::CountryCode;
|
pub fn serialize<S: serde::Serializer>(v: &CountryCode, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
|
||||||
pub fn serialize<S: serde::Serializer>(
|
|
||||||
v: &CountryCode,
|
|
||||||
serializer: S,
|
|
||||||
) -> Result<S::Ok, S::Error> {
|
|
||||||
serializer.serialize_str(v.alpha2)
|
serializer.serialize_str(v.alpha2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,8 +15,6 @@ pub fn deserialize<'de, D: serde::Deserializer<'de>>(
|
|||||||
rust_iso3166::ALPHA2_MAP
|
rust_iso3166::ALPHA2_MAP
|
||||||
.get(&country_code)
|
.get(&country_code)
|
||||||
.copied()
|
.copied()
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| D::Error::custom("invalid ISO 3166 alpha-2 country code"))
|
||||||
D::Error::custom("invalid ISO 3166 alpha-2 country code")
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
138
packages/muralpay/src/transaction.rs
Normal file
138
packages/muralpay/src/transaction.rs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use derive_more::{Deref, Display};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{AccountId, Blockchain, FiatAmount, PayoutId, PayoutRequestId, TokenAmount};
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
const _: () = {
|
||||||
|
use crate::{Account, MuralError, RequestExt, SearchParams, SearchResponse};
|
||||||
|
|
||||||
|
impl crate::Client {
|
||||||
|
pub async fn get_transaction(&self, id: TransactionId) -> Result<Transaction, MuralError> {
|
||||||
|
maybe_mock!(self, get_transaction(id));
|
||||||
|
|
||||||
|
self.http_get(|base| format!("{base}/api/transactions/{id}"))
|
||||||
|
.send_mural()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search_transactions(
|
||||||
|
&self,
|
||||||
|
account_id: AccountId,
|
||||||
|
params: Option<SearchParams<AccountId>>,
|
||||||
|
) -> Result<SearchResponse<AccountId, Account>, MuralError> {
|
||||||
|
maybe_mock!(self, search_transactions(account_id, params));
|
||||||
|
|
||||||
|
self.http_post(|base| format!("{base}/api/transactions/search/account/{account_id}"))
|
||||||
|
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
||||||
|
.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 TransactionId(pub Uuid);
|
||||||
|
|
||||||
|
impl FromStr for TransactionId {
|
||||||
|
type Err = <Uuid as FromStr>::Err;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
s.parse::<Uuid>().map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TransactionId> for Uuid {
|
||||||
|
fn from(value: TransactionId) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Transaction {
|
||||||
|
pub id: TransactionId,
|
||||||
|
pub hash: String,
|
||||||
|
pub transaction_execution_date: DateTime<Utc>,
|
||||||
|
pub memo: Option<String>,
|
||||||
|
pub blockchain: Blockchain,
|
||||||
|
pub amount: TokenAmount,
|
||||||
|
pub account_id: AccountId,
|
||||||
|
// pub counterparty_info,
|
||||||
|
pub transaction_details: TransactionDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
pub enum TransactionDetails {
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Payout {
|
||||||
|
payout_request_id: PayoutRequestId,
|
||||||
|
payout_id: PayoutId,
|
||||||
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Deposit { details: DepositDetails },
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
ExternalPayout { recipient_wallet_address: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
pub enum DepositDetails {
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Fiat {
|
||||||
|
deposit_id: Uuid,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
sent_fiat_amount: FiatAmount,
|
||||||
|
sender_metadata: Option<SenderMetadata>,
|
||||||
|
deposit_status_info: DepositStatus,
|
||||||
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Blockchain {
|
||||||
|
sender_address: String,
|
||||||
|
blockchain: Blockchain,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
pub enum SenderMetadata {
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Ach {
|
||||||
|
ach_routing_number: String,
|
||||||
|
sender_name: String,
|
||||||
|
description: Option<String>,
|
||||||
|
trace_number: String,
|
||||||
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Wire {
|
||||||
|
wire_routing_number: String,
|
||||||
|
sender_name: Option<String>,
|
||||||
|
bank_name: String,
|
||||||
|
bank_beneficiary_name: String,
|
||||||
|
imad: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
pub enum DepositStatus {
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
AwaitingFunds,
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Completed {
|
||||||
|
initiated_at: DateTime<Utc>,
|
||||||
|
completed_at: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,85 +1,3 @@
|
|||||||
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 {
|
macro_rules! display_as_serialize {
|
||||||
($T:ty) => {
|
($T:ty) => {
|
||||||
const _: () = {
|
const _: () = {
|
||||||
@@ -87,8 +5,7 @@ macro_rules! display_as_serialize {
|
|||||||
|
|
||||||
impl fmt::Display for $T {
|
impl fmt::Display for $T {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let value =
|
let value = serde_json::to_value(self).map_err(|_| fmt::Error)?;
|
||||||
serde_json::to_value(self).map_err(|_| fmt::Error)?;
|
|
||||||
let value = value.as_str().ok_or(fmt::Error)?;
|
let value = value.as_str().ok_or(fmt::Error)?;
|
||||||
write!(f, "{value}")
|
write!(f, "{value}")
|
||||||
}
|
}
|
||||||
@@ -96,5 +13,4 @@ macro_rules! display_as_serialize {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) use display_as_serialize;
|
pub(crate) use display_as_serialize;
|
||||||
|
|||||||
Reference in New Issue
Block a user