Files
Rocketmc/packages/muralpay/examples/muralpay.rs
aecsocket 9706f1597b Supporting documents for Mural payouts (#4721)
* wip: gotenberg

* Generate and provide supporting docs for Mural payouts

* Correct docs

* shear

* update cargo lock because r-a complains otherwise

* Remove local Gotenberg queue and use Redis instead

* Store platform_id in database correctly

* Address PR comments

* Fix up CI

* fix rebase

* Add timeout to default env vars
2025-11-08 23:27:31 +00:00

331 lines
9.6 KiB
Rust

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);
}
}
}