diff --git a/docs/mitractl.md b/docs/mitractl.md index 6a3e443..1b3059b 100644 --- a/docs/mitractl.md +++ b/docs/mitractl.md @@ -85,3 +85,9 @@ Create Monero wallet: ```shell mitractl create-monero-wallet "mitra-wallet" "passw0rd" ``` + +Check expired invoice: + +```shell +mitractl check-expired-invoice 0184b062-d8d5-cbf1-a71b-6d1aafbae2ab +``` diff --git a/src/bin/mitractl.rs b/src/bin/mitractl.rs index 9d7d800..8f9a987 100644 --- a/src/bin/mitractl.rs +++ b/src/bin/mitractl.rs @@ -36,6 +36,7 @@ async fn main() { SubCommand::UpdateCurrentBlock(cmd) => cmd.execute(&config, db_client).await.unwrap(), SubCommand::ResetSubscriptions(cmd) => cmd.execute(&config, db_client).await.unwrap(), SubCommand::CreateMoneroWallet(cmd) => cmd.execute(&config).await.unwrap(), + SubCommand::CheckExpiredInvoice(cmd) => cmd.execute(&config, db_client).await.unwrap(), _ => panic!(), }; }, diff --git a/src/cli.rs b/src/cli.rs index 9971c59..8273e5a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -29,7 +29,10 @@ use crate::models::users::queries::{ get_user_by_id, set_user_password, }; -use crate::monero::wallet::create_monero_wallet; +use crate::monero::{ + helpers::check_expired_invoice, + wallet::create_monero_wallet, +}; use crate::utils::{ crypto_rsa::{ generate_rsa_key, @@ -64,6 +67,7 @@ pub enum SubCommand { UpdateCurrentBlock(UpdateCurrentBlock), ResetSubscriptions(ResetSubscriptions), CreateMoneroWallet(CreateMoneroWallet), + CheckExpiredInvoice(CheckExpiredInvoice), } /// Generate RSA private key @@ -404,3 +408,27 @@ impl CreateMoneroWallet { Ok(()) } } + +/// Check expired invoice +#[derive(Parser)] +pub struct CheckExpiredInvoice { + id: Uuid, +} + +impl CheckExpiredInvoice { + pub async fn execute( + &self, + config: &Config, + db_client: &impl GenericClient, + ) -> Result<(), Error> { + let monero_config = config.blockchain() + .and_then(|conf| conf.monero_config()) + .ok_or(anyhow!("monero configuration not found"))?; + check_expired_invoice( + monero_config, + db_client, + &self.id, + ).await?; + Ok(()) + } +} diff --git a/src/models/invoices/queries.rs b/src/models/invoices/queries.rs index e793b98..3067d5b 100644 --- a/src/models/invoices/queries.rs +++ b/src/models/invoices/queries.rs @@ -156,6 +156,6 @@ mod tests { assert_eq!(invoice.chain_id, chain_id); assert_eq!(invoice.payment_address, payment_address); assert_eq!(invoice.amount, amount); - assert!(matches!(invoice.invoice_status, InvoiceStatus::Open)); + assert_eq!(invoice.invoice_status, InvoiceStatus::Open); } } diff --git a/src/models/invoices/types.rs b/src/models/invoices/types.rs index 7029ae0..8e89c40 100644 --- a/src/models/invoices/types.rs +++ b/src/models/invoices/types.rs @@ -8,7 +8,7 @@ use crate::database::int_enum::{int_enum_from_sql, int_enum_to_sql}; use crate::errors::ConversionError; use crate::utils::caip2::ChainId; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum InvoiceStatus { Open, Paid, diff --git a/src/monero/helpers.rs b/src/monero/helpers.rs new file mode 100644 index 0000000..3b65c9f --- /dev/null +++ b/src/monero/helpers.rs @@ -0,0 +1,59 @@ +use std::str::FromStr; + +use monero_rpc::TransferType; +use monero_rpc::monero::Address; +use tokio_postgres::GenericClient; +use uuid::Uuid; + +use crate::config::MoneroConfig; +use crate::models::{ + invoices::queries::{ + get_invoice_by_id, + set_invoice_status, + }, + invoices::types::InvoiceStatus, +}; +use super::wallet::{ + open_monero_wallet, + DEFAULT_ACCOUNT, + MoneroError, +}; + +pub async fn check_expired_invoice( + config: &MoneroConfig, + db_client: &impl GenericClient, + invoice_id: &Uuid, +) -> Result<(), MoneroError> { + let wallet_client = open_monero_wallet(config).await?; + let invoice = get_invoice_by_id(db_client, invoice_id).await?; + if invoice.chain_id != config.chain_id || + invoice.invoice_status != InvoiceStatus::Timeout + { + return Err(MoneroError::OtherError("can't process invoice")); + }; + let address = Address::from_str(&invoice.payment_address)?; + let address_index = wallet_client.get_address_index(address).await?; + let transfers = wallet_client.incoming_transfers( + TransferType::Available, + Some(DEFAULT_ACCOUNT), + Some(vec![address_index.minor]), + ).await? + .transfers + .unwrap_or_default(); + if transfers.is_empty() { + log::info!("no incoming transfers"); + } else { + for transfer in transfers { + if transfer.subaddr_index != address_index { + return Err(MoneroError::WalletRpcError("unexpected transfer")); + }; + log::info!( + "received payment for invoice {}: {}", + invoice.id, + transfer.amount, + ); + }; + set_invoice_status(db_client, &invoice.id, InvoiceStatus::Paid).await?; + }; + Ok(()) +} diff --git a/src/monero/mod.rs b/src/monero/mod.rs index 1909730..3445b97 100644 --- a/src/monero/mod.rs +++ b/src/monero/mod.rs @@ -1,2 +1,3 @@ +pub mod helpers; pub mod subscriptions; pub mod wallet; diff --git a/src/monero/subscriptions.rs b/src/monero/subscriptions.rs index 8761026..5da0504 100644 --- a/src/monero/subscriptions.rs +++ b/src/monero/subscriptions.rs @@ -2,7 +2,7 @@ use std::convert::TryInto; use std::str::FromStr; use chrono::{Duration, Utc}; -use monero_rpc::{RpcClient, TransferType}; +use monero_rpc::TransferType; use monero_rpc::monero::{Address, Amount}; use crate::config::{Instance, MoneroConfig}; @@ -28,6 +28,7 @@ use crate::models::{ use super::wallet::{ get_single_item, get_subaddress_balance, + open_monero_wallet, send_monero, DEFAULT_ACCOUNT, MoneroError, @@ -41,12 +42,7 @@ pub async fn check_monero_subscriptions( db_pool: &Pool, ) -> Result<(), MoneroError> { let db_client = &mut **get_database_client(db_pool).await?; - - let wallet_client = RpcClient::new(config.wallet_url.clone()).wallet(); - wallet_client.open_wallet( - config.wallet_name.clone(), - config.wallet_password.clone(), - ).await?; + let wallet_client = open_monero_wallet(config).await?; // Invoices waiting for payment let mut address_waitlist = vec![]; @@ -98,7 +94,11 @@ pub async fn check_monero_subscriptions( invoice.id, transfer.amount, ); - set_invoice_status(db_client, &invoice.id, InvoiceStatus::Paid).await?; + if invoice.invoice_status == InvoiceStatus::Open { + set_invoice_status(db_client, &invoice.id, InvoiceStatus::Paid).await?; + } else { + log::warn!("invoice has already been paid"); + }; }; }; diff --git a/src/monero/wallet.rs b/src/monero/wallet.rs index 0055ce3..88204d0 100644 --- a/src/monero/wallet.rs +++ b/src/monero/wallet.rs @@ -48,14 +48,21 @@ pub async fn create_monero_wallet( Ok(()) } -pub async fn create_monero_address( +pub async fn open_monero_wallet( config: &MoneroConfig, -) -> Result { +) -> Result { let wallet_client = RpcClient::new(config.wallet_url.clone()).wallet(); wallet_client.open_wallet( config.wallet_name.clone(), config.wallet_password.clone(), ).await?; + Ok(wallet_client) +} + +pub async fn create_monero_address( + config: &MoneroConfig, +) -> Result { + let wallet_client = open_monero_wallet(config).await?; let (address, address_index) = wallet_client.create_address(DEFAULT_ACCOUNT, None).await?; log::info!("created monero address {}/{}", DEFAULT_ACCOUNT, address_index);