Files
Rocketmc/apps/labrinth/src/queue/payouts/mural.rs
aecsocket 17f395ee55 Mural Pay integration (#4520)
* wip: muralpay integration

* Basic Mural Pay API bindings

* Fix clippy

* use dotenvy in muralpay example

* Refactor payout creation code

* wip: muralpay payout requests

* Mural Pay payouts work

* Fix clippy

* add mural pay fees API

* Work on payout fee API

* Fees API for more payment methods

* Fix CI

* Temporarily disable Venmo and PayPal methods from frontend

* wip: counterparties

* Start on counterparties and payment methods API

* Mural Pay multiple methods when fetching

* Don't send supported_countries to frontend

* Add countries to muralpay fiat methods

* Compile fix

* Add exchange rate info to fees endpoint

* Add fees to premium Tremendous options

* Add delivery email field to Tremendous payouts

* Add Tremendous product category to payout methods

* Add bank details API to muralpay

* Fix CI

* Fix CI

* Remove prepaid visa, compute fees properly for Tremendous methods

* Add more details to Tremendous errors

* Add fees to Mural

* Payout history route and bank details

* Re-add legacy PayPal/Venmo options for US

* move the mural bank details route

* Add utoipa support to payout endpoints

* address some PR comments

* add CORS to new utoipa routes

* Immediately approve mural payouts

* Add currency support to Tremendous payouts

* Currency forex

* add forex to tremendous fee request

* Add Mural balance to bank balance info

* Add more Tremendous currencies support

* Transaction payouts available use the correct date

* Address my own review comment

* Address PR comments

* Change Mural withdrawal limit to 3k

* maybe fix tremendous gift cards

* Change how Mural minimum withdrawals are calculated

* Tweak min/max withdrawal values

---------

Co-authored-by: Calum H. <contact@cal.engineer>
Co-authored-by: Alejandro González <me@alegon.dev>
2025-11-03 14:19:46 -08:00

181 lines
5.5 KiB
Rust

use ariadne::ids::UserId;
use eyre::Result;
use muralpay::{MuralError, TokenFeeRequest};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::{
queue::payouts::{AccountBalance, PayoutsQueue},
routes::ApiError,
util::error::Context,
};
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MuralPayoutRequest {
Fiat {
bank_name: String,
bank_account_owner: String,
fiat_and_rail_details: muralpay::FiatAndRailDetails,
},
Blockchain {
wallet_address: String,
},
}
impl PayoutsQueue {
pub async fn compute_muralpay_fees(
&self,
amount: Decimal,
fiat_and_rail_code: muralpay::FiatAndRailCode,
) -> Result<muralpay::TokenPayoutFee, ApiError> {
let muralpay = self.muralpay.load();
let muralpay = muralpay
.as_ref()
.wrap_internal_err("Mural Pay client not available")?;
let fees = muralpay
.client
.get_fees_for_token_amount(&[TokenFeeRequest {
amount: muralpay::TokenAmount {
token_symbol: muralpay::USDC.into(),
token_amount: amount,
},
fiat_and_rail_code,
}])
.await
.wrap_internal_err("failed to request fees")?;
let fee = fees
.into_iter()
.next()
.wrap_internal_err("no fees returned")?;
Ok(fee)
}
pub async fn create_muralpay_payout_request(
&self,
user_id: UserId,
amount: muralpay::TokenAmount,
payout_details: MuralPayoutRequest,
recipient_info: muralpay::PayoutRecipientInfo,
) -> Result<muralpay::PayoutRequest, ApiError> {
let muralpay = self.muralpay.load();
let muralpay = muralpay
.as_ref()
.wrap_internal_err("Mural Pay client not available")?;
let payout_details = match payout_details {
MuralPayoutRequest::Fiat {
bank_name,
bank_account_owner,
fiat_and_rail_details,
} => muralpay::CreatePayoutDetails::Fiat {
bank_name,
bank_account_owner,
developer_fee: None,
fiat_and_rail_details,
},
MuralPayoutRequest::Blockchain { wallet_address } => {
muralpay::CreatePayoutDetails::Blockchain {
wallet_details: muralpay::WalletDetails {
// only Polygon chain is currently supported
blockchain: muralpay::Blockchain::Polygon,
wallet_address,
},
}
}
};
let payout = muralpay::CreatePayout {
amount,
payout_details,
recipient_info,
supporting_details: None,
};
let payout_request = muralpay
.client
.create_payout_request(
muralpay.source_account_id,
Some(format!("User {user_id}")),
&[payout],
)
.await
.map_err(|err| match err {
MuralError::Api(err) => ApiError::Request(err.into()),
err => ApiError::Internal(err.into()),
})?;
// 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(
"failed to cancel unexecuted payout request",
)?;
return Err(ApiError::Internal(err));
}
Ok(payout_request)
}
pub async fn cancel_muralpay_payout_request(
&self,
id: muralpay::PayoutRequestId,
) -> Result<()> {
let muralpay = self.muralpay.load();
let muralpay = muralpay
.as_ref()
.wrap_err("Mural Pay client not available")?;
muralpay.client.cancel_payout_request(id).await?;
Ok(())
}
pub async fn get_mural_balance(&self) -> Result<Option<AccountBalance>> {
let muralpay = self.muralpay.load();
let muralpay = muralpay
.as_ref()
.wrap_err("Mural Pay client not available")?;
let account = muralpay
.client
.get_account(muralpay.source_account_id)
.await?;
let details = account
.account_details
.wrap_err("source account does not have details")?;
let available = details
.balances
.iter()
.map(|balance| {
if balance.token_symbol == muralpay::USDC {
balance.token_amount
} else {
Decimal::ZERO
}
})
.sum::<Decimal>();
Ok(Some(AccountBalance {
available,
pending: Decimal::ZERO,
}))
}
}