use crate::exchange::{Currency, CurrencyAmount}; use crate::fiat::{FiatPaymentInfo, FiatPaymentService}; use crate::json_api::{JsonApi, TokenGen}; use crate::settings::RevolutConfig; use anyhow::{bail, Result}; use chrono::{DateTime, Utc}; use nostr::Url; use reqwest::header::AUTHORIZATION; use reqwest::{Method, RequestBuilder}; use serde::{Deserialize, Serialize}; use std::future::Future; use std::pin::Pin; #[derive(Clone)] pub struct RevolutApi { api: JsonApi, } #[derive(Clone)] struct RevolutTokenGen { pub token: String, pub api_version: String, } impl TokenGen for RevolutTokenGen { fn generate_token( &self, _method: Method, _url: &Url, _body: Option<&str>, req: RequestBuilder, ) -> Result { Ok(req .header(AUTHORIZATION, format!("Bearer {}", &self.token)) .header("Revolut-Api-Version", &self.api_version)) } } impl RevolutApi { pub fn new(config: RevolutConfig) -> Result { let gen = RevolutTokenGen { token: config.token, api_version: config.api_version, }; const DEFAULT_URL: &str = "https://merchant.revolut.com"; Ok(Self { api: JsonApi::token_gen(&config.url.unwrap_or(DEFAULT_URL.to_string()), false, gen)?, }) } pub async fn list_webhooks(&self) -> Result> { self.api.get("/api/1.0/webhooks").await } pub async fn delete_webhook(&self, webhook_id: &str) -> Result<()> { self.api .req_status( Method::DELETE, &format!("/api/1.0/webhooks/{}", webhook_id), (), ) .await?; Ok(()) } pub async fn create_webhook( &self, url: &str, events: Vec, ) -> Result { self.api .post( "/api/1.0/webhooks", CreateWebhookRequest { url: url.to_string(), events, }, ) .await } pub async fn create_order( &self, amount: CurrencyAmount, description: Option, ) -> Result { self.api .post( "/api/orders", CreateOrderRequest { currency: amount.0.to_string(), amount: match amount.0 { Currency::BTC => bail!("Bitcoin amount not allowed for fiat payments"), _ => amount.value(), }, description, }, ) .await } pub async fn get_order(&self, order_id: &str) -> Result { self.api.get(&format!("/api/orders/{}", order_id)).await } } impl FiatPaymentService for RevolutApi { fn create_order( &self, description: &str, amount: CurrencyAmount, ) -> Pin> + Send>> { let s = self.clone(); let desc = description.to_string(); Box::pin(async move { let rsp = s.create_order(amount, Some(desc)).await?; Ok(FiatPaymentInfo { raw_data: serde_json::to_string(&rsp)?, external_id: rsp.id, }) }) } } #[derive(Clone, Serialize)] pub struct CreateOrderRequest { pub amount: u64, pub currency: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, } #[derive(Clone, Deserialize, Serialize)] pub struct RevolutOrder { pub id: String, pub token: String, pub state: RevolutOrderState, pub created_at: DateTime, pub updated_at: DateTime, pub description: Option, pub amount: u64, pub currency: String, pub outstanding_amount: u64, #[serde(skip_serializing_if = "Option::is_none")] pub checkout_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub payments: Option>, } #[derive(Clone, Deserialize, Serialize)] pub struct RevolutOrderPayment { pub id: String, pub state: RevolutPaymentState, #[serde(skip_serializing_if = "Option::is_none")] pub decline_reason: Option, #[serde(skip_serializing_if = "Option::is_none")] pub bank_message: Option, pub created_at: DateTime, pub updated_at: DateTime, #[serde(skip_serializing_if = "Option::is_none")] pub token: Option, pub amount: u64, #[serde(skip_serializing_if = "Option::is_none")] pub currency: Option, #[serde(skip_serializing_if = "Option::is_none")] pub settled_amount: Option, #[serde(skip_serializing_if = "Option::is_none")] pub settled_currency: Option, #[serde(skip_serializing_if = "Option::is_none")] pub payment_method: Option, #[serde(skip_serializing_if = "Option::is_none")] pub billing_address: Option, #[serde(skip_serializing_if = "Option::is_none")] pub risk_level: Option, } #[derive(Clone, Deserialize, Serialize)] pub struct RevolutPaymentMethod { #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(rename = "type")] pub kind: RevolutPaymentMethodType, #[serde(skip_serializing_if = "Option::is_none")] pub card_brand: Option, #[serde(skip_serializing_if = "Option::is_none")] pub funding: Option, #[serde(skip_serializing_if = "Option::is_none")] pub card_country_code: Option, #[serde(skip_serializing_if = "Option::is_none")] pub card_bin: Option, #[serde(skip_serializing_if = "Option::is_none")] pub card_last_four: Option, #[serde(skip_serializing_if = "Option::is_none")] pub card_expiry: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cardholder_name: Option, } #[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum RevolutPaymentMethodType { ApplePay, Card, GooglePay, RevolutPayCard, RevolutPayAccount, } #[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum RevolutRiskLevel { High, Low, } #[derive(Clone, Deserialize, Serialize)] pub struct RevolutBillingAddress { #[serde(skip_serializing_if = "Option::is_none")] pub street_line_1: Option, #[serde(skip_serializing_if = "Option::is_none")] pub street_line_2: Option, #[serde(skip_serializing_if = "Option::is_none")] pub region: Option, #[serde(skip_serializing_if = "Option::is_none")] pub city: Option, pub country_code: String, pub postcode: String, } #[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum RevolutOrderState { Pending, Processing, Authorised, Completed, Cancelled, Failed, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum RevolutPaymentState { Pending, AuthenticationChallenge, AuthenticationVerified, AuthorisationStarted, AuthorisationPassed, Authorised, CaptureStarted, Captured, RefundValidated, RefundStarted, CancellationStarted, Declining, Completing, Cancelling, Failing, Completed, Declined, SoftDeclined, Cancelled, Failed, } #[derive(Clone, Deserialize, Serialize)] pub struct RevolutWebhook { pub id: String, pub url: String, pub events: Vec, pub signing_secret: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum RevolutWebhookEvent { OrderAuthorised, OrderCompleted, OrderCancelled, } #[derive(Clone, Deserialize, Serialize)] pub struct CreateWebhookRequest { pub url: String, pub events: Vec, }