diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 2b43c6ac9e..0f06ffd0a7 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -62,14 +62,15 @@ use web3_transport::{EthFeeHistoryNamespace, Web3Transport, Web3TransportNode}; use super::{coin_conf, AsyncMutex, BalanceError, BalanceFut, CoinBalance, CoinFutSpawner, CoinProtocol, CoinTransportMetrics, CoinsContext, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, MyAddressError, NegotiateSwapContractAddrErr, NumConversError, NumConversResult, - RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, RawTransactionResult, - RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, SearchForSwapTxSpendInput, - SignatureError, SignatureResult, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, - TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails, TransactionEnum, TransactionErr, - TransactionFut, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationError, - VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; + PaymentInstructions, PaymentInstructionsErr, RawTransactionError, RawTransactionFut, + RawTransactionRequest, RawTransactionRes, RawTransactionResult, RpcClientType, RpcTransportEventHandler, + RpcTransportEventHandlerShared, SearchForSwapTxSpendInput, SignatureError, SignatureResult, SwapOps, + TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, + WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WithdrawError, WithdrawFee, + WithdrawFut, WithdrawRequest, WithdrawResult}; pub use rlp; @@ -737,6 +738,7 @@ impl SwapOps for EthCoin { amount: BigDecimal, swap_contract_address: &Option, _swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { let taker_addr = try_tx_fus!(addr_from_raw_pubkey(taker_pub)); let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); @@ -763,6 +765,7 @@ impl SwapOps for EthCoin { amount: BigDecimal, swap_contract_address: &Option, _swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { let maker_addr = try_tx_fus!(addr_from_raw_pubkey(maker_pub)); let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); @@ -1097,7 +1100,7 @@ impl SwapOps for EthCoin { unimplemented!(); } - fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { let unverified: UnverifiedTransaction = try_s!(rlp::decode(spend_tx)); let function = try_s!(SWAP_CONTRACT.function("receiverSpend")); let tokens = try_s!(function.decode_input(&unverified.data)); @@ -1150,6 +1153,25 @@ impl SwapOps for EthCoin { }; Ok(()) } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { false } } #[async_trait] diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index b5b56b33ca..d717eb62b1 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -257,6 +257,7 @@ fn send_and_refund_erc20_payment() { "0.001".parse().unwrap(), &coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -327,6 +328,7 @@ fn send_and_refund_eth_payment() { "0.001".parse().unwrap(), &coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -1416,7 +1418,7 @@ fn test_eth_extract_secret() { 100, 189, 72, 74, 221, 144, 66, 170, 68, 121, 29, 105, 19, 194, 35, 245, 196, 131, 236, 29, 105, 101, 30, ]; - let secret = coin.extract_secret(&[0u8; 20], tx_hex.as_slice()); + let secret = block_on(coin.extract_secret(&[0u8; 20], tx_hex.as_slice())); assert!(secret.is_ok()); let expect_secret = &[ 168, 151, 11, 232, 224, 253, 63, 180, 26, 114, 23, 184, 27, 10, 161, 80, 178, 251, 73, 204, 80, 174, 97, 118, diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 0adc8443e1..e81fb0e263 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -58,6 +58,7 @@ async fn test_send() { "0.001".parse().unwrap(), &None, &[], + &None, ) .compat() .await; diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 6b500a138b..3fe081e5e9 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -1,71 +1,61 @@ pub mod ln_conf; -mod ln_db; +pub(crate) mod ln_db; pub mod ln_errors; mod ln_events; mod ln_filesystem_persister; -mod ln_p2p; +pub(crate) mod ln_p2p; mod ln_platform; -mod ln_serialization; +pub(crate) mod ln_serialization; mod ln_sql; -mod ln_storage; +pub(crate) mod ln_storage; mod ln_utils; -use super::{lp_coinfind_or_err, DerivationMethod, MmCoinEnum}; use crate::coin_errors::MyAddressError; -use crate::lightning::ln_conf::OurChannelsConfigs; -use crate::lightning::ln_errors::{TrustedNodeError, TrustedNodeResult, UpdateChannelError, UpdateChannelResult}; -use crate::lightning::ln_events::init_abortable_events; -use crate::lightning::ln_serialization::PublicKeyForRPC; -use crate::lightning::ln_sql::SqliteLightningDB; -use crate::lightning::ln_storage::{NetworkGraph, TrustedNodesShared}; +use crate::lightning::ln_utils::filter_channels; use crate::utxo::rpc_clients::UtxoRpcClientEnum; -use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, UtxoTxBuilder}; -use crate::utxo::{sat_from_big_decimal, BlockchainNetwork, FeePolicy, GetUtxoListOps, UtxoTxGenerationOps}; -use crate::{BalanceFut, CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, - MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, RawTransactionFut, RawTransactionRequest, +use crate::utxo::utxo_common::{big_decimal_from_sat, big_decimal_from_sat_unsigned}; +use crate::utxo::{sat_from_big_decimal, utxo_common, BlockchainNetwork}; +use crate::{BalanceFut, CoinBalance, CoinFutSpawner, DerivationMethod, FeeApproxStage, FoundSwapTxSpend, + HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PaymentInstructions, + PaymentInstructionsErr, RawTransactionError, RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, SignatureError, SignatureResult, SwapOps, TradeFee, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TransactionEnum, TransactionFut, TxMarshalingErr, - UnexpectedDerivationMethod, UtxoStandardCoin, ValidateAddressResult, ValidateOtherPubKeyErr, - ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WatcherOps, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WithdrawError, WithdrawFut, WithdrawRequest}; + TradePreimageResult, TradePreimageValue, Transaction, TransactionEnum, TransactionErr, TransactionFut, + TxMarshalingErr, UnexpectedDerivationMethod, UtxoStandardCoin, ValidateAddressResult, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, VerificationError, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WithdrawError, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; +use bitcoin::bech32::ToBase32; use bitcoin::hashes::Hash; use bitcoin_hashes::sha256::Hash as Sha256; -use bitcrypto::dhash256; use bitcrypto::ChecksumType; -use chain::TransactionOutput; -use common::executor::SpawnFuture; -use common::log::{error, LogOnError, LogState}; -use common::{async_blocking, calc_total_pages, log, now_ms, ten, PagingOptionsEnum}; +use bitcrypto::{dhash256, ripemd160}; +use common::executor::{SpawnFuture, Timer}; +use common::log::{info, LogOnError, LogState}; +use common::{async_blocking, get_local_duration_since_epoch, log, now_ms, PagingOptionsEnum}; +use db_common::sqlite::rusqlite::Error as SqlError; use futures::{FutureExt, TryFutureExt}; use futures01::Future; -use keys::{hash::H256, AddressHashEnum, CompactSignature, KeyPair, Private, Public}; -use lightning::chain::channelmonitor::Balance; +use keys::{hash::H256, CompactSignature, KeyPair, Private, Public}; use lightning::chain::keysinterface::{KeysInterface, KeysManager, Recipient}; use lightning::chain::Access; use lightning::ln::channelmanager::{ChannelDetails, MIN_FINAL_CLTV_EXPIRY}; use lightning::ln::{PaymentHash, PaymentPreimage}; use lightning::routing::gossip; -use lightning::util::config::UserConfig; use lightning_background_processor::{BackgroundProcessor, GossipSync}; -use lightning_invoice::payment; -use lightning_invoice::utils::{create_invoice_from_channelmanager, DefaultRouter}; +use lightning_invoice::utils::DefaultRouter; +use lightning_invoice::{payment, CreationError, InvoiceBuilder, SignOrCreationError}; use lightning_invoice::{Invoice, InvoiceDescription}; -use ln_conf::{ChannelOptions, LightningCoinConf, LightningProtocolConf, PlatformCoinConfirmationTargets}; -use ln_db::{ClosedChannelsFilter, DBChannelDetails, DBPaymentInfo, DBPaymentsFilter, HTLCStatus, LightningDB, - PaymentType}; -use ln_errors::{ClaimableBalancesError, ClaimableBalancesResult, CloseChannelError, CloseChannelResult, - ConnectToNodeError, ConnectToNodeResult, EnableLightningError, EnableLightningResult, - GenerateInvoiceError, GenerateInvoiceResult, GetChannelDetailsError, GetChannelDetailsResult, - GetPaymentDetailsError, GetPaymentDetailsResult, ListChannelsError, ListChannelsResult, - ListPaymentsError, ListPaymentsResult, OpenChannelError, OpenChannelResult, SendPaymentError, - SendPaymentResult}; -use ln_events::LightningEventHandler; +use ln_conf::{LightningCoinConf, LightningProtocolConf, PlatformCoinConfirmationTargets}; +use ln_db::{DBChannelDetails, HTLCStatus, LightningDB, PaymentInfo, PaymentType}; +use ln_errors::{EnableLightningError, EnableLightningResult}; +use ln_events::{init_abortable_events, LightningEventHandler}; use ln_filesystem_persister::LightningFilesystemPersister; -use ln_p2p::{connect_to_node, ConnectToNodeRes, PeerManager}; -use ln_platform::{h256_json_from_txid, Platform}; -use ln_serialization::NodeAddress; -use ln_storage::{LightningStorage, NodesAddressesMapShared, Scorer}; +use ln_p2p::PeerManager; +use ln_platform::Platform; +use ln_serialization::{ChannelDetailsForRPC, PublicKeyForRPC}; +use ln_sql::SqliteLightningDB; +use ln_storage::{LightningStorage, NetworkGraph, NodesAddressesMapShared, Scorer, TrustedNodesShared}; use ln_utils::{ChainMonitor, ChannelManager}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -73,11 +63,10 @@ use mm2_net::ip_addr::myipaddr; use mm2_number::{BigDecimal, MmNumber}; use parking_lot::Mutex as PaMutex; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; -use script::{Builder, TransactionInputSigner}; +use script::TransactionInputSigner; use secp256k1v22::PublicKey; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; -use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::fmt; use std::net::SocketAddr; @@ -120,13 +109,60 @@ impl fmt::Debug for LightningCoin { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "LightningCoin {{ conf: {:?} }}", self.conf) } } +#[derive(Deserialize)] +pub struct OpenChannelsFilter { + pub channel_id: Option, + pub counterparty_node_id: Option, + pub funding_tx: Option, + pub from_funding_value_sats: Option, + pub to_funding_value_sats: Option, + pub is_outbound: Option, + pub from_balance_msat: Option, + pub to_balance_msat: Option, + pub from_outbound_capacity_msat: Option, + pub to_outbound_capacity_msat: Option, + pub from_inbound_capacity_msat: Option, + pub to_inbound_capacity_msat: Option, + pub is_ready: Option, + pub is_usable: Option, + pub is_public: Option, +} + +pub(crate) struct GetOpenChannelsResult { + pub channels: Vec, + pub skipped: usize, + pub total: usize, +} + +#[derive(Debug, Display)] +pub(crate) enum PaymentError { + #[display(fmt = "Final cltv expiry delta {} is below the required minimum of {}", _0, _1)] + CLTVExpiry(u32, u32), + #[display(fmt = "Error paying invoice: {}", _0)] + Invoice(String), + #[display(fmt = "Keysend error: {}", _0)] + Keysend(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl From for PaymentError { + fn from(err: SqlError) -> PaymentError { PaymentError::DbError(err.to_string()) } +} + +impl Transaction for PaymentHash { + fn tx_hex(&self) -> Vec { self.0.to_vec() } + + fn tx_hash(&self) -> BytesJson { self.0.to_vec().into() } +} + impl LightningCoin { - fn platform_coin(&self) -> &UtxoStandardCoin { &self.platform.coin } + pub fn platform_coin(&self) -> &UtxoStandardCoin { &self.platform.coin } #[inline] fn my_node_id(&self) -> String { self.channel_manager.get_our_node_id().to_string() } - async fn list_channels(&self) -> Vec { + pub(crate) async fn list_channels(&self) -> Vec { let channel_manager = self.channel_manager.clone(); async_blocking(move || channel_manager.list_channels()).await } @@ -147,14 +183,14 @@ impl LightningCoin { }) } - async fn get_channel_by_rpc_id(&self, rpc_id: u64) -> Option { + pub(crate) async fn get_channel_by_rpc_id(&self, rpc_id: u64) -> Option { self.list_channels() .await .into_iter() .find(|chan| chan.user_channel_id == rpc_id) } - async fn pay_invoice(&self, invoice: Invoice) -> SendPaymentResult { + pub(crate) async fn pay_invoice(&self, invoice: Invoice) -> Result> { let payment_hash = PaymentHash((invoice.payment_hash()).into_inner()); let payment_type = PaymentType::OutboundPayment { destination: *invoice.payee_pub_key().unwrap_or(&invoice.recover_payee_pub_key()), @@ -171,11 +207,11 @@ impl LightningCoin { selfi .invoice_payer .pay_invoice(&invoice) - .map_to_mm(|e| SendPaymentError::PaymentError(format!("{:?}", e))) + .map_to_mm(|e| PaymentError::Invoice(format!("{:?}", e))) }) .await?; - Ok(DBPaymentInfo { + let payment_info = PaymentInfo { payment_hash, payment_type, description, @@ -186,20 +222,19 @@ impl LightningCoin { status: HTLCStatus::Pending, created_at: (now_ms() / 1000) as i64, last_updated: (now_ms() / 1000) as i64, - }) + }; + self.db.add_or_update_payment_in_db(payment_info.clone()).await?; + Ok(payment_info) } - async fn keysend( + pub(crate) async fn keysend( &self, destination: PublicKey, amount_msat: u64, final_cltv_expiry_delta: u32, - ) -> SendPaymentResult { + ) -> Result> { if final_cltv_expiry_delta < MIN_FINAL_CLTV_EXPIRY { - return MmError::err(SendPaymentError::CLTVExpiryError( - final_cltv_expiry_delta, - MIN_FINAL_CLTV_EXPIRY, - )); + return MmError::err(PaymentError::CLTVExpiry(final_cltv_expiry_delta, MIN_FINAL_CLTV_EXPIRY)); } let payment_preimage = PaymentPreimage(self.keys_manager.get_secure_random_bytes()); @@ -208,14 +243,13 @@ impl LightningCoin { selfi .invoice_payer .pay_pubkey(destination, payment_preimage, amount_msat, final_cltv_expiry_delta) - .map_to_mm(|e| SendPaymentError::PaymentError(format!("{:?}", e))) + .map_to_mm(|e| PaymentError::Keysend(format!("{:?}", e))) }) .await?; let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).into_inner()); let payment_type = PaymentType::OutboundPayment { destination }; - - Ok(DBPaymentInfo { + let payment_info = PaymentInfo { payment_hash, payment_type, description: "".into(), @@ -226,15 +260,115 @@ impl LightningCoin { status: HTLCStatus::Pending, created_at: (now_ms() / 1000) as i64, last_updated: (now_ms() / 1000) as i64, - }) + }; + self.db.add_or_update_payment_in_db(payment_info.clone()).await?; + + Ok(payment_info) } - async fn get_open_channels_by_filter( + pub(crate) async fn get_open_channels_by_filter( &self, filter: Option, paging: PagingOptionsEnum, limit: usize, - ) -> ListChannelsResult { + ) -> GetOpenChannelsResult { + fn apply_open_channel_filter(channel_details: &ChannelDetailsForRPC, filter: &OpenChannelsFilter) -> bool { + // Checking if channel_id is some and not equal + if filter.channel_id.is_some() && Some(&channel_details.channel_id) != filter.channel_id.as_ref() { + return false; + } + + // Checking if counterparty_node_id is some and not equal + if filter.counterparty_node_id.is_some() + && Some(&channel_details.counterparty_node_id) != filter.counterparty_node_id.as_ref() + { + return false; + } + + // Checking if funding_tx is some and not equal + if filter.funding_tx.is_some() && channel_details.funding_tx != filter.funding_tx { + return false; + } + + // Checking if from_funding_value_sats is some and more than funding_tx_value_sats + if filter.from_funding_value_sats.is_some() + && Some(&channel_details.funding_tx_value_sats) < filter.from_funding_value_sats.as_ref() + { + return false; + } + + // Checking if to_funding_value_sats is some and less than funding_tx_value_sats + if filter.to_funding_value_sats.is_some() + && Some(&channel_details.funding_tx_value_sats) > filter.to_funding_value_sats.as_ref() + { + return false; + } + + // Checking if is_outbound is some and not equal + if filter.is_outbound.is_some() && Some(&channel_details.is_outbound) != filter.is_outbound.as_ref() { + return false; + } + + // Checking if from_balance_msat is some and more than balance_msat + if filter.from_balance_msat.is_some() + && Some(&channel_details.balance_msat) < filter.from_balance_msat.as_ref() + { + return false; + } + + // Checking if to_balance_msat is some and less than balance_msat + if filter.to_balance_msat.is_some() && Some(&channel_details.balance_msat) > filter.to_balance_msat.as_ref() + { + return false; + } + + // Checking if from_outbound_capacity_msat is some and more than outbound_capacity_msat + if filter.from_outbound_capacity_msat.is_some() + && Some(&channel_details.outbound_capacity_msat) < filter.from_outbound_capacity_msat.as_ref() + { + return false; + } + + // Checking if to_outbound_capacity_msat is some and less than outbound_capacity_msat + if filter.to_outbound_capacity_msat.is_some() + && Some(&channel_details.outbound_capacity_msat) > filter.to_outbound_capacity_msat.as_ref() + { + return false; + } + + // Checking if from_inbound_capacity_msat is some and more than outbound_capacity_msat + if filter.from_inbound_capacity_msat.is_some() + && Some(&channel_details.inbound_capacity_msat) < filter.from_inbound_capacity_msat.as_ref() + { + return false; + } + + // Checking if to_inbound_capacity_msat is some and less than inbound_capacity_msat + if filter.to_inbound_capacity_msat.is_some() + && Some(&channel_details.inbound_capacity_msat) > filter.to_inbound_capacity_msat.as_ref() + { + return false; + } + + // Checking if is_ready is some and not equal + if filter.is_ready.is_some() && Some(&channel_details.is_ready) != filter.is_ready.as_ref() { + return false; + } + + // Checking if is_usable is some and not equal + if filter.is_usable.is_some() && Some(&channel_details.is_usable) != filter.is_usable.as_ref() { + return false; + } + + // Checking if is_public is some and not equal + if filter.is_public.is_some() && Some(&channel_details.is_public) != filter.is_public.as_ref() { + return false; + } + + // All checks pass + true + } + let mut total_open_channels: Vec = self.list_channels().await.into_iter().map(From::from).collect(); @@ -267,18 +401,85 @@ impl LightningCoin { open_channels_filtered[offset..].to_vec() }; - Ok(GetOpenChannelsResult { + GetOpenChannelsResult { channels, skipped: offset, total, - }) + } + } + + async fn create_invoice_for_hash( + &self, + payment_hash: PaymentHash, + amt_msat: Option, + description: String, + invoice_expiry_delta_secs: u32, + ) -> Result>> { + let open_channels_nodes = self.open_channels_nodes.lock().clone(); + for (node_pubkey, node_addr) in open_channels_nodes { + ln_p2p::connect_to_ln_node(node_pubkey, node_addr, self.peer_manager.clone()) + .await + .error_log_with_msg(&format!( + "Channel with node: {} can't be used for invoice routing hints due to connection error.", + node_pubkey + )); + } + + // `create_inbound_payment` only returns an error if the amount is greater than the total bitcoin + // supply. + let payment_secret = self + .channel_manager + .create_inbound_payment_for_hash(payment_hash, amt_msat, invoice_expiry_delta_secs) + .map_to_mm(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?; + let our_node_pubkey = self.channel_manager.get_our_node_id(); + // Todo: Check if it's better to use UTC instead of local time for invoice generations + let duration = get_local_duration_since_epoch().expect("for the foreseeable future this shouldn't happen"); + + let mut invoice = InvoiceBuilder::new(self.platform.network.clone().into()) + .description(description) + .duration_since_epoch(duration) + .payee_pub_key(our_node_pubkey) + .payment_hash(Hash::from_inner(payment_hash.0)) + .payment_secret(payment_secret) + .basic_mpp() + // Todo: This will probably be important in locktime calculations in the next PRs and should be validated by the other side + .min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into()) + .expiry_time(core::time::Duration::from_secs(invoice_expiry_delta_secs.into())); + if let Some(amt) = amt_msat { + invoice = invoice.amount_milli_satoshis(amt); + } + + let route_hints = filter_channels(self.channel_manager.list_usable_channels(), amt_msat); + for hint in route_hints { + invoice = invoice.private_route(hint); + } + + let raw_invoice = match invoice.build_raw() { + Ok(inv) => inv, + Err(e) => return MmError::err(SignOrCreationError::CreationError(e)), + }; + let hrp_str = raw_invoice.hrp.to_string(); + let hrp_bytes = hrp_str.as_bytes(); + let data_without_signature = raw_invoice.data.to_base32(); + let signed_raw_invoice = raw_invoice.sign(|_| { + self.keys_manager + .sign_invoice(hrp_bytes, &data_without_signature, Recipient::Node) + }); + match signed_raw_invoice { + Ok(inv) => Ok(Invoice::from_signed(inv).map_err(|_| SignOrCreationError::SignError(()))?), + Err(e) => MmError::err(SignOrCreationError::SignError(e)), + } } } #[async_trait] // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps impl SwapOps for LightningCoin { - fn send_taker_fee(&self, _fee_addr: &[u8], _amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { unimplemented!() } + // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning + fn send_taker_fee(&self, _fee_addr: &[u8], _amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { + let fut = async move { Ok(TransactionEnum::LightningPayment(PaymentHash([1; 32]))) }; + Box::new(fut.boxed().compat()) + } fn send_maker_payment( &self, @@ -289,8 +490,16 @@ impl SwapOps for LightningCoin { _amount: BigDecimal, _swap_contract_address: &Option, _swap_unique_data: &[u8], + payment_instructions: &Option, ) -> TransactionFut { - unimplemented!() + let PaymentInstructions::Lightning(invoice) = + try_tx_fus!(payment_instructions.clone().ok_or("payment_instructions can't be None")); + let coin = self.clone(); + let fut = async move { + let payment = try_tx_s!(coin.pay_invoice(invoice).await); + Ok(payment.payment_hash.into()) + }; + Box::new(fut.boxed().compat()) } fn send_taker_payment( @@ -302,21 +511,47 @@ impl SwapOps for LightningCoin { _amount: BigDecimal, _swap_contract_address: &Option, _swap_unique_data: &[u8], + payment_instructions: &Option, ) -> TransactionFut { - unimplemented!() + let PaymentInstructions::Lightning(invoice) = + try_tx_fus!(payment_instructions.clone().ok_or("payment_instructions can't be None")); + let coin = self.clone(); + let fut = async move { + let payment = try_tx_s!(coin.pay_invoice(invoice).await); + Ok(payment.payment_hash.into()) + }; + Box::new(fut.boxed().compat()) } fn send_maker_spends_taker_payment( &self, - _taker_payment_tx: &[u8], + taker_payment_tx: &[u8], _time_lock: u32, _taker_pub: &[u8], - _secret: &[u8], + secret: &[u8], _secret_hash: &[u8], _swap_contract_address: &Option, _swap_unique_data: &[u8], ) -> TransactionFut { - unimplemented!() + let payment_hash = try_tx_fus!(payment_hash_from_slice(taker_payment_tx)); + let mut preimage = [b' '; 32]; + preimage.copy_from_slice(secret); + + let coin = self.clone(); + let fut = async move { + let payment_preimage = PaymentPreimage(preimage); + coin.channel_manager.claim_funds(payment_preimage); + coin.db + .update_payment_preimage_in_db(payment_hash, payment_preimage) + .await + .error_log_with_msg(&format!( + "Unable to update payment {} information in DB with preimage: {}!", + hex::encode(payment_hash.0), + hex::encode(preimage) + )); + Ok(TransactionEnum::LightningPayment(payment_hash)) + }; + Box::new(fut.boxed().compat()) } fn send_taker_spends_maker_payment( @@ -356,6 +591,7 @@ impl SwapOps for LightningCoin { unimplemented!() } + // Todo: This validates the dummy fee for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning fn validate_fee( &self, _fee_tx: &TransactionEnum, @@ -365,13 +601,54 @@ impl SwapOps for LightningCoin { _min_block_number: u64, _uuid: &[u8], ) -> Box + Send> { - unimplemented!() + Box::new(futures01::future::ok(())) } fn validate_maker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } - fn validate_taker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + let payment_hash = try_f!(payment_hash_from_slice(&input.payment_tx) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))); + let payment_hex = hex::encode(payment_hash.0); + let amt_msat = try_f!(sat_from_big_decimal(&input.amount, self.decimals())); + + let coin = self.clone(); + let fut = async move { + match coin.db.get_payment_from_db(payment_hash).await { + Ok(Some(mut payment)) => { + let amount_sent = payment.amt_msat; + // Todo: Add more validations if needed, locktime is probably the most important + if amount_sent != Some(amt_msat as i64) { + // Free the htlc to allow for this inbound liquidity to be used for other inbound payments + coin.channel_manager.fail_htlc_backwards(&payment_hash); + payment.status = HTLCStatus::Failed; + drop_mutability!(payment); + coin.db + .add_or_update_payment_in_db(payment) + .await + .error_log_with_msg("Unable to update payment information in DB!"); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided payment {} amount {:?} doesn't match required amount {}", + payment_hex, amount_sent, amt_msat + ))); + } + Ok(()) + }, + Ok(None) => MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( + "Payment {} should be found on the database", + payment_hex + ))), + Err(e) => MmError::err(ValidatePaymentError::InternalError(format!( + "Unable to retrieve payment {} from the database error: {}", + payment_hex, e + ))), + } + }; + Box::new(fut.boxed().compat()) + } + + // Todo: This is None for now for the sake of swap P.O.C., this should be implemented probably in next PRs and should be tested across restarts fn check_if_my_payment_sent( &self, _time_lock: u32, @@ -382,7 +659,7 @@ impl SwapOps for LightningCoin { _swap_unique_data: &[u8], _amount: &BigDecimal, ) -> Box, Error = String> + Send> { - unimplemented!() + Box::new(futures01::future::ok(None)) } async fn search_for_swap_tx_spend_my( @@ -403,18 +680,103 @@ impl SwapOps for LightningCoin { unimplemented!(); } - fn extract_secret(&self, _secret_hash: &[u8], _spend_tx: &[u8]) -> Result, String> { unimplemented!() } + async fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + let payment_hash = payment_hash_from_slice(spend_tx).map_err(|e| e.to_string())?; + let payment_hex = hex::encode(payment_hash.0); + + return match self.db.get_payment_from_db(payment_hash).await { + Ok(Some(payment)) => match payment.preimage { + Some(preimage) => Ok(preimage.0.to_vec()), + None => ERR!("Preimage for payment {} should be found on the database", payment_hex), + }, + Ok(None) => ERR!("Payment {} should be found on the database", payment_hex), + Err(e) => ERR!( + "Unable to retrieve payment {} from the database error: {}", + payment_hex, + e + ), + }; + } fn negotiate_swap_contract_addr( &self, _other_side_address: Option<&[u8]>, ) -> Result, MmError> { - unimplemented!() + Ok(None) } - fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { unimplemented!() } + // Todo: This can be changed if private swaps were to be implemented for lightning + fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { + utxo_common::derive_htlc_key_pair(self.platform.coin.as_ref(), swap_unique_data) + } + + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + utxo_common::validate_other_pubkey(raw_pubkey) + } + + async fn payment_instructions( + &self, + secret_hash: &[u8], + amount: &BigDecimal, + ) -> Result>, MmError> { + // lightning decimals should be 11 in config since the smallest divisible unit in lightning coin is msat + let amt_msat = sat_from_big_decimal(amount, self.decimals())?; + let payment_hash = + payment_hash_from_slice(secret_hash).map_to_mm(|e| PaymentInstructionsErr::InternalError(e.to_string()))?; + + // note: No description is provided in the invoice to reduce the payload + // Todo: The invoice expiry should probably be the same as maker_payment_wait/wait_taker_payment + let invoice = self + .create_invoice_for_hash(payment_hash, Some(amt_msat), "".into(), DEFAULT_INVOICE_EXPIRY) + .await + .map_err(|e| PaymentInstructionsErr::LightningInvoiceErr(e.to_string()))?; + Ok(Some(invoice.to_string().into_bytes())) + } + + fn validate_instructions( + &self, + instructions: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + ) -> Result> { + let invoice = Invoice::from_str(&String::from_utf8_lossy(instructions))?; + if invoice.payment_hash().as_inner() != secret_hash + && ripemd160(invoice.payment_hash().as_inner()).as_slice() != secret_hash + { + return Err( + ValidateInstructionsErr::ValidateLightningInvoiceErr("Invalid invoice payment hash!".into()).into(), + ); + } + let invoice_amount = invoice + .amount_milli_satoshis() + .ok_or_else(|| ValidateInstructionsErr::ValidateLightningInvoiceErr("No invoice amount!".into()))?; + if big_decimal_from_sat(invoice_amount as i64, self.decimals()) != amount { + return Err(ValidateInstructionsErr::ValidateLightningInvoiceErr("Invalid invoice amount!".into()).into()); + } + // Todo: continue validation here by comparing locktime, etc.. + Ok(PaymentInstructions::Lightning(invoice)) + } + + // Watchers cannot be used for lightning swaps for now + // Todo: Check if watchers can work in some cases with lightning and implement it if it's possible, the watcher will not be able to retrieve the preimage since it's retrieved through the lightning network + // Todo: The watcher can retrieve the preimage only if he is running a lightning node and is part of the nodes that routed the taker payment which is a very low probability event that shouldn't be considered + fn is_supported_by_watchers(&self) -> bool { false } +} + +#[derive(Debug, Display)] +pub enum PaymentHashFromSliceErr { + #[display(fmt = "Invalid data length of {}", _0)] + InvalidLength(usize), +} - fn validate_other_pubkey(&self, _raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } +fn payment_hash_from_slice(data: &[u8]) -> Result { + let len = data.len(); + if len != 32 { + return Err(PaymentHashFromSliceErr::InvalidLength(len)); + } + let mut hash = [b' '; 32]; + hash.copy_from_slice(data); + Ok(PaymentHash(hash)) } #[async_trait] @@ -544,34 +906,123 @@ impl MarketCoinOps for LightningCoin { )) } - // Todo: Implement this when implementing swaps for lightning as it's is used mainly for swaps + // Todo: Add waiting for confirmations logic for the case of if the channel is closed and the htlc can be claimed on-chain fn wait_for_confirmations( &self, - _tx: &[u8], + tx: &[u8], _confirmations: u64, _requires_nota: bool, - _wait_until: u64, - _check_every: u64, + wait_until: u64, + check_every: u64, ) -> Box + Send> { - unimplemented!() + let payment_hash = try_f!(payment_hash_from_slice(tx).map_err(|e| e.to_string())); + let payment_hex = hex::encode(payment_hash.0); + + let coin = self.clone(); + let fut = async move { + loop { + if now_ms() / 1000 > wait_until { + return ERR!( + "Waited too long until {} for payment {} to be received", + wait_until, + payment_hex + ); + } + + match coin.db.get_payment_from_db(payment_hash).await { + Ok(Some(_)) => { + // Todo: This should check for different payment statuses depending on where wait_for_confirmations is called, + // Todo: which might lead to breaking wait_for_confirmations to 3 functions (wait_for_payment_sent_confirmations, wait_for_payment_received_confirmations, wait_for_payment_spent_confirmations) + return Ok(()); + }, + Ok(None) => info!("Payment {} not received yet!", payment_hex), + Err(e) => { + return ERR!( + "Error getting payment {} from db: {}, retrying in {} seconds", + payment_hex, + e, + check_every + ) + }, + } + + // note: When sleeping for only 1 second the test_send_payment_and_swaps unit test took 20 seconds to complete instead of 37 seconds when WAIT_CONFIRM_INTERVAL (15 seconds) is used + // Todo: In next sprints, should add a mutex for lightning swap payments to avoid overloading the shared db connection with requests when the sleep time is reduced and multiple swaps are ran together + // Todo: The aim is to make lightning swap payments as fast as possible. Running swap payments statuses should be loaded from db on restarts in this case. + Timer::sleep(check_every as f64).await; + } + }; + Box::new(fut.boxed().compat()) } - // Todo: Implement this when implementing swaps for lightning as it's is used mainly for swaps fn wait_for_htlc_tx_spend( &self, - _transaction: &[u8], + transaction: &[u8], _secret_hash: &[u8], - _wait_until: u64, + wait_until: u64, _from_block: u64, _swap_contract_address: &Option, ) -> TransactionFut { - unimplemented!() + let payment_hash = try_tx_fus!(payment_hash_from_slice(transaction)); + let payment_hex = hex::encode(payment_hash.0); + + let coin = self.clone(); + let fut = async move { + loop { + if now_ms() / 1000 > wait_until { + return Err(TransactionErr::Plain(format!( + "Waited too long until {} for payment {} to be spent", + wait_until, payment_hex + ))); + } + + match coin.db.get_payment_from_db(payment_hash).await { + Ok(Some(payment)) => { + match payment.status { + HTLCStatus::Pending => (), + HTLCStatus::Received => { + return Err(TransactionErr::Plain(format!( + "Payment {} has an invalid status of {} in the db", + payment_hex, payment.status + ))) + }, + HTLCStatus::Succeeded => return Ok(TransactionEnum::LightningPayment(payment_hash)), + // Todo: Retry payment multiple times returning an error only if all paths failed or other permenant error, should also keep locktime in mind when using different paths with different CLTVs + HTLCStatus::Failed => { + return Err(TransactionErr::Plain(format!( + "Lightning swap payment {} failed", + payment_hex + ))) + }, + } + }, + Ok(None) => { + return Err(TransactionErr::Plain(format!( + "Payment {} not found in DB", + payment_hex + ))) + }, + Err(e) => { + return Err(TransactionErr::Plain(format!( + "Error getting payment {} from db: {}", + payment_hex, e + ))) + }, + } + + // note: When sleeping for only 1 second the test_send_payment_and_swaps unit test took 20 seconds to complete instead of 37 seconds when sleeping for 10 seconds + // Todo: In next sprints, should add a mutex for lightning swap payments to avoid overloading the shared db connection with requests when the sleep time is reduced and multiple swaps are ran together. + // Todo: The aim is to make lightning swap payments as fast as possible, more sleep time can be allowed for maker payment since it waits for the secret to be revealed on another chain first. + // Todo: Running swap payments statuses should be loaded from db on restarts in this case. + Timer::sleep(10.).await; + } + }; + Box::new(fut.boxed().compat()) } - // Todo: Implement this when implementing swaps for lightning as it's is used mainly for swaps - fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result> { - MmError::err(TxMarshalingErr::NotSupported( - "tx_enum_from_bytes is not supported for Lightning yet.".to_string(), + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { + Ok(TransactionEnum::LightningPayment( + payment_hash_from_slice(bytes).map_to_mm(|e| TxMarshalingErr::InvalidInput(e.to_string()))?, )) } @@ -586,11 +1037,11 @@ impl MarketCoinOps for LightningCoin { .to_string()) } - // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps - fn min_tx_amount(&self) -> BigDecimal { unimplemented!() } + // Todo: min_tx_amount should depend on inbound_htlc_minimum_msat of the channel/s the payment will be sent through, 1 satoshi is used for for now (1000 of the base unit of lightning which is msat) + fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat(1000, self.decimals()) } - // Todo: Implement this when implementing swaps for lightning as it's is used only for order matching/swaps - fn min_trading_vol(&self) -> MmNumber { unimplemented!() } + // Todo: Equals to min_tx_amount for now (1 satoshi), should change this later + fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } } #[async_trait] @@ -599,8 +1050,13 @@ impl MmCoin for LightningCoin { fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.platform.abortable_system) } - fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { - Box::new(self.platform_coin().get_raw_transaction(req)) + fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { + let fut = async move { + MmError::err(RawTransactionError::InternalError( + "get_raw_transaction method is not supported for lightning, please use get_payment_details method instead.".into(), + )) + }; + Box::new(fut.boxed().compat()) } fn withdraw(&self, _req: WithdrawRequest) -> WithdrawFut { @@ -640,25 +1096,39 @@ impl MmCoin for LightningCoin { // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps fn get_trade_fee(&self) -> Box + Send> { unimplemented!() } - // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps + // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning async fn get_sender_trade_fee( &self, _value: TradePreimageValue, _stage: FeeApproxStage, ) -> TradePreimageResult { - unimplemented!() + Ok(TradeFee { + coin: self.ticker().to_owned(), + amount: Default::default(), + paid_from_trading_vol: false, + }) } - // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning + fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + Box::new(futures01::future::ok(TradeFee { + coin: self.ticker().to_owned(), + amount: Default::default(), + paid_from_trading_vol: false, + })) + } - // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps + // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning async fn get_fee_to_send_taker_fee( &self, _dex_fee_amount: BigDecimal, _stage: FeeApproxStage, ) -> TradePreimageResult { - unimplemented!() + Ok(TradeFee { + coin: self.ticker().to_owned(), + amount: Default::default(), + paid_from_trading_vol: false, + }) } // Lightning payments are either pending, successful or failed. Once a payment succeeds there is no need to for confirmations @@ -675,11 +1145,11 @@ impl MmCoin for LightningCoin { fn mature_confirmations(&self) -> Option { None } - // Todo: Implement this when implementing order matching for lightning as it's is used only for order matching - fn coin_protocol_info(&self) -> Vec { unimplemented!() } + // Todo: This uses default data for now for the sake of swap P.O.C., this should be implemented probably when implementing order matching if it's needed + fn coin_protocol_info(&self) -> Vec { Vec::new() } - // Todo: Implement this when implementing order matching for lightning as it's is used only for order matching - fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { unimplemented!() } + // Todo: This uses default data for now for the sake of swap P.O.C., this should be implemented probably when implementing order matching if it's needed + fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -828,7 +1298,7 @@ pub async fn start_lightning( ln_utils::get_open_channels_nodes_addresses(persister.clone(), channel_manager.clone()).await?, )); - platform.spawner().spawn(ln_p2p::connect_to_nodes_loop( + platform.spawner().spawn(ln_p2p::connect_to_ln_nodes_loop( open_channels_nodes.clone(), peer_manager.clone(), )); @@ -855,994 +1325,3 @@ pub async fn start_lightning( trusted_nodes, }) } - -#[derive(Deserialize)] -pub struct ConnectToNodeRequest { - pub coin: String, - pub node_address: NodeAddress, -} - -/// Connect to a certain node on the lightning network. -pub async fn connect_to_lightning_node(ctx: MmArc, req: ConnectToNodeRequest) -> ConnectToNodeResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(ConnectToNodeError::UnsupportedCoin(e.ticker().to_string())), - }; - - let node_pubkey = req.node_address.pubkey; - let node_addr = req.node_address.addr; - let res = connect_to_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()).await?; - - // If a node that we have an open channel with changed it's address, "connect_to_lightning_node" - // can be used to reconnect to the new address while saving this new address for reconnections. - if let ConnectToNodeRes::ConnectedSuccessfully { .. } = res { - if let Entry::Occupied(mut entry) = ln_coin.open_channels_nodes.lock().entry(node_pubkey) { - entry.insert(node_addr); - } - ln_coin - .persister - .save_nodes_addresses(ln_coin.open_channels_nodes) - .await?; - } - - Ok(res.to_string()) -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(tag = "type", content = "value")] -pub enum ChannelOpenAmount { - Exact(BigDecimal), - Max, -} - -#[derive(Deserialize)] -pub struct OpenChannelRequest { - pub coin: String, - pub node_address: NodeAddress, - pub amount: ChannelOpenAmount, - /// The amount to push to the counterparty as part of the open, in milli-satoshi. Creates inbound liquidity for the channel. - /// By setting push_msat to a value, opening channel request will be equivalent to opening a channel then sending a payment with - /// the push_msat amount. - #[serde(default)] - pub push_msat: u64, - pub channel_options: Option, - pub channel_configs: Option, -} - -#[derive(Serialize)] -pub struct OpenChannelResponse { - rpc_channel_id: u64, - node_address: NodeAddress, -} - -/// Opens a channel on the lightning network. -pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(OpenChannelError::UnsupportedCoin(e.ticker().to_string())), - }; - - // Making sure that the node data is correct and that we can connect to it before doing more operations - let node_pubkey = req.node_address.pubkey; - let node_addr = req.node_address.addr; - connect_to_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()).await?; - - let platform_coin = ln_coin.platform_coin().clone(); - let decimals = platform_coin.as_ref().decimals; - let my_address = platform_coin.as_ref().derivation_method.iguana_or_err()?; - let (unspents, _) = platform_coin.get_unspent_ordered_list(my_address).await?; - let (value, fee_policy) = match req.amount.clone() { - ChannelOpenAmount::Max => ( - unspents.iter().fold(0, |sum, unspent| sum + unspent.value), - FeePolicy::DeductFromOutput(0), - ), - ChannelOpenAmount::Exact(v) => { - let value = sat_from_big_decimal(&v, decimals)?; - (value, FeePolicy::SendExact) - }, - }; - - // The actual script_pubkey will replace this before signing the transaction after receiving the required - // output script from the other node when the channel is accepted - let script_pubkey = - Builder::build_witness_script(&AddressHashEnum::WitnessScriptHash(Default::default())).to_bytes(); - let outputs = vec![TransactionOutput { value, script_pubkey }]; - - let mut tx_builder = UtxoTxBuilder::new(&platform_coin) - .add_available_inputs(unspents) - .add_outputs(outputs) - .with_fee_policy(fee_policy); - - let fee = platform_coin - .get_tx_fee() - .await - .map_err(|e| OpenChannelError::RpcError(e.to_string()))?; - tx_builder = tx_builder.with_fee(fee); - - let (unsigned, _) = tx_builder.build().await?; - - let amount_in_sat = unsigned.outputs[0].value; - let push_msat = req.push_msat; - let channel_manager = ln_coin.channel_manager.clone(); - - let mut conf = ln_coin.conf.clone(); - if let Some(options) = req.channel_options { - match conf.channel_options.as_mut() { - Some(o) => o.update_according_to(options), - None => conf.channel_options = Some(options), - } - } - if let Some(configs) = req.channel_configs { - match conf.our_channels_configs.as_mut() { - Some(o) => o.update_according_to(configs), - None => conf.our_channels_configs = Some(configs), - } - } - drop_mutability!(conf); - let user_config: UserConfig = conf.into(); - - let rpc_channel_id = ln_coin.db.get_last_channel_rpc_id().await? as u64 + 1; - - let temp_channel_id = async_blocking(move || { - channel_manager - .create_channel(node_pubkey, amount_in_sat, push_msat, rpc_channel_id, Some(user_config)) - .map_to_mm(|e| OpenChannelError::FailureToOpenChannel(node_pubkey.to_string(), format!("{:?}", e))) - }) - .await?; - - { - let mut unsigned_funding_txs = ln_coin.platform.unsigned_funding_txs.lock(); - unsigned_funding_txs.insert(rpc_channel_id, unsigned); - } - - let pending_channel_details = DBChannelDetails::new( - rpc_channel_id, - temp_channel_id, - node_pubkey, - true, - user_config.channel_handshake_config.announced_channel, - ); - - // Saving node data to reconnect to it on restart - ln_coin.open_channels_nodes.lock().insert(node_pubkey, node_addr); - ln_coin - .persister - .save_nodes_addresses(ln_coin.open_channels_nodes) - .await?; - - if let Err(e) = ln_coin.db.add_channel_to_db(pending_channel_details).await { - error!("Unable to add new outbound channel {} to db: {}", rpc_channel_id, e); - } - - Ok(OpenChannelResponse { - rpc_channel_id, - node_address: req.node_address, - }) -} - -#[derive(Deserialize)] -pub struct UpdateChannelReq { - pub coin: String, - pub rpc_channel_id: u64, - pub channel_options: ChannelOptions, -} - -#[derive(Serialize)] -pub struct UpdateChannelResponse { - channel_options: ChannelOptions, -} - -/// Updates configuration for an open channel. -pub async fn update_channel(ctx: MmArc, req: UpdateChannelReq) -> UpdateChannelResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(UpdateChannelError::UnsupportedCoin(e.ticker().to_string())), - }; - - let channel_details = ln_coin - .get_channel_by_rpc_id(req.rpc_channel_id) - .await - .ok_or(UpdateChannelError::NoSuchChannel(req.rpc_channel_id))?; - - async_blocking(move || { - let mut channel_options = ln_coin - .conf - .channel_options - .unwrap_or_else(|| req.channel_options.clone()); - if channel_options != req.channel_options { - channel_options.update_according_to(req.channel_options.clone()); - } - drop_mutability!(channel_options); - let channel_ids = &[channel_details.channel_id]; - let counterparty_node_id = channel_details.counterparty.node_id; - ln_coin - .channel_manager - .update_channel_config(&counterparty_node_id, channel_ids, &channel_options.clone().into()) - .map_to_mm(|e| UpdateChannelError::FailureToUpdateChannel(req.rpc_channel_id, format!("{:?}", e)))?; - Ok(UpdateChannelResponse { channel_options }) - }) - .await -} - -#[derive(Deserialize)] -pub struct OpenChannelsFilter { - pub channel_id: Option, - pub counterparty_node_id: Option, - pub funding_tx: Option, - pub from_funding_value_sats: Option, - pub to_funding_value_sats: Option, - pub is_outbound: Option, - pub from_balance_msat: Option, - pub to_balance_msat: Option, - pub from_outbound_capacity_msat: Option, - pub to_outbound_capacity_msat: Option, - pub from_inbound_capacity_msat: Option, - pub to_inbound_capacity_msat: Option, - pub is_ready: Option, - pub is_usable: Option, - pub is_public: Option, -} - -fn apply_open_channel_filter(channel_details: &ChannelDetailsForRPC, filter: &OpenChannelsFilter) -> bool { - let is_channel_id = filter.channel_id.is_none() || Some(&channel_details.channel_id) == filter.channel_id.as_ref(); - - let is_counterparty_node_id = filter.counterparty_node_id.is_none() - || Some(&channel_details.counterparty_node_id) == filter.counterparty_node_id.as_ref(); - - let is_funding_tx = filter.funding_tx.is_none() || channel_details.funding_tx == filter.funding_tx; - - let is_from_funding_value_sats = - Some(&channel_details.funding_tx_value_sats) >= filter.from_funding_value_sats.as_ref(); - - let is_to_funding_value_sats = filter.to_funding_value_sats.is_none() - || Some(&channel_details.funding_tx_value_sats) <= filter.to_funding_value_sats.as_ref(); - - let is_outbound = filter.is_outbound.is_none() || Some(&channel_details.is_outbound) == filter.is_outbound.as_ref(); - - let is_from_balance_msat = Some(&channel_details.balance_msat) >= filter.from_balance_msat.as_ref(); - - let is_to_balance_msat = - filter.to_balance_msat.is_none() || Some(&channel_details.balance_msat) <= filter.to_balance_msat.as_ref(); - - let is_from_outbound_capacity_msat = - Some(&channel_details.outbound_capacity_msat) >= filter.from_outbound_capacity_msat.as_ref(); - - let is_to_outbound_capacity_msat = filter.to_outbound_capacity_msat.is_none() - || Some(&channel_details.outbound_capacity_msat) <= filter.to_outbound_capacity_msat.as_ref(); - - let is_from_inbound_capacity_msat = - Some(&channel_details.inbound_capacity_msat) >= filter.from_inbound_capacity_msat.as_ref(); - - let is_to_inbound_capacity_msat = filter.to_inbound_capacity_msat.is_none() - || Some(&channel_details.inbound_capacity_msat) <= filter.to_inbound_capacity_msat.as_ref(); - - let is_confirmed = filter.is_ready.is_none() || Some(&channel_details.is_ready) == filter.is_ready.as_ref(); - - let is_usable = filter.is_usable.is_none() || Some(&channel_details.is_usable) == filter.is_usable.as_ref(); - - let is_public = filter.is_public.is_none() || Some(&channel_details.is_public) == filter.is_public.as_ref(); - - is_channel_id - && is_counterparty_node_id - && is_funding_tx - && is_from_funding_value_sats - && is_to_funding_value_sats - && is_outbound - && is_from_balance_msat - && is_to_balance_msat - && is_from_outbound_capacity_msat - && is_to_outbound_capacity_msat - && is_from_inbound_capacity_msat - && is_to_inbound_capacity_msat - && is_confirmed - && is_usable - && is_public -} - -#[derive(Deserialize)] -pub struct ListOpenChannelsRequest { - pub coin: String, - pub filter: Option, - #[serde(default = "ten")] - limit: usize, - #[serde(default)] - paging_options: PagingOptionsEnum, -} - -#[derive(Clone, Serialize)] -pub struct ChannelDetailsForRPC { - pub rpc_channel_id: u64, - pub channel_id: H256Json, - pub counterparty_node_id: PublicKeyForRPC, - pub funding_tx: Option, - pub funding_tx_output_index: Option, - pub funding_tx_value_sats: u64, - /// True if the channel was initiated (and thus funded) by us. - pub is_outbound: bool, - pub balance_msat: u64, - pub outbound_capacity_msat: u64, - pub inbound_capacity_msat: u64, - // Channel is confirmed onchain, this means that funding_locked messages have been exchanged, - // the channel is not currently being shut down, and the required confirmation count has been reached. - pub is_ready: bool, - // Channel is confirmed and channel_ready messages have been exchanged, the peer is connected, - // and the channel is not currently negotiating a shutdown. - pub is_usable: bool, - // A publicly-announced channel. - pub is_public: bool, -} - -impl From for ChannelDetailsForRPC { - fn from(details: ChannelDetails) -> ChannelDetailsForRPC { - ChannelDetailsForRPC { - rpc_channel_id: details.user_channel_id, - channel_id: details.channel_id.into(), - counterparty_node_id: PublicKeyForRPC(details.counterparty.node_id), - funding_tx: details.funding_txo.map(|tx| h256_json_from_txid(tx.txid)), - funding_tx_output_index: details.funding_txo.map(|tx| tx.index), - funding_tx_value_sats: details.channel_value_satoshis, - is_outbound: details.is_outbound, - balance_msat: details.balance_msat, - outbound_capacity_msat: details.outbound_capacity_msat, - inbound_capacity_msat: details.inbound_capacity_msat, - is_ready: details.is_channel_ready, - is_usable: details.is_usable, - is_public: details.is_public, - } - } -} - -struct GetOpenChannelsResult { - pub channels: Vec, - pub skipped: usize, - pub total: usize, -} - -#[derive(Serialize)] -pub struct ListOpenChannelsResponse { - open_channels: Vec, - limit: usize, - skipped: usize, - total: usize, - total_pages: usize, - paging_options: PagingOptionsEnum, -} - -pub async fn list_open_channels_by_filter( - ctx: MmArc, - req: ListOpenChannelsRequest, -) -> ListChannelsResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(ListChannelsError::UnsupportedCoin(e.ticker().to_string())), - }; - - let result = ln_coin - .get_open_channels_by_filter(req.filter, req.paging_options.clone(), req.limit) - .await?; - - Ok(ListOpenChannelsResponse { - open_channels: result.channels, - limit: req.limit, - skipped: result.skipped, - total: result.total, - total_pages: calc_total_pages(result.total, req.limit), - paging_options: req.paging_options, - }) -} - -#[derive(Deserialize)] -pub struct ListClosedChannelsRequest { - pub coin: String, - pub filter: Option, - #[serde(default = "ten")] - limit: usize, - #[serde(default)] - paging_options: PagingOptionsEnum, -} - -#[derive(Serialize)] -pub struct ListClosedChannelsResponse { - closed_channels: Vec, - limit: usize, - skipped: usize, - total: usize, - total_pages: usize, - paging_options: PagingOptionsEnum, -} - -pub async fn list_closed_channels_by_filter( - ctx: MmArc, - req: ListClosedChannelsRequest, -) -> ListChannelsResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(ListChannelsError::UnsupportedCoin(e.ticker().to_string())), - }; - let closed_channels_res = ln_coin - .db - .get_closed_channels_by_filter(req.filter, req.paging_options.clone(), req.limit) - .await?; - - Ok(ListClosedChannelsResponse { - closed_channels: closed_channels_res.channels, - limit: req.limit, - skipped: closed_channels_res.skipped, - total: closed_channels_res.total, - total_pages: calc_total_pages(closed_channels_res.total, req.limit), - paging_options: req.paging_options, - }) -} - -#[derive(Deserialize)] -pub struct GetChannelDetailsRequest { - pub coin: String, - pub rpc_channel_id: u64, -} - -#[derive(Serialize)] -#[serde(tag = "status", content = "details")] -pub enum GetChannelDetailsResponse { - Open(ChannelDetailsForRPC), - Closed(DBChannelDetails), -} - -pub async fn get_channel_details( - ctx: MmArc, - req: GetChannelDetailsRequest, -) -> GetChannelDetailsResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(GetChannelDetailsError::UnsupportedCoin(e.ticker().to_string())), - }; - - let channel_details = match ln_coin.get_channel_by_rpc_id(req.rpc_channel_id).await { - Some(details) => GetChannelDetailsResponse::Open(details.into()), - None => GetChannelDetailsResponse::Closed( - ln_coin - .db - .get_channel_from_db(req.rpc_channel_id) - .await? - .ok_or(GetChannelDetailsError::NoSuchChannel(req.rpc_channel_id))?, - ), - }; - - Ok(channel_details) -} - -#[derive(Deserialize)] -pub struct GenerateInvoiceRequest { - pub coin: String, - pub amount_in_msat: Option, - pub description: String, - pub expiry: Option, -} - -#[derive(Serialize)] -pub struct GenerateInvoiceResponse { - payment_hash: H256Json, - invoice: Invoice, -} - -/// Generates an invoice (request for payment) that can be paid on the lightning network by another node using send_payment. -pub async fn generate_invoice( - ctx: MmArc, - req: GenerateInvoiceRequest, -) -> GenerateInvoiceResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(GenerateInvoiceError::UnsupportedCoin(e.ticker().to_string())), - }; - let open_channels_nodes = ln_coin.open_channels_nodes.lock().clone(); - for (node_pubkey, node_addr) in open_channels_nodes { - connect_to_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()) - .await - .error_log_with_msg(&format!( - "Channel with node: {} can't be used for invoice routing hints due to connection error.", - node_pubkey - )); - } - - let network = ln_coin.platform.network.clone().into(); - let channel_manager = ln_coin.channel_manager.clone(); - let keys_manager = ln_coin.keys_manager.clone(); - let amount_in_msat = req.amount_in_msat; - let description = req.description.clone(); - let expiry = req.expiry.unwrap_or(DEFAULT_INVOICE_EXPIRY); - let invoice = async_blocking(move || { - create_invoice_from_channelmanager( - &channel_manager, - keys_manager, - network, - amount_in_msat, - description, - expiry, - ) - }) - .await?; - - let payment_hash = invoice.payment_hash().into_inner(); - let payment_info = DBPaymentInfo { - payment_hash: PaymentHash(payment_hash), - payment_type: PaymentType::InboundPayment, - description: req.description, - preimage: None, - secret: Some(*invoice.payment_secret()), - amt_msat: req.amount_in_msat.map(|a| a as i64), - fee_paid_msat: None, - status: HTLCStatus::Pending, - created_at: (now_ms() / 1000) as i64, - last_updated: (now_ms() / 1000) as i64, - }; - ln_coin.db.add_or_update_payment_in_db(payment_info).await?; - Ok(GenerateInvoiceResponse { - payment_hash: payment_hash.into(), - invoice, - }) -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(tag = "type")] -pub enum Payment { - #[serde(rename = "invoice")] - Invoice { invoice: Invoice }, - #[serde(rename = "keysend")] - Keysend { - // The recieving node pubkey (node ID) - destination: PublicKeyForRPC, - // Amount to send in millisatoshis - amount_in_msat: u64, - // The number of blocks the payment will be locked for if not claimed by the destination, - // It's can be assumed that 6 blocks = 1 hour. We can claim the payment amount back after this cltv expires. - // Minmum value allowed is MIN_FINAL_CLTV_EXPIRY which is currently 24 for rust-lightning. - expiry: u32, - }, -} - -#[derive(Deserialize)] -pub struct SendPaymentReq { - pub coin: String, - pub payment: Payment, -} - -#[derive(Serialize)] -pub struct SendPaymentResponse { - payment_hash: H256Json, -} - -pub async fn send_payment(ctx: MmArc, req: SendPaymentReq) -> SendPaymentResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(SendPaymentError::UnsupportedCoin(e.ticker().to_string())), - }; - let open_channels_nodes = ln_coin.open_channels_nodes.lock().clone(); - for (node_pubkey, node_addr) in open_channels_nodes { - connect_to_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()) - .await - .error_log_with_msg(&format!( - "Channel with node: {} can't be used to route this payment due to connection error.", - node_pubkey - )); - } - let payment_info = match req.payment { - Payment::Invoice { invoice } => ln_coin.pay_invoice(invoice).await?, - Payment::Keysend { - destination, - amount_in_msat, - expiry, - } => ln_coin.keysend(destination.into(), amount_in_msat, expiry).await?, - }; - ln_coin.db.add_or_update_payment_in_db(payment_info.clone()).await?; - Ok(SendPaymentResponse { - payment_hash: payment_info.payment_hash.0.into(), - }) -} - -#[derive(Deserialize)] -pub struct PaymentsFilterForRPC { - pub payment_type: Option, - pub description: Option, - pub status: Option, - pub from_amount_msat: Option, - pub to_amount_msat: Option, - pub from_fee_paid_msat: Option, - pub to_fee_paid_msat: Option, - pub from_timestamp: Option, - pub to_timestamp: Option, -} - -impl From for DBPaymentsFilter { - fn from(filter: PaymentsFilterForRPC) -> Self { - let (is_outbound, destination) = if let Some(payment_type) = filter.payment_type { - match payment_type { - PaymentTypeForRPC::OutboundPayment { destination } => (Some(true), Some(destination.0.to_string())), - PaymentTypeForRPC::InboundPayment => (Some(false), None), - } - } else { - (None, None) - }; - DBPaymentsFilter { - is_outbound, - destination, - description: filter.description, - status: filter.status.map(|s| s.to_string()), - from_amount_msat: filter.from_amount_msat.map(|a| a as i64), - to_amount_msat: filter.to_amount_msat.map(|a| a as i64), - from_fee_paid_msat: filter.from_fee_paid_msat.map(|f| f as i64), - to_fee_paid_msat: filter.to_fee_paid_msat.map(|f| f as i64), - from_timestamp: filter.from_timestamp.map(|f| f as i64), - to_timestamp: filter.to_timestamp.map(|f| f as i64), - } - } -} - -#[derive(Deserialize)] -pub struct ListPaymentsReq { - pub coin: String, - pub filter: Option, - #[serde(default = "ten")] - limit: usize, - #[serde(default)] - paging_options: PagingOptionsEnum, -} - -#[derive(Deserialize, Serialize)] -#[serde(tag = "type")] -pub enum PaymentTypeForRPC { - #[serde(rename = "Outbound Payment")] - OutboundPayment { destination: PublicKeyForRPC }, - #[serde(rename = "Inbound Payment")] - InboundPayment, -} - -impl From for PaymentTypeForRPC { - fn from(payment_type: PaymentType) -> Self { - match payment_type { - PaymentType::OutboundPayment { destination } => PaymentTypeForRPC::OutboundPayment { - destination: PublicKeyForRPC(destination), - }, - PaymentType::InboundPayment => PaymentTypeForRPC::InboundPayment, - } - } -} - -impl From for PaymentType { - fn from(payment_type: PaymentTypeForRPC) -> Self { - match payment_type { - PaymentTypeForRPC::OutboundPayment { destination } => PaymentType::OutboundPayment { - destination: destination.into(), - }, - PaymentTypeForRPC::InboundPayment => PaymentType::InboundPayment, - } - } -} - -#[derive(Serialize)] -pub struct PaymentInfoForRPC { - payment_hash: H256Json, - payment_type: PaymentTypeForRPC, - description: String, - #[serde(skip_serializing_if = "Option::is_none")] - amount_in_msat: Option, - #[serde(skip_serializing_if = "Option::is_none")] - fee_paid_msat: Option, - status: HTLCStatus, - created_at: i64, - last_updated: i64, -} - -impl From for PaymentInfoForRPC { - fn from(info: DBPaymentInfo) -> Self { - PaymentInfoForRPC { - payment_hash: info.payment_hash.0.into(), - payment_type: info.payment_type.into(), - description: info.description, - amount_in_msat: info.amt_msat, - fee_paid_msat: info.fee_paid_msat, - status: info.status, - created_at: info.created_at, - last_updated: info.last_updated, - } - } -} - -#[derive(Serialize)] -pub struct ListPaymentsResponse { - payments: Vec, - limit: usize, - skipped: usize, - total: usize, - total_pages: usize, - paging_options: PagingOptionsEnum, -} - -pub async fn list_payments_by_filter(ctx: MmArc, req: ListPaymentsReq) -> ListPaymentsResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(ListPaymentsError::UnsupportedCoin(e.ticker().to_string())), - }; - let get_payments_res = ln_coin - .db - .get_payments_by_filter( - req.filter.map(From::from), - req.paging_options.clone().map(|h| PaymentHash(h.0)), - req.limit, - ) - .await?; - - Ok(ListPaymentsResponse { - payments: get_payments_res.payments.into_iter().map(From::from).collect(), - limit: req.limit, - skipped: get_payments_res.skipped, - total: get_payments_res.total, - total_pages: calc_total_pages(get_payments_res.total, req.limit), - paging_options: req.paging_options, - }) -} - -#[derive(Deserialize)] -pub struct GetPaymentDetailsRequest { - pub coin: String, - pub payment_hash: H256Json, -} - -#[derive(Serialize)] -pub struct GetPaymentDetailsResponse { - payment_details: PaymentInfoForRPC, -} - -pub async fn get_payment_details( - ctx: MmArc, - req: GetPaymentDetailsRequest, -) -> GetPaymentDetailsResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(GetPaymentDetailsError::UnsupportedCoin(e.ticker().to_string())), - }; - - if let Some(payment_info) = ln_coin.db.get_payment_from_db(PaymentHash(req.payment_hash.0)).await? { - return Ok(GetPaymentDetailsResponse { - payment_details: payment_info.into(), - }); - } - - MmError::err(GetPaymentDetailsError::NoSuchPayment(req.payment_hash)) -} - -#[derive(Deserialize)] -pub struct CloseChannelReq { - pub coin: String, - pub rpc_channel_id: u64, - #[serde(default)] - pub force_close: bool, -} - -pub async fn close_channel(ctx: MmArc, req: CloseChannelReq) -> CloseChannelResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(CloseChannelError::UnsupportedCoin(e.ticker().to_string())), - }; - - let channel_details = ln_coin - .get_channel_by_rpc_id(req.rpc_channel_id) - .await - .ok_or(CloseChannelError::NoSuchChannel(req.rpc_channel_id))?; - let channel_id = channel_details.channel_id; - let counterparty_node_id = channel_details.counterparty.node_id; - - if req.force_close { - async_blocking(move || { - ln_coin - .channel_manager - .force_close_broadcasting_latest_txn(&channel_id, &counterparty_node_id) - .map_to_mm(|e| CloseChannelError::CloseChannelError(format!("{:?}", e))) - }) - .await?; - } else { - async_blocking(move || { - ln_coin - .channel_manager - .close_channel(&channel_id, &counterparty_node_id) - .map_to_mm(|e| CloseChannelError::CloseChannelError(format!("{:?}", e))) - }) - .await?; - } - - Ok(format!( - "Initiated closing of channel with rpc_channel_id: {}", - req.rpc_channel_id - )) -} - -/// Details about the balance(s) available for spending once the channel appears on chain. -#[derive(Serialize)] -pub enum ClaimableBalance { - /// The channel is not yet closed (or the commitment or closing transaction has not yet - /// appeared in a block). The given balance is claimable (less on-chain fees) if the channel is - /// force-closed now. - ClaimableOnChannelClose { - /// The amount available to claim, in satoshis, excluding the on-chain fees which will be - /// required to do so. - claimable_amount_satoshis: u64, - }, - /// The channel has been closed, and the given balance is ours but awaiting confirmations until - /// we consider it spendable. - ClaimableAwaitingConfirmations { - /// The amount available to claim, in satoshis, possibly excluding the on-chain fees which - /// were spent in broadcasting the transaction. - claimable_amount_satoshis: u64, - /// The height at which an [`Event::SpendableOutputs`] event will be generated for this - /// amount. - confirmation_height: u32, - }, - /// The channel has been closed, and the given balance should be ours but awaiting spending - /// transaction confirmation. If the spending transaction does not confirm in time, it is - /// possible our counterparty can take the funds by broadcasting an HTLC timeout on-chain. - /// - /// Once the spending transaction confirms, before it has reached enough confirmations to be - /// considered safe from chain reorganizations, the balance will instead be provided via - /// [`Balance::ClaimableAwaitingConfirmations`]. - ContentiousClaimable { - /// The amount available to claim, in satoshis, excluding the on-chain fees which will be - /// required to do so. - claimable_amount_satoshis: u64, - /// The height at which the counterparty may be able to claim the balance if we have not - /// done so. - timeout_height: u32, - }, - /// HTLCs which we sent to our counterparty which are claimable after a timeout (less on-chain - /// fees) if the counterparty does not know the preimage for the HTLCs. These are somewhat - /// likely to be claimed by our counterparty before we do. - MaybeClaimableHTLCAwaitingTimeout { - /// The amount available to claim, in satoshis, excluding the on-chain fees which will be - /// required to do so. - claimable_amount_satoshis: u64, - /// The height at which we will be able to claim the balance if our counterparty has not - /// done so. - claimable_height: u32, - }, -} - -impl From for ClaimableBalance { - fn from(balance: Balance) -> Self { - match balance { - Balance::ClaimableOnChannelClose { - claimable_amount_satoshis, - } => ClaimableBalance::ClaimableOnChannelClose { - claimable_amount_satoshis, - }, - Balance::ClaimableAwaitingConfirmations { - claimable_amount_satoshis, - confirmation_height, - } => ClaimableBalance::ClaimableAwaitingConfirmations { - claimable_amount_satoshis, - confirmation_height, - }, - Balance::ContentiousClaimable { - claimable_amount_satoshis, - timeout_height, - } => ClaimableBalance::ContentiousClaimable { - claimable_amount_satoshis, - timeout_height, - }, - Balance::MaybeClaimableHTLCAwaitingTimeout { - claimable_amount_satoshis, - claimable_height, - } => ClaimableBalance::MaybeClaimableHTLCAwaitingTimeout { - claimable_amount_satoshis, - claimable_height, - }, - } - } -} - -#[derive(Deserialize)] -pub struct ClaimableBalancesReq { - pub coin: String, - #[serde(default)] - pub include_open_channels_balances: bool, -} - -pub async fn get_claimable_balances( - ctx: MmArc, - req: ClaimableBalancesReq, -) -> ClaimableBalancesResult> { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(ClaimableBalancesError::UnsupportedCoin(e.ticker().to_string())), - }; - let ignored_channels = if req.include_open_channels_balances { - Vec::new() - } else { - ln_coin.list_channels().await - }; - let claimable_balances = async_blocking(move || { - ln_coin - .chain_monitor - .get_claimable_balances(&ignored_channels.iter().collect::>()[..]) - .into_iter() - .map(From::from) - .collect() - }) - .await; - - Ok(claimable_balances) -} - -#[derive(Deserialize)] -pub struct AddTrustedNodeReq { - pub coin: String, - pub node_id: PublicKeyForRPC, -} - -#[derive(Serialize)] -pub struct AddTrustedNodeResponse { - pub added_node: PublicKeyForRPC, -} - -pub async fn add_trusted_node(ctx: MmArc, req: AddTrustedNodeReq) -> TrustedNodeResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), - }; - - if ln_coin.trusted_nodes.lock().insert(req.node_id.clone().into()) { - ln_coin.persister.save_trusted_nodes(ln_coin.trusted_nodes).await?; - } - - Ok(AddTrustedNodeResponse { - added_node: req.node_id, - }) -} - -#[derive(Deserialize)] -pub struct RemoveTrustedNodeReq { - pub coin: String, - pub node_id: PublicKeyForRPC, -} - -#[derive(Serialize)] -pub struct RemoveTrustedNodeResponse { - pub removed_node: PublicKeyForRPC, -} - -pub async fn remove_trusted_node( - ctx: MmArc, - req: RemoveTrustedNodeReq, -) -> TrustedNodeResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), - }; - - if ln_coin.trusted_nodes.lock().remove(&req.node_id.clone().into()) { - ln_coin.persister.save_trusted_nodes(ln_coin.trusted_nodes).await?; - } - - Ok(RemoveTrustedNodeResponse { - removed_node: req.node_id, - }) -} - -#[derive(Deserialize)] -pub struct ListTrustedNodesReq { - pub coin: String, -} - -#[derive(Serialize)] -pub struct ListTrustedNodesResponse { - trusted_nodes: Vec, -} - -pub async fn list_trusted_nodes(ctx: MmArc, req: ListTrustedNodesReq) -> TrustedNodeResult { - let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::LightningCoin(c) => c, - e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), - }; - - let trusted_nodes = ln_coin.trusted_nodes.lock().clone(); - - Ok(ListTrustedNodesResponse { - trusted_nodes: trusted_nodes.into_iter().map(PublicKeyForRPC).collect(), - }) -} diff --git a/mm2src/coins/lightning/ln_db.rs b/mm2src/coins/lightning/ln_db.rs index 248c164b06..722e6ab2ac 100644 --- a/mm2src/coins/lightning/ln_db.rs +++ b/mm2src/coins/lightning/ln_db.rs @@ -101,6 +101,7 @@ pub struct GetClosedChannelsResult { #[serde(rename_all = "lowercase")] pub enum HTLCStatus { Pending, + Received, Succeeded, Failed, } @@ -111,6 +112,7 @@ impl FromStr for HTLCStatus { fn from_str(s: &str) -> Result { match s { "Pending" => Ok(HTLCStatus::Pending), + "Received" => Ok(HTLCStatus::Received), "Succeeded" => Ok(HTLCStatus::Succeeded), "Failed" => Ok(HTLCStatus::Failed), _ => Err(FromSqlError::InvalidType), @@ -125,7 +127,7 @@ pub enum PaymentType { } #[derive(Clone, Debug, PartialEq)] -pub struct DBPaymentInfo { +pub struct PaymentInfo { pub payment_hash: PaymentHash, pub payment_type: PaymentType, pub description: String, @@ -153,7 +155,7 @@ pub struct DBPaymentsFilter { } pub struct GetPaymentsResult { - pub payments: Vec, + pub payments: Vec, pub skipped: usize, pub total: usize, } @@ -228,10 +230,17 @@ pub trait LightningDB { ) -> Result; /// Inserts or updates a new payment record in the DB. - async fn add_or_update_payment_in_db(&self, info: DBPaymentInfo) -> Result<(), Self::Error>; + async fn add_or_update_payment_in_db(&self, info: PaymentInfo) -> Result<(), Self::Error>; + + /// Updates a payment's preimage in DB by the payment's hash. + async fn update_payment_preimage_in_db( + &self, + hash: PaymentHash, + preimage: PaymentPreimage, + ) -> Result<(), Self::Error>; /// Gets a payment's record from DB by the payment's hash. - async fn get_payment_from_db(&self, hash: PaymentHash) -> Result, Self::Error>; + async fn get_payment_from_db(&self, hash: PaymentHash) -> Result, Self::Error>; /// Gets the list of payments that match the provided filter criteria. The number of requested records is specified /// by the limit parameter, the starting record to list from is specified by the paging parameter. The total number diff --git a/mm2src/coins/lightning/ln_errors.rs b/mm2src/coins/lightning/ln_errors.rs index c76102cddc..de92a5dd81 100644 --- a/mm2src/coins/lightning/ln_errors.rs +++ b/mm2src/coins/lightning/ln_errors.rs @@ -1,30 +1,13 @@ use crate::utxo::rpc_clients::UtxoRpcError; -use crate::utxo::GenerateTxError; -use crate::{BalanceError, CoinFindError, NumConversError, PrivKeyNotAllowed, UnexpectedDerivationMethod}; use common::HttpStatusCode; use db_common::sqlite::rusqlite::Error as SqlError; use derive_more::Display; use http::StatusCode; -use lightning_invoice::SignOrCreationError; use mm2_err_handle::prelude::*; -use rpc::v1::types::H256 as H256Json; use std::num::TryFromIntError; -use utxo_signer::with_key_pair::UtxoSignWithKeyPairError; pub type EnableLightningResult = Result>; -pub type ConnectToNodeResult = Result>; -pub type OpenChannelResult = Result>; -pub type UpdateChannelResult = Result>; -pub type ListChannelsResult = Result>; -pub type GetChannelDetailsResult = Result>; -pub type GenerateInvoiceResult = Result>; -pub type SendPaymentResult = Result>; -pub type ListPaymentsResult = Result>; -pub type GetPaymentDetailsResult = Result>; -pub type CloseChannelResult = Result>; -pub type ClaimableBalancesResult = Result>; pub type SaveChannelClosingResult = Result>; -pub type TrustedNodeResult = Result>; #[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] @@ -78,457 +61,6 @@ impl From for EnableLightningError { fn from(e: UtxoRpcError) -> Self { EnableLightningError::RpcError(e.to_string()) } } -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum ConnectToNodeError { - #[display(fmt = "Parse error: {}", _0)] - ParseError(String), - #[display(fmt = "Error connecting to node: {}", _0)] - ConnectionError(String), - #[display(fmt = "I/O error {}", _0)] - IOError(String), - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), -} - -impl HttpStatusCode for ConnectToNodeError { - fn status_code(&self) -> StatusCode { - match self { - ConnectToNodeError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, - ConnectToNodeError::ParseError(_) - | ConnectToNodeError::IOError(_) - | ConnectToNodeError::ConnectionError(_) => StatusCode::INTERNAL_SERVER_ERROR, - ConnectToNodeError::NoSuchCoin(_) => StatusCode::NOT_FOUND, - } - } -} - -impl From for EnableLightningError { - fn from(err: ConnectToNodeError) -> EnableLightningError { - EnableLightningError::ConnectToNodeError(err.to_string()) - } -} - -impl From for ConnectToNodeError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => ConnectToNodeError::NoSuchCoin(coin), - } - } -} - -impl From for ConnectToNodeError { - fn from(err: std::io::Error) -> ConnectToNodeError { ConnectToNodeError::IOError(err.to_string()) } -} - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum OpenChannelError { - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "Balance Error {}", _0)] - BalanceError(String), - #[display(fmt = "Invalid path: {}", _0)] - InvalidPath(String), - #[display(fmt = "Failure to open channel with node {}: {}", _0, _1)] - FailureToOpenChannel(String, String), - #[display(fmt = "RPC error {}", _0)] - RpcError(String), - #[display(fmt = "Internal error: {}", _0)] - InternalError(String), - #[display(fmt = "I/O error {}", _0)] - IOError(String), - #[display(fmt = "DB error {}", _0)] - DbError(String), - ConnectToNodeError(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), - #[display(fmt = "Generate Tx Error {}", _0)] - GenerateTxErr(String), - #[display(fmt = "Error converting transaction: {}", _0)] - ConvertTxErr(String), - PrivKeyNotAllowed(String), -} - -impl HttpStatusCode for OpenChannelError { - fn status_code(&self) -> StatusCode { - match self { - OpenChannelError::UnsupportedCoin(_) - | OpenChannelError::RpcError(_) - | OpenChannelError::PrivKeyNotAllowed(_) => StatusCode::BAD_REQUEST, - OpenChannelError::FailureToOpenChannel(_, _) - | OpenChannelError::ConnectToNodeError(_) - | OpenChannelError::InternalError(_) - | OpenChannelError::GenerateTxErr(_) - | OpenChannelError::IOError(_) - | OpenChannelError::DbError(_) - | OpenChannelError::InvalidPath(_) - | OpenChannelError::ConvertTxErr(_) - | OpenChannelError::BalanceError(_) => StatusCode::INTERNAL_SERVER_ERROR, - OpenChannelError::NoSuchCoin(_) => StatusCode::NOT_FOUND, - } - } -} - -impl From for OpenChannelError { - fn from(err: ConnectToNodeError) -> OpenChannelError { OpenChannelError::ConnectToNodeError(err.to_string()) } -} - -impl From for OpenChannelError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => OpenChannelError::NoSuchCoin(coin), - } - } -} - -impl From for OpenChannelError { - fn from(e: BalanceError) -> Self { OpenChannelError::BalanceError(e.to_string()) } -} - -impl From for OpenChannelError { - fn from(e: NumConversError) -> Self { OpenChannelError::InternalError(e.to_string()) } -} - -impl From for OpenChannelError { - fn from(e: GenerateTxError) -> Self { OpenChannelError::GenerateTxErr(e.to_string()) } -} - -impl From for OpenChannelError { - fn from(e: UtxoRpcError) -> Self { OpenChannelError::RpcError(e.to_string()) } -} - -impl From for OpenChannelError { - fn from(e: UnexpectedDerivationMethod) -> Self { OpenChannelError::InternalError(e.to_string()) } -} - -impl From for OpenChannelError { - fn from(e: UtxoSignWithKeyPairError) -> Self { OpenChannelError::InternalError(e.to_string()) } -} - -impl From for OpenChannelError { - fn from(e: PrivKeyNotAllowed) -> Self { OpenChannelError::PrivKeyNotAllowed(e.to_string()) } -} - -impl From for OpenChannelError { - fn from(err: std::io::Error) -> OpenChannelError { OpenChannelError::IOError(err.to_string()) } -} - -impl From for OpenChannelError { - fn from(err: SqlError) -> OpenChannelError { OpenChannelError::DbError(err.to_string()) } -} - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum UpdateChannelError { - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), - #[display(fmt = "No such channel with rpc_channel_id {}", _0)] - NoSuchChannel(u64), - #[display(fmt = "Failure to channel {}: {}", _0, _1)] - FailureToUpdateChannel(u64, String), -} - -impl HttpStatusCode for UpdateChannelError { - fn status_code(&self) -> StatusCode { - match self { - UpdateChannelError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, - UpdateChannelError::NoSuchChannel(_) => StatusCode::NOT_FOUND, - UpdateChannelError::NoSuchCoin(_) => StatusCode::NOT_FOUND, - UpdateChannelError::FailureToUpdateChannel(_, _) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for UpdateChannelError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => UpdateChannelError::NoSuchCoin(coin), - } - } -} - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum ListChannelsError { - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), - #[display(fmt = "DB error {}", _0)] - DbError(String), -} - -impl HttpStatusCode for ListChannelsError { - fn status_code(&self) -> StatusCode { - match self { - ListChannelsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, - ListChannelsError::NoSuchCoin(_) => StatusCode::NOT_FOUND, - ListChannelsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for ListChannelsError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => ListChannelsError::NoSuchCoin(coin), - } - } -} - -impl From for ListChannelsError { - fn from(err: SqlError) -> ListChannelsError { ListChannelsError::DbError(err.to_string()) } -} - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum GetChannelDetailsError { - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), - #[display(fmt = "Channel with rpc id: {} is not found", _0)] - NoSuchChannel(u64), - #[display(fmt = "DB error {}", _0)] - DbError(String), -} - -impl HttpStatusCode for GetChannelDetailsError { - fn status_code(&self) -> StatusCode { - match self { - GetChannelDetailsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, - GetChannelDetailsError::NoSuchCoin(_) | GetChannelDetailsError::NoSuchChannel(_) => StatusCode::NOT_FOUND, - GetChannelDetailsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for GetChannelDetailsError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => GetChannelDetailsError::NoSuchCoin(coin), - } - } -} - -impl From for GetChannelDetailsError { - fn from(err: SqlError) -> GetChannelDetailsError { GetChannelDetailsError::DbError(err.to_string()) } -} - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum GenerateInvoiceError { - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), - #[display(fmt = "Invoice signing or creation error: {}", _0)] - SignOrCreationError(String), - #[display(fmt = "DB error {}", _0)] - DbError(String), -} - -impl HttpStatusCode for GenerateInvoiceError { - fn status_code(&self) -> StatusCode { - match self { - GenerateInvoiceError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, - GenerateInvoiceError::NoSuchCoin(_) => StatusCode::NOT_FOUND, - GenerateInvoiceError::SignOrCreationError(_) | GenerateInvoiceError::DbError(_) => { - StatusCode::INTERNAL_SERVER_ERROR - }, - } - } -} - -impl From for GenerateInvoiceError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => GenerateInvoiceError::NoSuchCoin(coin), - } - } -} - -impl From for GenerateInvoiceError { - fn from(e: SignOrCreationError) -> Self { GenerateInvoiceError::SignOrCreationError(e.to_string()) } -} - -impl From for GenerateInvoiceError { - fn from(err: SqlError) -> GenerateInvoiceError { GenerateInvoiceError::DbError(err.to_string()) } -} - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum SendPaymentError { - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), - #[display(fmt = "Couldn't parse destination pubkey: {}", _0)] - NoRouteFound(String), - #[display(fmt = "Payment error: {}", _0)] - PaymentError(String), - #[display(fmt = "Final cltv expiry delta {} is below the required minimum of {}", _0, _1)] - CLTVExpiryError(u32, u32), - #[display(fmt = "DB error {}", _0)] - DbError(String), -} - -impl HttpStatusCode for SendPaymentError { - fn status_code(&self) -> StatusCode { - match self { - SendPaymentError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, - SendPaymentError::NoSuchCoin(_) => StatusCode::NOT_FOUND, - SendPaymentError::PaymentError(_) - | SendPaymentError::NoRouteFound(_) - | SendPaymentError::CLTVExpiryError(_, _) - | SendPaymentError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for SendPaymentError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => SendPaymentError::NoSuchCoin(coin), - } - } -} - -impl From for SendPaymentError { - fn from(err: SqlError) -> SendPaymentError { SendPaymentError::DbError(err.to_string()) } -} - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum ListPaymentsError { - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), - #[display(fmt = "DB error {}", _0)] - DbError(String), -} - -impl HttpStatusCode for ListPaymentsError { - fn status_code(&self) -> StatusCode { - match self { - ListPaymentsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, - ListPaymentsError::NoSuchCoin(_) => StatusCode::NOT_FOUND, - ListPaymentsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for ListPaymentsError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => ListPaymentsError::NoSuchCoin(coin), - } - } -} - -impl From for ListPaymentsError { - fn from(err: SqlError) -> ListPaymentsError { ListPaymentsError::DbError(err.to_string()) } -} - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum GetPaymentDetailsError { - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), - #[display(fmt = "Payment with hash: {:?} is not found", _0)] - NoSuchPayment(H256Json), - #[display(fmt = "DB error {}", _0)] - DbError(String), -} - -impl HttpStatusCode for GetPaymentDetailsError { - fn status_code(&self) -> StatusCode { - match self { - GetPaymentDetailsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, - GetPaymentDetailsError::NoSuchCoin(_) => StatusCode::NOT_FOUND, - GetPaymentDetailsError::NoSuchPayment(_) => StatusCode::NOT_FOUND, - GetPaymentDetailsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for GetPaymentDetailsError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => GetPaymentDetailsError::NoSuchCoin(coin), - } - } -} - -impl From for GetPaymentDetailsError { - fn from(err: SqlError) -> GetPaymentDetailsError { GetPaymentDetailsError::DbError(err.to_string()) } -} - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum CloseChannelError { - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), - #[display(fmt = "No such channel with rpc_channel_id {}", _0)] - NoSuchChannel(u64), - #[display(fmt = "Closing channel error: {}", _0)] - CloseChannelError(String), -} - -impl HttpStatusCode for CloseChannelError { - fn status_code(&self) -> StatusCode { - match self { - CloseChannelError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, - CloseChannelError::NoSuchChannel(_) | CloseChannelError::NoSuchCoin(_) => StatusCode::NOT_FOUND, - CloseChannelError::CloseChannelError(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for CloseChannelError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => CloseChannelError::NoSuchCoin(coin), - } - } -} - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum ClaimableBalancesError { - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), -} - -impl HttpStatusCode for ClaimableBalancesError { - fn status_code(&self) -> StatusCode { - match self { - ClaimableBalancesError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, - ClaimableBalancesError::NoSuchCoin(_) => StatusCode::NOT_FOUND, - } - } -} - -impl From for ClaimableBalancesError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => ClaimableBalancesError::NoSuchCoin(coin), - } - } -} - #[derive(Display, PartialEq)] pub enum SaveChannelClosingError { #[display(fmt = "DB error: {}", _0)] @@ -554,36 +86,3 @@ impl From for SaveChannelClosingError { impl From for SaveChannelClosingError { fn from(err: TryFromIntError) -> SaveChannelClosingError { SaveChannelClosingError::ConversionError(err) } } - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum TrustedNodeError { - #[display(fmt = "Lightning network is not supported for {}", _0)] - UnsupportedCoin(String), - #[display(fmt = "No such coin {}", _0)] - NoSuchCoin(String), - #[display(fmt = "I/O error {}", _0)] - IOError(String), -} - -impl HttpStatusCode for TrustedNodeError { - fn status_code(&self) -> StatusCode { - match self { - TrustedNodeError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, - TrustedNodeError::NoSuchCoin(_) => StatusCode::NOT_FOUND, - TrustedNodeError::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for TrustedNodeError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => TrustedNodeError::NoSuchCoin(coin), - } - } -} - -impl From for TrustedNodeError { - fn from(err: std::io::Error) -> TrustedNodeError { TrustedNodeError::IOError(err.to_string()) } -} diff --git a/mm2src/coins/lightning/ln_events.rs b/mm2src/coins/lightning/ln_events.rs index 9be8c739bc..2b0a7f609d 100644 --- a/mm2src/coins/lightning/ln_events.rs +++ b/mm2src/coins/lightning/ln_events.rs @@ -16,7 +16,7 @@ use lightning::util::events::{Event, EventHandler, PaymentPurpose}; use rand::Rng; use script::{Builder, SignatureVersion}; use secp256k1v22::Secp256k1; -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; use std::sync::Arc; use utxo_signer::with_key_pair::sign_tx; @@ -174,19 +174,29 @@ pub async fn init_abortable_events(platform: Arc, db: SqliteLightningD Ok(()) } +#[derive(Display)] +pub enum SignFundingTransactionError { + #[display(fmt = "Internal error: {}", _0)] + Internal(String), + #[display(fmt = "Error converting transaction: {}", _0)] + ConvertTxErr(String), + #[display(fmt = "Error signing transaction: {}", _0)] + TxSignFailed(String), +} + // Generates the raw funding transaction with one output equal to the channel value. fn sign_funding_transaction( user_channel_id: u64, output_script: &Script, platform: Arc, -) -> OpenChannelResult { +) -> Result { let coin = &platform.coin; let mut unsigned = { let unsigned_funding_txs = platform.unsigned_funding_txs.lock(); unsigned_funding_txs .get(&user_channel_id) .ok_or_else(|| { - OpenChannelError::InternalError(format!( + SignFundingTransactionError::Internal(format!( "Unsigned funding tx not found for internal channel id: {}", user_channel_id )) @@ -195,8 +205,16 @@ fn sign_funding_transaction( }; unsigned.outputs[0].script_pubkey = output_script.to_bytes().into(); - let my_address = coin.as_ref().derivation_method.iguana_or_err()?; - let key_pair = coin.as_ref().priv_key_policy.key_pair_or_err()?; + let my_address = coin + .as_ref() + .derivation_method + .iguana_or_err() + .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; + let key_pair = coin + .as_ref() + .priv_key_policy + .key_pair_or_err() + .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; let prev_script = Builder::build_p2pkh(&my_address.hash); let signed = sign_tx( @@ -205,9 +223,10 @@ fn sign_funding_transaction( prev_script, SignatureVersion::WitnessV0, coin.as_ref().conf.fork_id, - )?; + ) + .map_err(|e| SignFundingTransactionError::TxSignFailed(e.to_string()))?; - Transaction::try_from(signed).map_to_mm(|e| OpenChannelError::ConvertTxErr(e.to_string())) + Transaction::try_from(signed).map_err(|e| SignFundingTransactionError::ConvertTxErr(e.to_string())) } async fn save_channel_closing_details( @@ -323,10 +342,38 @@ impl LightningEventHandler { let payment_preimage = match purpose { PaymentPurpose::InvoicePayment { payment_preimage, .. } => match payment_preimage { Some(preimage) => *preimage, + // This is a swap related payment since we don't have the preimage yet None => { - // Free the htlc immediately if we don't have the preimage required to claim the payment - // to allow for this inbound liquidity to be used for other inbound payments. - self.channel_manager.fail_htlc_backwards(payment_hash); + let payment_info = PaymentInfo { + payment_hash: *payment_hash, + payment_type: PaymentType::InboundPayment, + description: "Swap Payment".into(), + preimage: None, + secret: None, + amt_msat: Some( + received_amount + .try_into() + .expect("received_amount shouldn't exceed i64::MAX"), + ), + fee_paid_msat: None, + status: HTLCStatus::Received, + created_at: (now_ms() / 1000) + .try_into() + .expect("created_at shouldn't exceed i64::MAX"), + last_updated: (now_ms() / 1000) + .try_into() + .expect("last_updated shouldn't exceed i64::MAX"), + }; + let db = self.db.clone(); + let fut = async move { + db.add_or_update_payment_in_db(payment_info) + .await + .error_log_with_msg("Unable to add payment information to DB!"); + }; + + let settings = AbortSettings::default().critical_timout_s(CRITICAL_FUTURE_TIMEOUT); + self.platform.spawner().spawn_with_settings(fut, settings); + return; }, }, @@ -361,7 +408,7 @@ impl LightningEventHandler { self.platform.spawner().spawn_with_settings(fut, settings); }, PaymentPurpose::SpontaneousPayment(payment_preimage) => { - let payment_info = DBPaymentInfo { + let payment_info = PaymentInfo { payment_hash, payment_type: PaymentType::InboundPayment, description: "".into(), diff --git a/mm2src/coins/lightning/ln_p2p.rs b/mm2src/coins/lightning/ln_p2p.rs index 4850c6687f..c93c017f06 100644 --- a/mm2src/coins/lightning/ln_p2p.rs +++ b/mm2src/coins/lightning/ln_p2p.rs @@ -1,5 +1,5 @@ use super::*; -use common::executor::{spawn_abortable, Timer}; +use common::executor::{spawn_abortable, SpawnFuture, Timer}; use common::log::LogState; use derive_more::Display; use lightning::chain::Access; @@ -29,11 +29,19 @@ pub enum ConnectToNodeRes { ConnectedSuccessfully { pubkey: PublicKey, node_addr: SocketAddr }, } -pub async fn connect_to_node( +#[derive(Display)] +pub enum ConnectionError { + #[display(fmt = "Handshake error: {}", _0)] + HandshakeErr(String), + #[display(fmt = "Timeout error: {}", _0)] + TimeOut(String), +} + +pub async fn connect_to_ln_node( pubkey: PublicKey, node_addr: SocketAddr, peer_manager: Arc, -) -> ConnectToNodeResult { +) -> Result { let peer_manager_ref = peer_manager.clone(); let peer_node_ids = async_blocking(move || peer_manager_ref.get_peer_node_ids()).await; if peer_node_ids.contains(&pubkey) { @@ -44,7 +52,7 @@ pub async fn connect_to_node( match lightning_net_tokio::connect_outbound(Arc::clone(&peer_manager), pubkey, node_addr).await { Some(fut) => Box::pin(fut), None => { - return MmError::err(ConnectToNodeError::ConnectionError(format!( + return Err(ConnectionError::TimeOut(format!( "Failed to connect to node: {}", pubkey ))) @@ -55,7 +63,7 @@ pub async fn connect_to_node( // Make sure the connection is still established. match futures::poll!(&mut connection_closed_future) { std::task::Poll::Ready(_) => { - return MmError::err(ConnectToNodeError::ConnectionError(format!( + return Err(ConnectionError::HandshakeErr(format!( "Node {} disconnected before finishing the handshake", pubkey ))); @@ -76,12 +84,12 @@ pub async fn connect_to_node( Ok(ConnectToNodeRes::ConnectedSuccessfully { pubkey, node_addr }) } -pub async fn connect_to_nodes_loop(open_channels_nodes: NodesAddressesMapShared, peer_manager: Arc) { +pub async fn connect_to_ln_nodes_loop(open_channels_nodes: NodesAddressesMapShared, peer_manager: Arc) { loop { let open_channels_nodes = open_channels_nodes.lock().clone(); for (pubkey, node_addr) in open_channels_nodes { let peer_manager = peer_manager.clone(); - match connect_to_node(pubkey, node_addr, peer_manager.clone()).await { + match connect_to_ln_node(pubkey, node_addr, peer_manager.clone()).await { Ok(res) => { if let ConnectToNodeRes::ConnectedSuccessfully { .. } = res { log::info!("{}", res.to_string()); diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs index 01aaceb378..1812aee25a 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -6,14 +6,14 @@ use crate::utxo::rpc_clients::{BestBlock as RpcBestBlock, BlockHashOrHeight, Con use crate::utxo::spv::SimplePaymentVerification; use crate::utxo::utxo_standard::UtxoStandardCoin; use crate::utxo::GetConfirmedTxError; -use crate::{MarketCoinOps, MmCoin}; +use crate::{CoinFutSpawner, MarketCoinOps, MmCoin}; use bitcoin::blockdata::block::BlockHeader; use bitcoin::blockdata::script::Script; use bitcoin::blockdata::transaction::Transaction; use bitcoin::consensus::encode::{deserialize, serialize_hex}; use bitcoin::hash_types::{BlockHash, TxMerkleNode, Txid}; use bitcoin_hashes::{sha256d, Hash}; -use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, Timer}; +use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, SpawnFuture, Timer}; use common::log::{debug, error, info}; use futures::compat::Future01CompatExt; use futures::future::join_all; diff --git a/mm2src/coins/lightning/ln_serialization.rs b/mm2src/coins/lightning/ln_serialization.rs index 01b99d0bd0..511522c328 100644 --- a/mm2src/coins/lightning/ln_serialization.rs +++ b/mm2src/coins/lightning/ln_serialization.rs @@ -1,3 +1,8 @@ +use crate::lightning::ln_db::{DBPaymentsFilter, HTLCStatus, PaymentInfo, PaymentType}; +use crate::lightning::ln_platform::h256_json_from_txid; +use crate::H256Json; +use lightning::chain::channelmonitor::Balance; +use lightning::ln::channelmanager::ChannelDetails; use secp256k1v22::PublicKey; use serde::{de, Serialize, Serializer}; use std::fmt; @@ -94,6 +99,229 @@ impl<'de> de::Deserialize<'de> for PublicKeyForRPC { } } +#[derive(Clone, Serialize)] +pub struct ChannelDetailsForRPC { + pub rpc_channel_id: u64, + pub channel_id: H256Json, + pub counterparty_node_id: PublicKeyForRPC, + pub funding_tx: Option, + pub funding_tx_output_index: Option, + pub funding_tx_value_sats: u64, + /// True if the channel was initiated (and thus funded) by us. + pub is_outbound: bool, + pub balance_msat: u64, + pub outbound_capacity_msat: u64, + pub inbound_capacity_msat: u64, + // Channel is confirmed onchain, this means that funding_locked messages have been exchanged, + // the channel is not currently being shut down, and the required confirmation count has been reached. + pub is_ready: bool, + // Channel is confirmed and channel_ready messages have been exchanged, the peer is connected, + // and the channel is not currently negotiating a shutdown. + pub is_usable: bool, + // A publicly-announced channel. + pub is_public: bool, +} + +impl From for ChannelDetailsForRPC { + fn from(details: ChannelDetails) -> ChannelDetailsForRPC { + ChannelDetailsForRPC { + rpc_channel_id: details.user_channel_id, + channel_id: details.channel_id.into(), + counterparty_node_id: PublicKeyForRPC(details.counterparty.node_id), + funding_tx: details.funding_txo.map(|tx| h256_json_from_txid(tx.txid)), + funding_tx_output_index: details.funding_txo.map(|tx| tx.index), + funding_tx_value_sats: details.channel_value_satoshis, + is_outbound: details.is_outbound, + balance_msat: details.balance_msat, + outbound_capacity_msat: details.outbound_capacity_msat, + inbound_capacity_msat: details.inbound_capacity_msat, + is_ready: details.is_channel_ready, + is_usable: details.is_usable, + is_public: details.is_public, + } + } +} + +#[derive(Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum PaymentTypeForRPC { + #[serde(rename = "Outbound Payment")] + OutboundPayment { destination: PublicKeyForRPC }, + #[serde(rename = "Inbound Payment")] + InboundPayment, +} + +impl From for PaymentTypeForRPC { + fn from(payment_type: PaymentType) -> Self { + match payment_type { + PaymentType::OutboundPayment { destination } => PaymentTypeForRPC::OutboundPayment { + destination: PublicKeyForRPC(destination), + }, + PaymentType::InboundPayment => PaymentTypeForRPC::InboundPayment, + } + } +} + +impl From for PaymentType { + fn from(payment_type: PaymentTypeForRPC) -> Self { + match payment_type { + PaymentTypeForRPC::OutboundPayment { destination } => PaymentType::OutboundPayment { + destination: destination.into(), + }, + PaymentTypeForRPC::InboundPayment => PaymentType::InboundPayment, + } + } +} + +#[derive(Serialize)] +pub struct PaymentInfoForRPC { + payment_hash: H256Json, + payment_type: PaymentTypeForRPC, + description: String, + #[serde(skip_serializing_if = "Option::is_none")] + amount_in_msat: Option, + #[serde(skip_serializing_if = "Option::is_none")] + fee_paid_msat: Option, + status: HTLCStatus, + created_at: i64, + last_updated: i64, +} + +impl From for PaymentInfoForRPC { + fn from(info: PaymentInfo) -> Self { + PaymentInfoForRPC { + payment_hash: info.payment_hash.0.into(), + payment_type: info.payment_type.into(), + description: info.description, + amount_in_msat: info.amt_msat, + fee_paid_msat: info.fee_paid_msat, + status: info.status, + created_at: info.created_at, + last_updated: info.last_updated, + } + } +} + +#[derive(Deserialize)] +pub struct PaymentsFilterForRPC { + pub payment_type: Option, + pub description: Option, + pub status: Option, + pub from_amount_msat: Option, + pub to_amount_msat: Option, + pub from_fee_paid_msat: Option, + pub to_fee_paid_msat: Option, + pub from_timestamp: Option, + pub to_timestamp: Option, +} + +impl From for DBPaymentsFilter { + fn from(filter: PaymentsFilterForRPC) -> Self { + let (is_outbound, destination) = if let Some(payment_type) = filter.payment_type { + match payment_type { + PaymentTypeForRPC::OutboundPayment { destination } => (Some(true), Some(destination.0.to_string())), + PaymentTypeForRPC::InboundPayment => (Some(false), None), + } + } else { + (None, None) + }; + DBPaymentsFilter { + is_outbound, + destination, + description: filter.description, + status: filter.status.map(|s| s.to_string()), + from_amount_msat: filter.from_amount_msat.map(|a| a as i64), + to_amount_msat: filter.to_amount_msat.map(|a| a as i64), + from_fee_paid_msat: filter.from_fee_paid_msat.map(|f| f as i64), + to_fee_paid_msat: filter.to_fee_paid_msat.map(|f| f as i64), + from_timestamp: filter.from_timestamp.map(|f| f as i64), + to_timestamp: filter.to_timestamp.map(|f| f as i64), + } + } +} + +/// Details about the balance(s) available for spending once the channel appears on chain. +#[derive(Serialize)] +pub enum ClaimableBalance { + /// The channel is not yet closed (or the commitment or closing transaction has not yet + /// appeared in a block). The given balance is claimable (less on-chain fees) if the channel is + /// force-closed now. + ClaimableOnChannelClose { + /// The amount available to claim, in satoshis, excluding the on-chain fees which will be + /// required to do so. + claimable_amount_satoshis: u64, + }, + /// The channel has been closed, and the given balance is ours but awaiting confirmations until + /// we consider it spendable. + ClaimableAwaitingConfirmations { + /// The amount available to claim, in satoshis, possibly excluding the on-chain fees which + /// were spent in broadcasting the transaction. + claimable_amount_satoshis: u64, + /// The height at which an [`Event::SpendableOutputs`] event will be generated for this + /// amount. + confirmation_height: u32, + }, + /// The channel has been closed, and the given balance should be ours but awaiting spending + /// transaction confirmation. If the spending transaction does not confirm in time, it is + /// possible our counterparty can take the funds by broadcasting an HTLC timeout on-chain. + /// + /// Once the spending transaction confirms, before it has reached enough confirmations to be + /// considered safe from chain reorganizations, the balance will instead be provided via + /// [`Balance::ClaimableAwaitingConfirmations`]. + ContentiousClaimable { + /// The amount available to claim, in satoshis, excluding the on-chain fees which will be + /// required to do so. + claimable_amount_satoshis: u64, + /// The height at which the counterparty may be able to claim the balance if we have not + /// done so. + timeout_height: u32, + }, + /// HTLCs which we sent to our counterparty which are claimable after a timeout (less on-chain + /// fees) if the counterparty does not know the preimage for the HTLCs. These are somewhat + /// likely to be claimed by our counterparty before we do. + MaybeClaimableHTLCAwaitingTimeout { + /// The amount available to claim, in satoshis, excluding the on-chain fees which will be + /// required to do so. + claimable_amount_satoshis: u64, + /// The height at which we will be able to claim the balance if our counterparty has not + /// done so. + claimable_height: u32, + }, +} + +impl From for ClaimableBalance { + fn from(balance: Balance) -> Self { + match balance { + Balance::ClaimableOnChannelClose { + claimable_amount_satoshis, + } => ClaimableBalance::ClaimableOnChannelClose { + claimable_amount_satoshis, + }, + Balance::ClaimableAwaitingConfirmations { + claimable_amount_satoshis, + confirmation_height, + } => ClaimableBalance::ClaimableAwaitingConfirmations { + claimable_amount_satoshis, + confirmation_height, + }, + Balance::ContentiousClaimable { + claimable_amount_satoshis, + timeout_height, + } => ClaimableBalance::ContentiousClaimable { + claimable_amount_satoshis, + timeout_height, + }, + Balance::MaybeClaimableHTLCAwaitingTimeout { + claimable_amount_satoshis, + claimable_height, + } => ClaimableBalance::MaybeClaimableHTLCAwaitingTimeout { + claimable_amount_satoshis, + claimable_height, + }, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/mm2src/coins/lightning/ln_sql.rs b/mm2src/coins/lightning/ln_sql.rs index ac21b6351e..a1da1d9145 100644 --- a/mm2src/coins/lightning/ln_sql.rs +++ b/mm2src/coins/lightning/ln_sql.rs @@ -1,6 +1,6 @@ -use crate::lightning::ln_db::{ChannelType, ChannelVisibility, ClosedChannelsFilter, DBChannelDetails, DBPaymentInfo, +use crate::lightning::ln_db::{ChannelType, ChannelVisibility, ClosedChannelsFilter, DBChannelDetails, DBPaymentsFilter, GetClosedChannelsResult, GetPaymentsResult, HTLCStatus, LightningDB, - PaymentType}; + PaymentInfo, PaymentType}; use async_trait::async_trait; use common::{async_blocking, PagingOptionsEnum}; use db_common::sqlite::rusqlite::{Error as SqlError, Row, ToSql, NO_PARAMS}; @@ -119,6 +119,21 @@ fn upsert_payment_sql(for_coin: &str) -> Result { Ok(sql) } +fn update_payment_preimage_sql(for_coin: &str) -> Result { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "UPDATE {} SET + preimage = ?1 + WHERE + payment_hash = ?2;", + table_name + ); + + Ok(sql) +} + fn select_channel_by_rpc_id_sql(for_coin: &str) -> Result { let table_name = channels_history_table(for_coin); validate_table_name(&table_name)?; @@ -198,7 +213,7 @@ fn channel_details_from_row(row: &Row<'_>) -> Result Ok(channel_details) } -fn payment_info_from_row(row: &Row<'_>) -> Result { +fn payment_info_from_row(row: &Row<'_>) -> Result { let is_outbound = row.get::<_, bool>(8)?; let payment_type = if is_outbound { PaymentType::OutboundPayment { @@ -208,7 +223,7 @@ fn payment_info_from_row(row: &Row<'_>) -> Result { PaymentType::InboundPayment }; - let payment_info = DBPaymentInfo { + let payment_info = PaymentInfo { payment_hash: PaymentHash(h256_slice_from_row::(row, 0)?), payment_type, description: row.get(2)?, @@ -768,7 +783,7 @@ impl LightningDB for SqliteLightningDB { .await } - async fn add_or_update_payment_in_db(&self, info: DBPaymentInfo) -> Result<(), Self::Error> { + async fn add_or_update_payment_in_db(&self, info: PaymentInfo) -> Result<(), Self::Error> { let for_coin = self.db_ticker.clone(); let payment_hash = hex::encode(info.payment_hash.0); let (is_outbound, destination) = match info.payment_type { @@ -803,7 +818,28 @@ impl LightningDB for SqliteLightningDB { .await } - async fn get_payment_from_db(&self, hash: PaymentHash) -> Result, Self::Error> { + async fn update_payment_preimage_in_db( + &self, + hash: PaymentHash, + preimage: PaymentPreimage, + ) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + let payment_hash = hex::encode(hash.0); + let preimage = hex::encode(preimage.0); + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let params = [&preimage as &dyn ToSql, &payment_hash as &dyn ToSql]; + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + sql_transaction.execute(&update_payment_preimage_sql(&for_coin)?, ¶ms)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn get_payment_from_db(&self, hash: PaymentHash) -> Result, Self::Error> { let params = [hex::encode(hash.0)]; let sql = select_payment_by_hash_sql(self.db_ticker.as_str())?; @@ -946,7 +982,7 @@ mod tests { channels } - fn generate_random_payments(num: u64) -> Vec { + fn generate_random_payments(num: u64) -> Vec { let mut rng = rand::thread_rng(); let mut payments = vec![]; let s = Secp256k1::new(); @@ -970,7 +1006,7 @@ mod tests { HTLCStatus::Failed }; let description: String = rng.sample_iter(&Alphanumeric).take(30).map(char::from).collect(); - let info = DBPaymentInfo { + let info = PaymentInfo { payment_hash: { rng.fill_bytes(&mut bytes); PaymentHash(bytes) @@ -1143,7 +1179,7 @@ mod tests { let payment = block_on(db.get_payment_from_db(PaymentHash([0; 32]))).unwrap(); assert!(payment.is_none()); - let mut expected_payment_info = DBPaymentInfo { + let mut expected_payment_info = PaymentInfo { payment_hash: PaymentHash([0; 32]), payment_type: PaymentType::InboundPayment, description: "test payment".into(), @@ -1173,6 +1209,16 @@ mod tests { let actual_payment_info = block_on(db.get_payment_from_db(PaymentHash([1; 32]))).unwrap().unwrap(); assert_eq!(expected_payment_info, actual_payment_info); + + // Test update_payment_preimage_in_db + let new_preimage = PaymentPreimage([4; 32]); + block_on(db.update_payment_preimage_in_db(PaymentHash([1; 32]), new_preimage)).unwrap(); + let preimage_after_update = block_on(db.get_payment_from_db(PaymentHash([1; 32]))) + .unwrap() + .unwrap() + .preimage + .unwrap(); + assert_eq!(new_preimage, preimage_after_update); } #[test] @@ -1242,7 +1288,7 @@ mod tests { let limit = 10; let result = block_on(db.get_payments_by_filter(Some(filter.clone()), paging.clone(), limit)).unwrap(); - let expected_payments_vec: Vec = payments + let expected_payments_vec: Vec = payments .iter() .map(|p| p.clone()) .filter(|p| p.payment_type == PaymentType::InboundPayment) @@ -1258,7 +1304,7 @@ mod tests { filter.status = Some(HTLCStatus::Succeeded.to_string()); let result = block_on(db.get_payments_by_filter(Some(filter.clone()), paging.clone(), limit)).unwrap(); - let expected_payments_vec: Vec = expected_payments_vec + let expected_payments_vec: Vec = expected_payments_vec .iter() .map(|p| p.clone()) .filter(|p| p.status == HTLCStatus::Succeeded) @@ -1279,7 +1325,7 @@ mod tests { filter.status = None; filter.description = Some(substr.to_string()); let result = block_on(db.get_payments_by_filter(Some(filter), paging, limit)).unwrap(); - let expected_payments_vec: Vec = payments + let expected_payments_vec: Vec = payments .iter() .map(|p| p.clone()) .filter(|p| p.description.contains(&substr)) diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index 0ff77ef00f..d7ddc85595 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -6,17 +6,20 @@ use crate::lightning::ln_storage::{LightningStorage, NodesAddressesMap}; use crate::utxo::rpc_clients::BestBlock as RpcBestBlock; use bitcoin::hash_types::BlockHash; use bitcoin_hashes::{sha256d, Hash}; +use common::executor::SpawnFuture; use common::log::LogState; use lightning::chain::keysinterface::{InMemorySigner, KeysManager}; use lightning::chain::{chainmonitor, BestBlock, Watch}; use lightning::ln::channelmanager::{ChainParameters, ChannelManagerReadArgs, SimpleArcChannelManager}; +use lightning::routing::gossip::RoutingFees; +use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning::util::config::UserConfig; use lightning::util::ser::ReadableArgs; use mm2_core::mm_ctx::MmArc; +use std::collections::hash_map::Entry; use std::fs::File; use std::path::PathBuf; use std::sync::Arc; -use std::time::SystemTime; pub type ChainMonitor = chainmonitor::ChainMonitor< InMemorySigner, @@ -79,9 +82,7 @@ pub async fn init_db(ctx: &MmArc, ticker: String) -> EnableLightningResult EnableLightningResult> { // The current time is used to derive random numbers from the seed where required, to ensure all random generation is unique across restarts. let seed: [u8; 32] = ctx.secp256k1_key_pair().private().secret.into(); - let cur = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .map_to_mm(|e| EnableLightningError::SystemTimeError(e.to_string()))?; + let cur = get_local_duration_since_epoch().map_to_mm(|e| EnableLightningError::SystemTimeError(e.to_string()))?; Ok(Arc::new(KeysManager::new(&seed, cur.as_secs(), cur.subsec_nanos()))) } @@ -239,3 +240,71 @@ pub async fn get_open_channels_nodes_addresses( }); Ok(nodes_addresses) } + +// Todo: Make this public in rust-lightning by opening a PR there instead of importing it here +/// Filters the `channels` for an invoice, and returns the corresponding `RouteHint`s to include +/// in the invoice. +/// +/// The filtering is based on the following criteria: +/// * Only one channel per counterparty node +/// * Always select the channel with the highest inbound capacity per counterparty node +/// * Filter out channels with a lower inbound capacity than `min_inbound_capacity_msat`, if any +/// channel with a higher or equal inbound capacity than `min_inbound_capacity_msat` exists +/// * If any public channel exists, the returned `RouteHint`s will be empty, and the sender will +/// need to find the path by looking at the public channels instead +pub(crate) fn filter_channels(channels: Vec, min_inbound_capacity_msat: Option) -> Vec { + let mut filtered_channels: HashMap = HashMap::new(); + let min_inbound_capacity = min_inbound_capacity_msat.unwrap_or(0); + let mut min_capacity_channel_exists = false; + + for channel in channels.iter() { + if channel.get_inbound_payment_scid().is_none() || channel.counterparty.forwarding_info.is_none() { + continue; + } + + // Todo: if all public channels have inbound_capacity_msat less than min_inbound_capacity we need to give the user the option to reveal his/her private channels to the swap counterparty in this case or not + // Todo: the problem with revealing the private channels in the swap message (invoice) is that it can be used by malicious nodes to probe for private channels so maybe there should be a + // Todo: requirement that the other party has the amount required to be sent in the swap first (do we have a way to check if the other side of the swap has the balance required for the swap on-chain or not) + if channel.is_public { + // If any public channel exists, return no hints and let the sender + // look at the public channels instead. + return vec![]; + } + + if channel.inbound_capacity_msat >= min_inbound_capacity { + min_capacity_channel_exists = true; + }; + match filtered_channels.entry(channel.counterparty.node_id) { + Entry::Occupied(entry) if channel.inbound_capacity_msat < entry.get().inbound_capacity_msat => continue, + Entry::Occupied(mut entry) => entry.insert(channel), + Entry::Vacant(entry) => entry.insert(channel), + }; + } + + let route_hint_from_channel = |channel: &ChannelDetails| { + // It's safe to unwrap here since all filtered_channels have forwarding_info + let forwarding_info = channel.counterparty.forwarding_info.as_ref().unwrap(); + RouteHint(vec![RouteHintHop { + src_node_id: channel.counterparty.node_id, + // It's safe to unwrap here since all filtered_channels have inbound_payment_scid + short_channel_id: channel.get_inbound_payment_scid().unwrap(), + fees: RoutingFees { + base_msat: forwarding_info.fee_base_msat, + proportional_millionths: forwarding_info.fee_proportional_millionths, + }, + cltv_expiry_delta: forwarding_info.cltv_expiry_delta, + htlc_minimum_msat: channel.inbound_htlc_minimum_msat, + htlc_maximum_msat: channel.inbound_htlc_maximum_msat, + }]) + }; + // If all channels are private, return the route hint for the highest inbound capacity channel + // per counterparty node. If channels with an higher inbound capacity than the + // min_inbound_capacity exists, filter out the channels with a lower capacity than that. + filtered_channels + .into_iter() + .filter(|(_counterparty_id, channel)| { + !min_capacity_channel_exists || channel.inbound_capacity_msat >= min_inbound_capacity + }) + .map(|(_counterparty_id, channel)| route_hint_from_channel(channel)) + .collect::>() +} diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 3e05efc781..460b59e908 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -70,8 +70,10 @@ use utxo_signer::with_key_pair::UtxoSignWithKeyPairError; cfg_native! { use crate::lightning::LightningCoin; use crate::lightning::ln_conf::PlatformCoinConfirmationTargets; + use ::lightning::ln::PaymentHash as LightningPayment; use async_std::fs; use futures::AsyncWriteExt; + use lightning_invoice::{Invoice, ParseOrSemanticError}; use std::io; use zcash_primitives::transaction::Transaction as ZTransaction; use z_coin::ZcoinProtocolInfo; @@ -81,7 +83,6 @@ cfg_wasm32! { use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; use hd_wallet_storage::HDWalletDb; use tx_history_storage::wasm::{clear_tx_history, load_tx_history, save_tx_history, TxHistoryDb}; - pub type TxHistoryDbLocked<'a> = DbLocked<'a, TxHistoryDb>; } @@ -362,12 +363,24 @@ pub enum TransactionEnum { #[cfg(not(target_arch = "wasm32"))] ZTransaction(ZTransaction), CosmosTransaction(CosmosTransaction), + #[cfg(not(target_arch = "wasm32"))] + LightningPayment(LightningPayment), } ifrom!(TransactionEnum, UtxoTx); ifrom!(TransactionEnum, SignedEthTx); #[cfg(not(target_arch = "wasm32"))] ifrom!(TransactionEnum, ZTransaction); +#[cfg(not(target_arch = "wasm32"))] +ifrom!(TransactionEnum, LightningPayment); + +impl TransactionEnum { + #[cfg(not(target_arch = "wasm32"))] + pub fn supports_tx_helper(&self) -> bool { !matches!(self, TransactionEnum::LightningPayment(_)) } + + #[cfg(target_arch = "wasm32")] + pub fn supports_tx_helper(&self) -> bool { true } +} // NB: When stable and groked by IDEs, `enum_dispatch` can be used instead of `Deref` to speed things up. impl Deref for TransactionEnum { @@ -379,6 +392,8 @@ impl Deref for TransactionEnum { #[cfg(not(target_arch = "wasm32"))] TransactionEnum::ZTransaction(ref t) => t, TransactionEnum::CosmosTransaction(ref t) => t, + #[cfg(not(target_arch = "wasm32"))] + TransactionEnum::LightningPayment(ref p) => p, } } } @@ -390,6 +405,7 @@ pub enum TxMarshalingErr { /// For cases where serialized and deserialized values doesn't verify each other. CrossCheckFailed(String), NotSupported(String), + Internal(String), } #[derive(Debug, Clone)] @@ -496,6 +512,33 @@ pub struct SearchForSwapTxSpendInput<'a> { pub swap_unique_data: &'a [u8], } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum PaymentInstructions { + #[cfg(not(target_arch = "wasm32"))] + Lightning(Invoice), +} + +#[derive(Display)] +pub enum PaymentInstructionsErr { + LightningInvoiceErr(String), + InternalError(String), +} + +impl From for PaymentInstructionsErr { + fn from(e: NumConversError) -> Self { PaymentInstructionsErr::InternalError(e.to_string()) } +} + +#[derive(Display)] +pub enum ValidateInstructionsErr { + ValidateLightningInvoiceErr(String), + UnsupportedCoin(String), +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for ValidateInstructionsErr { + fn from(e: ParseOrSemanticError) -> Self { ValidateInstructionsErr::ValidateLightningInvoiceErr(e.to_string()) } +} + /// Swap operations (mostly based on the Hash/Time locked transactions implemented by coin wallets). #[async_trait] pub trait SwapOps { @@ -511,6 +554,7 @@ pub trait SwapOps { amount: BigDecimal, swap_contract_address: &Option, swap_unique_data: &[u8], + payment_instructions: &Option, ) -> TransactionFut; #[allow(clippy::too_many_arguments)] @@ -523,6 +567,7 @@ pub trait SwapOps { amount: BigDecimal, swap_contract_address: &Option, swap_unique_data: &[u8], + payment_instructions: &Option, ) -> TransactionFut; #[allow(clippy::too_many_arguments)] @@ -605,7 +650,7 @@ pub trait SwapOps { input: SearchForSwapTxSpendInput<'_>, ) -> Result, String>; - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String>; + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String>; fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result; @@ -630,6 +675,21 @@ pub trait SwapOps { fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair; fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr>; + + async fn payment_instructions( + &self, + secret_hash: &[u8], + amount: &BigDecimal, + ) -> Result>, MmError>; + + fn validate_instructions( + &self, + instructions: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + ) -> Result>; + + fn is_supported_by_watchers(&self) -> bool; } #[async_trait] diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 4bb782f9b7..2ed0268d87 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -15,13 +15,14 @@ use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, AddrFromStrError, Broadca UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFromLegacyReqErr, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; use crate::{BalanceError, BalanceFut, CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, - MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PrivKeyNotAllowed, RawTransactionFut, - RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradeFee, TradePreimageError, - TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, - TransactionErr, TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, - ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, - VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; + MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, + PrivKeyNotAllowed, RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, + SwapOps, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionType, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherOps, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WithdrawError, WithdrawFee, WithdrawFut, + WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; @@ -738,6 +739,7 @@ impl SwapOps for Qrc20Coin { amount: BigDecimal, swap_contract_address: &Option, _swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { let taker_addr = try_tx_fus!(self.contract_address_from_raw_pubkey(taker_pub)); let id = qrc20_swap_id(time_lock, secret_hash); @@ -764,6 +766,7 @@ impl SwapOps for Qrc20Coin { amount: BigDecimal, swap_contract_address: &Option, _swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { let maker_addr = try_tx_fus!(self.contract_address_from_raw_pubkey(maker_pub)); let id = qrc20_swap_id(time_lock, secret_hash); @@ -1006,7 +1009,7 @@ impl SwapOps for Qrc20Coin { } #[inline] - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { self.extract_secret_impl(secret_hash, spend_tx) } @@ -1045,6 +1048,25 @@ impl SwapOps for Qrc20Coin { fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { utxo_common::validate_other_pubkey(raw_pubkey) } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { false } } #[async_trait] diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 1598f52bfc..81c13c3f17 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -427,7 +427,7 @@ fn test_extract_secret() { // taker spent maker payment - d3f5dab4d54c14b3d7ed8c7f5c8cc7f47ccf45ce589fdc7cd5140a3c1c3df6e1 let tx_hex = hex::decode("01000000033f56ecafafc8602fde083ba868d1192d6649b8433e42e1a2d79ba007ea4f7abb010000006b48304502210093404e90e40d22730013035d31c404c875646dcf2fad9aa298348558b6d65ba60220297d045eac5617c1a3eddb71d4bca9772841afa3c4c9d6c68d8d2d42ee6de3950121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff9cac7fe90d597922a1d92e05306c2215628e7ea6d5b855bfb4289c2944f4c73a030000006b483045022100b987da58c2c0c40ce5b6ef2a59e8124ed4ef7a8b3e60c7fb631139280019bc93022069649bcde6fe4dd5df9462a1fcae40598488d6af8c324cd083f5c08afd9568be0121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff70b9870f2b0c65d220a839acecebf80f5b44c3ca4c982fa2fdc5552c037f5610010000006a473044022071b34dd3ebb72d29ca24f3fa0fc96571c815668d3b185dd45cc46a7222b6843f02206c39c030e618d411d4124f7b3e7ca1dd5436775bd8083a85712d123d933a51300121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff020000000000000000c35403a0860101284ca402ed292b806a1835a1b514ad643f2acdb5c8db6b6a9714accff3275ea0d79a3f23be8fd00000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101010101010101010101010101000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac2c02288d4010000001976a914783cf0be521101942da509846ea476e683aad83288ac0f047f5f").unwrap(); - let secret = coin.extract_secret(secret_hash, &tx_hex).unwrap(); + let secret = block_on(coin.extract_secret(secret_hash, &tx_hex)).unwrap(); assert_eq!(secret, expected_secret); } @@ -448,7 +448,7 @@ fn test_extract_secret_malicious() { let spend_tx = hex::decode("01000000022bc8299981ec0cea664cdf9df4f8306396a02e2067d6ac2d3770b34646d2bc2a010000006b483045022100eb13ef2d99ac1cd9984045c2365654b115dd8a7815b7fbf8e2a257f0b93d1592022060d648e73118c843e97f75fafc94e5ff6da70ec8ba36ae255f8c96e2626af6260121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffffd92a0a10ac6d144b36033916f67ae79889f40f35096629a5cd87be1a08f40ee7010000006b48304502210080cdad5c4770dfbeb760e215494c63cc30da843b8505e75e7bf9e8dad18568000220234c0b11c41bfbcdd50046c69059976aedabe17657fe43d809af71e9635678e20121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff030000000000000000c35403a0860101284ca402ed292b8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d000202020202020202020202020202020202020202020202020202020202020202000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac20000000000000000c35403a0860101284ca402ed292b8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101010101010101010101010101000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac2b8ea82d3010000001976a914783cf0be521101942da509846ea476e683aad83288ac735d855f").unwrap(); let expected_secret = &[1; 32]; let secret_hash = &*dhash160(expected_secret); - let actual = coin.extract_secret(secret_hash, &spend_tx); + let actual = block_on(coin.extract_secret(secret_hash, &spend_tx)); assert_eq!(actual, Ok(expected_secret.to_vec())); } diff --git a/mm2src/coins/rpc_command/lightning/close_channel.rs b/mm2src/coins/rpc_command/lightning/close_channel.rs new file mode 100644 index 0000000000..a3fa05644c --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/close_channel.rs @@ -0,0 +1,83 @@ +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::{async_blocking, HttpStatusCode}; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type CloseChannelResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum CloseChannelError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "No such channel with rpc_channel_id {}", _0)] + NoSuchChannel(u64), + #[display(fmt = "Closing channel error: {}", _0)] + CloseChannelError(String), +} + +impl HttpStatusCode for CloseChannelError { + fn status_code(&self) -> StatusCode { + match self { + CloseChannelError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + CloseChannelError::NoSuchChannel(_) | CloseChannelError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + CloseChannelError::CloseChannelError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for CloseChannelError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => CloseChannelError::NoSuchCoin(coin), + } + } +} + +#[derive(Deserialize)] +pub struct CloseChannelReq { + pub coin: String, + pub rpc_channel_id: u64, + #[serde(default)] + pub force_close: bool, +} + +pub async fn close_channel(ctx: MmArc, req: CloseChannelReq) -> CloseChannelResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(CloseChannelError::UnsupportedCoin(e.ticker().to_string())), + }; + + let channel_details = ln_coin + .get_channel_by_rpc_id(req.rpc_channel_id) + .await + .ok_or(CloseChannelError::NoSuchChannel(req.rpc_channel_id))?; + let channel_id = channel_details.channel_id; + let counterparty_node_id = channel_details.counterparty.node_id; + + if req.force_close { + async_blocking(move || { + ln_coin + .channel_manager + .force_close_broadcasting_latest_txn(&channel_id, &counterparty_node_id) + .map_to_mm(|e| CloseChannelError::CloseChannelError(format!("{:?}", e))) + }) + .await?; + } else { + async_blocking(move || { + ln_coin + .channel_manager + .close_channel(&channel_id, &counterparty_node_id) + .map_to_mm(|e| CloseChannelError::CloseChannelError(format!("{:?}", e))) + }) + .await?; + } + + Ok(format!( + "Initiated closing of channel with rpc_channel_id: {}", + req.rpc_channel_id + )) +} diff --git a/mm2src/coins/rpc_command/lightning/connect_to_node.rs b/mm2src/coins/rpc_command/lightning/connect_to_node.rs new file mode 100644 index 0000000000..79ba0f1889 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/connect_to_node.rs @@ -0,0 +1,93 @@ +use crate::lightning::ln_errors::EnableLightningError; +use crate::lightning::ln_p2p::{connect_to_ln_node, ConnectToNodeRes, ConnectionError}; +use crate::lightning::ln_serialization::NodeAddress; +use crate::lightning::ln_storage::LightningStorage; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use std::collections::hash_map::Entry; + +type ConnectToNodeResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ConnectToNodeError { + #[display(fmt = "Parse error: {}", _0)] + ParseError(String), + #[display(fmt = "Error connecting to node: {}", _0)] + ConnectionError(String), + #[display(fmt = "I/O error {}", _0)] + IOError(String), + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), +} + +impl HttpStatusCode for ConnectToNodeError { + fn status_code(&self) -> StatusCode { + match self { + ConnectToNodeError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + ConnectToNodeError::ParseError(_) + | ConnectToNodeError::IOError(_) + | ConnectToNodeError::ConnectionError(_) => StatusCode::INTERNAL_SERVER_ERROR, + ConnectToNodeError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + } + } +} + +impl From for EnableLightningError { + fn from(err: ConnectToNodeError) -> EnableLightningError { + EnableLightningError::ConnectToNodeError(err.to_string()) + } +} + +impl From for ConnectToNodeError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => ConnectToNodeError::NoSuchCoin(coin), + } + } +} + +impl From for ConnectToNodeError { + fn from(err: std::io::Error) -> ConnectToNodeError { ConnectToNodeError::IOError(err.to_string()) } +} + +impl From for ConnectToNodeError { + fn from(err: ConnectionError) -> ConnectToNodeError { ConnectToNodeError::ConnectionError(err.to_string()) } +} + +#[derive(Deserialize)] +pub struct ConnectToNodeRequest { + pub coin: String, + pub node_address: NodeAddress, +} + +/// Connect to a certain node on the lightning network. +pub async fn connect_to_node(ctx: MmArc, req: ConnectToNodeRequest) -> ConnectToNodeResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(ConnectToNodeError::UnsupportedCoin(e.ticker().to_string())), + }; + + let node_pubkey = req.node_address.pubkey; + let node_addr = req.node_address.addr; + let res = connect_to_ln_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()).await?; + + // If a node that we have an open channel with changed it's address, "connect_to_node" + // can be used to reconnect to the new address while saving this new address for reconnections. + if let ConnectToNodeRes::ConnectedSuccessfully { .. } = res { + if let Entry::Occupied(mut entry) = ln_coin.open_channels_nodes.lock().entry(node_pubkey) { + entry.insert(node_addr); + } + ln_coin + .persister + .save_nodes_addresses(ln_coin.open_channels_nodes) + .await?; + } + + Ok(res.to_string()) +} diff --git a/mm2src/coins/rpc_command/lightning/generate_invoice.rs b/mm2src/coins/rpc_command/lightning/generate_invoice.rs new file mode 100644 index 0000000000..68aa8b2237 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/generate_invoice.rs @@ -0,0 +1,104 @@ +use crate::lightning::ln_p2p::connect_to_ln_node; +use crate::lightning::DEFAULT_INVOICE_EXPIRY; +use crate::{lp_coinfind_or_err, CoinFindError, H256Json, MmCoinEnum}; +use bitcoin_hashes::Hash; +use common::log::LogOnError; +use common::{async_blocking, HttpStatusCode}; +use http::StatusCode; +use lightning_invoice::utils::create_invoice_from_channelmanager; +use lightning_invoice::{Invoice, SignOrCreationError}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type GenerateInvoiceResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GenerateInvoiceError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "Invoice signing or creation error: {}", _0)] + SignOrCreationError(String), +} + +impl HttpStatusCode for GenerateInvoiceError { + fn status_code(&self) -> StatusCode { + match self { + GenerateInvoiceError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + GenerateInvoiceError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + GenerateInvoiceError::SignOrCreationError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for GenerateInvoiceError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => GenerateInvoiceError::NoSuchCoin(coin), + } + } +} + +impl From for GenerateInvoiceError { + fn from(e: SignOrCreationError) -> Self { GenerateInvoiceError::SignOrCreationError(e.to_string()) } +} + +#[derive(Deserialize)] +pub struct GenerateInvoiceRequest { + pub coin: String, + pub amount_in_msat: Option, + pub description: String, + pub expiry: Option, +} + +#[derive(Serialize)] +pub struct GenerateInvoiceResponse { + payment_hash: H256Json, + invoice: Invoice, +} + +/// Generates an invoice (request for payment) that can be paid on the lightning network by another node using send_payment. +pub async fn generate_invoice( + ctx: MmArc, + req: GenerateInvoiceRequest, +) -> GenerateInvoiceResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(GenerateInvoiceError::UnsupportedCoin(e.ticker().to_string())), + }; + let open_channels_nodes = ln_coin.open_channels_nodes.lock().clone(); + for (node_pubkey, node_addr) in open_channels_nodes { + connect_to_ln_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()) + .await + .error_log_with_msg(&format!( + "Channel with node: {} can't be used for invoice routing hints due to connection error.", + node_pubkey + )); + } + + let network = ln_coin.platform.network.clone().into(); + let channel_manager = ln_coin.channel_manager.clone(); + let keys_manager = ln_coin.keys_manager.clone(); + let amount_in_msat = req.amount_in_msat; + let description = req.description.clone(); + let expiry = req.expiry.unwrap_or(DEFAULT_INVOICE_EXPIRY); + let invoice = async_blocking(move || { + create_invoice_from_channelmanager( + &channel_manager, + keys_manager, + network, + amount_in_msat, + description, + expiry, + ) + }) + .await?; + + // Note: adding payment to db step is removed since the preimage can be recreated from the keymanager and the invoice secret + Ok(GenerateInvoiceResponse { + payment_hash: invoice.payment_hash().into_inner().into(), + invoice, + }) +} diff --git a/mm2src/coins/rpc_command/lightning/get_channel_details.rs b/mm2src/coins/rpc_command/lightning/get_channel_details.rs new file mode 100644 index 0000000000..f6c18ae633 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/get_channel_details.rs @@ -0,0 +1,81 @@ +use crate::lightning::ln_db::{DBChannelDetails, LightningDB}; +use crate::lightning::ln_serialization::ChannelDetailsForRPC; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::HttpStatusCode; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type GetChannelDetailsResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetChannelDetailsError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "Channel with rpc id: {} is not found", _0)] + NoSuchChannel(u64), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for GetChannelDetailsError { + fn status_code(&self) -> StatusCode { + match self { + GetChannelDetailsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + GetChannelDetailsError::NoSuchCoin(_) | GetChannelDetailsError::NoSuchChannel(_) => StatusCode::NOT_FOUND, + GetChannelDetailsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for GetChannelDetailsError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => GetChannelDetailsError::NoSuchCoin(coin), + } + } +} + +impl From for GetChannelDetailsError { + fn from(err: SqlError) -> GetChannelDetailsError { GetChannelDetailsError::DbError(err.to_string()) } +} + +#[derive(Deserialize)] +pub struct GetChannelDetailsRequest { + pub coin: String, + pub rpc_channel_id: u64, +} + +#[derive(Serialize)] +#[serde(tag = "status", content = "details")] +pub enum GetChannelDetailsResponse { + Open(ChannelDetailsForRPC), + Closed(DBChannelDetails), +} + +pub async fn get_channel_details( + ctx: MmArc, + req: GetChannelDetailsRequest, +) -> GetChannelDetailsResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(GetChannelDetailsError::UnsupportedCoin(e.ticker().to_string())), + }; + + let channel_details = match ln_coin.get_channel_by_rpc_id(req.rpc_channel_id).await { + Some(details) => GetChannelDetailsResponse::Open(details.into()), + None => GetChannelDetailsResponse::Closed( + ln_coin + .db + .get_channel_from_db(req.rpc_channel_id) + .await? + .ok_or(GetChannelDetailsError::NoSuchChannel(req.rpc_channel_id))?, + ), + }; + + Ok(channel_details) +} diff --git a/mm2src/coins/rpc_command/lightning/get_claimable_balances.rs b/mm2src/coins/rpc_command/lightning/get_claimable_balances.rs new file mode 100644 index 0000000000..5d1b84dad1 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/get_claimable_balances.rs @@ -0,0 +1,67 @@ +use crate::lightning::ln_serialization::ClaimableBalance; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::{async_blocking, HttpStatusCode}; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type ClaimableBalancesResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ClaimableBalancesError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), +} + +impl HttpStatusCode for ClaimableBalancesError { + fn status_code(&self) -> StatusCode { + match self { + ClaimableBalancesError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + ClaimableBalancesError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + } + } +} + +impl From for ClaimableBalancesError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => ClaimableBalancesError::NoSuchCoin(coin), + } + } +} + +#[derive(Deserialize)] +pub struct ClaimableBalancesReq { + pub coin: String, + #[serde(default)] + pub include_open_channels_balances: bool, +} + +pub async fn get_claimable_balances( + ctx: MmArc, + req: ClaimableBalancesReq, +) -> ClaimableBalancesResult> { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(ClaimableBalancesError::UnsupportedCoin(e.ticker().to_string())), + }; + let ignored_channels = if req.include_open_channels_balances { + Vec::new() + } else { + ln_coin.list_channels().await + }; + let claimable_balances = async_blocking(move || { + ln_coin + .chain_monitor + .get_claimable_balances(&ignored_channels.iter().collect::>()[..]) + .into_iter() + .map(From::from) + .collect() + }) + .await; + + Ok(claimable_balances) +} diff --git a/mm2src/coins/rpc_command/lightning/get_payment_details.rs b/mm2src/coins/rpc_command/lightning/get_payment_details.rs new file mode 100644 index 0000000000..0e7a4868c2 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/get_payment_details.rs @@ -0,0 +1,76 @@ +use crate::lightning::ln_db::LightningDB; +use crate::lightning::ln_serialization::PaymentInfoForRPC; +use crate::{lp_coinfind_or_err, CoinFindError, H256Json, MmCoinEnum}; +use common::HttpStatusCode; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use lightning::ln::PaymentHash; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type GetPaymentDetailsResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetPaymentDetailsError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "Payment with hash: {:?} is not found", _0)] + NoSuchPayment(H256Json), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for GetPaymentDetailsError { + fn status_code(&self) -> StatusCode { + match self { + GetPaymentDetailsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + GetPaymentDetailsError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + GetPaymentDetailsError::NoSuchPayment(_) => StatusCode::NOT_FOUND, + GetPaymentDetailsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for GetPaymentDetailsError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => GetPaymentDetailsError::NoSuchCoin(coin), + } + } +} + +impl From for GetPaymentDetailsError { + fn from(err: SqlError) -> GetPaymentDetailsError { GetPaymentDetailsError::DbError(err.to_string()) } +} + +#[derive(Deserialize)] +pub struct GetPaymentDetailsRequest { + pub coin: String, + pub payment_hash: H256Json, +} + +#[derive(Serialize)] +pub struct GetPaymentDetailsResponse { + payment_details: PaymentInfoForRPC, +} + +pub async fn get_payment_details( + ctx: MmArc, + req: GetPaymentDetailsRequest, +) -> GetPaymentDetailsResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(GetPaymentDetailsError::UnsupportedCoin(e.ticker().to_string())), + }; + + if let Some(payment_info) = ln_coin.db.get_payment_from_db(PaymentHash(req.payment_hash.0)).await? { + return Ok(GetPaymentDetailsResponse { + payment_details: payment_info.into(), + }); + } + + MmError::err(GetPaymentDetailsError::NoSuchPayment(req.payment_hash)) +} diff --git a/mm2src/coins/rpc_command/lightning/list_channels.rs b/mm2src/coins/rpc_command/lightning/list_channels.rs new file mode 100644 index 0000000000..a96106a99f --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/list_channels.rs @@ -0,0 +1,130 @@ +use crate::lightning::ln_db::{ClosedChannelsFilter, DBChannelDetails, LightningDB}; +use crate::lightning::ln_serialization::ChannelDetailsForRPC; +use crate::lightning::OpenChannelsFilter; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum}; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type ListChannelsResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ListChannelsError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for ListChannelsError { + fn status_code(&self) -> StatusCode { + match self { + ListChannelsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + ListChannelsError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + ListChannelsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for ListChannelsError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => ListChannelsError::NoSuchCoin(coin), + } + } +} + +impl From for ListChannelsError { + fn from(err: SqlError) -> ListChannelsError { ListChannelsError::DbError(err.to_string()) } +} + +#[derive(Deserialize)] +pub struct ListOpenChannelsRequest { + pub coin: String, + pub filter: Option, + #[serde(default = "ten")] + limit: usize, + #[serde(default)] + paging_options: PagingOptionsEnum, +} + +#[derive(Serialize)] +pub struct ListOpenChannelsResponse { + open_channels: Vec, + limit: usize, + skipped: usize, + total: usize, + total_pages: usize, + paging_options: PagingOptionsEnum, +} + +pub async fn list_open_channels_by_filter( + ctx: MmArc, + req: ListOpenChannelsRequest, +) -> ListChannelsResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(ListChannelsError::UnsupportedCoin(e.ticker().to_string())), + }; + + let result = ln_coin + .get_open_channels_by_filter(req.filter, req.paging_options.clone(), req.limit) + .await; + + Ok(ListOpenChannelsResponse { + open_channels: result.channels, + limit: req.limit, + skipped: result.skipped, + total: result.total, + total_pages: calc_total_pages(result.total, req.limit), + paging_options: req.paging_options, + }) +} + +#[derive(Deserialize)] +pub struct ListClosedChannelsRequest { + pub coin: String, + pub filter: Option, + #[serde(default = "ten")] + limit: usize, + #[serde(default)] + paging_options: PagingOptionsEnum, +} + +#[derive(Serialize)] +pub struct ListClosedChannelsResponse { + closed_channels: Vec, + limit: usize, + skipped: usize, + total: usize, + total_pages: usize, + paging_options: PagingOptionsEnum, +} + +pub async fn list_closed_channels_by_filter( + ctx: MmArc, + req: ListClosedChannelsRequest, +) -> ListChannelsResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(ListChannelsError::UnsupportedCoin(e.ticker().to_string())), + }; + let closed_channels_res = ln_coin + .db + .get_closed_channels_by_filter(req.filter, req.paging_options.clone(), req.limit) + .await?; + + Ok(ListClosedChannelsResponse { + closed_channels: closed_channels_res.channels, + limit: req.limit, + skipped: closed_channels_res.skipped, + total: closed_channels_res.total, + total_pages: calc_total_pages(closed_channels_res.total, req.limit), + paging_options: req.paging_options, + }) +} diff --git a/mm2src/coins/rpc_command/lightning/list_payments_by_filter.rs b/mm2src/coins/rpc_command/lightning/list_payments_by_filter.rs new file mode 100644 index 0000000000..0a6d0d3308 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/list_payments_by_filter.rs @@ -0,0 +1,88 @@ +use crate::lightning::ln_db::LightningDB; +use crate::lightning::ln_serialization::{PaymentInfoForRPC, PaymentsFilterForRPC}; +use crate::{lp_coinfind_or_err, CoinFindError, H256Json, MmCoinEnum}; +use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum}; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use lightning::ln::PaymentHash; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type ListPaymentsResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ListPaymentsError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for ListPaymentsError { + fn status_code(&self) -> StatusCode { + match self { + ListPaymentsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + ListPaymentsError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + ListPaymentsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for ListPaymentsError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => ListPaymentsError::NoSuchCoin(coin), + } + } +} + +impl From for ListPaymentsError { + fn from(err: SqlError) -> ListPaymentsError { ListPaymentsError::DbError(err.to_string()) } +} + +#[derive(Deserialize)] +pub struct ListPaymentsReq { + pub coin: String, + pub filter: Option, + #[serde(default = "ten")] + limit: usize, + #[serde(default)] + paging_options: PagingOptionsEnum, +} + +#[derive(Serialize)] +pub struct ListPaymentsResponse { + payments: Vec, + limit: usize, + skipped: usize, + total: usize, + total_pages: usize, + paging_options: PagingOptionsEnum, +} + +pub async fn list_payments_by_filter(ctx: MmArc, req: ListPaymentsReq) -> ListPaymentsResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(ListPaymentsError::UnsupportedCoin(e.ticker().to_string())), + }; + let get_payments_res = ln_coin + .db + .get_payments_by_filter( + req.filter.map(From::from), + req.paging_options.clone().map(|h| PaymentHash(h.0)), + req.limit, + ) + .await?; + + Ok(ListPaymentsResponse { + payments: get_payments_res.payments.into_iter().map(From::from).collect(), + limit: req.limit, + skipped: get_payments_res.skipped, + total: get_payments_res.total, + total_pages: calc_total_pages(get_payments_res.total, req.limit), + paging_options: req.paging_options, + }) +} diff --git a/mm2src/coins/rpc_command/lightning/mod.rs b/mm2src/coins/rpc_command/lightning/mod.rs new file mode 100644 index 0000000000..73aa00bd08 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/mod.rs @@ -0,0 +1,33 @@ +mod close_channel; +mod connect_to_node; +mod generate_invoice; +mod get_channel_details; +mod get_claimable_balances; +mod get_payment_details; +mod list_channels; +mod list_payments_by_filter; +mod open_channel; +mod send_payment; +mod trusted_nodes; +mod update_channel; + +pub mod channels { + pub use super::close_channel::*; + pub use super::get_channel_details::*; + pub use super::get_claimable_balances::*; + pub use super::list_channels::*; + pub use super::open_channel::*; + pub use super::update_channel::*; +} + +pub mod nodes { + pub use super::connect_to_node::*; + pub use super::trusted_nodes::*; +} + +pub mod payments { + pub use super::generate_invoice::*; + pub use super::get_payment_details::*; + pub use super::list_payments_by_filter::*; + pub use super::send_payment::*; +} diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs new file mode 100644 index 0000000000..bf9ccec931 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -0,0 +1,237 @@ +use crate::lightning::ln_conf::{ChannelOptions, OurChannelsConfigs}; +use crate::lightning::ln_db::{DBChannelDetails, LightningDB}; +use crate::lightning::ln_p2p::{connect_to_ln_node, ConnectionError}; +use crate::lightning::ln_serialization::NodeAddress; +use crate::lightning::ln_storage::LightningStorage; +use crate::utxo::utxo_common::UtxoTxBuilder; +use crate::utxo::{sat_from_big_decimal, FeePolicy, GetUtxoListOps, UtxoTxGenerationOps}; +use crate::{lp_coinfind_or_err, BalanceError, CoinFindError, GenerateTxError, MmCoinEnum, NumConversError, + UnexpectedDerivationMethod, UtxoRpcError}; +use chain::TransactionOutput; +use common::log::error; +use common::{async_blocking, HttpStatusCode}; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use keys::AddressHashEnum; +use lightning::util::config::UserConfig; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::BigDecimal; +use script::Builder; + +type OpenChannelResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum OpenChannelError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "Balance Error {}", _0)] + BalanceError(String), + #[display(fmt = "Invalid path: {}", _0)] + InvalidPath(String), + #[display(fmt = "Failure to open channel with node {}: {}", _0, _1)] + FailureToOpenChannel(String, String), + #[display(fmt = "RPC error {}", _0)] + RpcError(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), + #[display(fmt = "I/O error {}", _0)] + IOError(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), + ConnectToNodeError(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "Generate Tx Error {}", _0)] + GenerateTxErr(String), +} + +impl HttpStatusCode for OpenChannelError { + fn status_code(&self) -> StatusCode { + match self { + OpenChannelError::UnsupportedCoin(_) | OpenChannelError::RpcError(_) => StatusCode::BAD_REQUEST, + OpenChannelError::FailureToOpenChannel(_, _) + | OpenChannelError::ConnectToNodeError(_) + | OpenChannelError::InternalError(_) + | OpenChannelError::GenerateTxErr(_) + | OpenChannelError::IOError(_) + | OpenChannelError::DbError(_) + | OpenChannelError::InvalidPath(_) + | OpenChannelError::BalanceError(_) => StatusCode::INTERNAL_SERVER_ERROR, + OpenChannelError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + } + } +} + +impl From for OpenChannelError { + fn from(err: ConnectionError) -> OpenChannelError { OpenChannelError::ConnectToNodeError(err.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => OpenChannelError::NoSuchCoin(coin), + } + } +} + +impl From for OpenChannelError { + fn from(e: BalanceError) -> Self { OpenChannelError::BalanceError(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: NumConversError) -> Self { OpenChannelError::InternalError(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: GenerateTxError) -> Self { OpenChannelError::GenerateTxErr(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: UtxoRpcError) -> Self { OpenChannelError::RpcError(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: UnexpectedDerivationMethod) -> Self { OpenChannelError::InternalError(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(err: std::io::Error) -> OpenChannelError { OpenChannelError::IOError(err.to_string()) } +} + +impl From for OpenChannelError { + fn from(err: SqlError) -> OpenChannelError { OpenChannelError::DbError(err.to_string()) } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(tag = "type", content = "value")] +pub enum ChannelOpenAmount { + Exact(BigDecimal), + Max, +} + +#[derive(Deserialize)] +pub struct OpenChannelRequest { + pub coin: String, + pub node_address: NodeAddress, + pub amount: ChannelOpenAmount, + /// The amount to push to the counterparty as part of the open, in milli-satoshi. Creates inbound liquidity for the channel. + /// By setting push_msat to a value, opening channel request will be equivalent to opening a channel then sending a payment with + /// the push_msat amount. + #[serde(default)] + pub push_msat: u64, + pub channel_options: Option, + pub channel_configs: Option, +} + +#[derive(Serialize)] +pub struct OpenChannelResponse { + rpc_channel_id: u64, + node_address: NodeAddress, +} + +/// Opens a channel on the lightning network. +pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(OpenChannelError::UnsupportedCoin(e.ticker().to_string())), + }; + + // Making sure that the node data is correct and that we can connect to it before doing more operations + let node_pubkey = req.node_address.pubkey; + let node_addr = req.node_address.addr; + connect_to_ln_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()).await?; + + let platform_coin = ln_coin.platform_coin().clone(); + let decimals = platform_coin.as_ref().decimals; + let my_address = platform_coin.as_ref().derivation_method.iguana_or_err()?; + let (unspents, _) = platform_coin.get_unspent_ordered_list(my_address).await?; + let (value, fee_policy) = match req.amount.clone() { + ChannelOpenAmount::Max => ( + unspents.iter().fold(0, |sum, unspent| sum + unspent.value), + FeePolicy::DeductFromOutput(0), + ), + ChannelOpenAmount::Exact(v) => { + let value = sat_from_big_decimal(&v, decimals)?; + (value, FeePolicy::SendExact) + }, + }; + + // The actual script_pubkey will replace this before signing the transaction after receiving the required + // output script from the other node when the channel is accepted + let script_pubkey = + Builder::build_witness_script(&AddressHashEnum::WitnessScriptHash(Default::default())).to_bytes(); + let outputs = vec![TransactionOutput { value, script_pubkey }]; + + let mut tx_builder = UtxoTxBuilder::new(&platform_coin) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_fee_policy(fee_policy); + + let fee = platform_coin + .get_tx_fee() + .await + .map_err(|e| OpenChannelError::RpcError(e.to_string()))?; + tx_builder = tx_builder.with_fee(fee); + + let (unsigned, _) = tx_builder.build().await?; + + let amount_in_sat = unsigned.outputs[0].value; + let push_msat = req.push_msat; + let channel_manager = ln_coin.channel_manager.clone(); + + let mut conf = ln_coin.conf.clone(); + if let Some(options) = req.channel_options { + match conf.channel_options.as_mut() { + Some(o) => o.update_according_to(options), + None => conf.channel_options = Some(options), + } + } + if let Some(configs) = req.channel_configs { + match conf.our_channels_configs.as_mut() { + Some(o) => o.update_according_to(configs), + None => conf.our_channels_configs = Some(configs), + } + } + drop_mutability!(conf); + let user_config: UserConfig = conf.into(); + + let rpc_channel_id = ln_coin.db.get_last_channel_rpc_id().await? as u64 + 1; + + let temp_channel_id = async_blocking(move || { + channel_manager + .create_channel(node_pubkey, amount_in_sat, push_msat, rpc_channel_id, Some(user_config)) + .map_to_mm(|e| OpenChannelError::FailureToOpenChannel(node_pubkey.to_string(), format!("{:?}", e))) + }) + .await?; + + { + let mut unsigned_funding_txs = ln_coin.platform.unsigned_funding_txs.lock(); + unsigned_funding_txs.insert(rpc_channel_id, unsigned); + } + + let pending_channel_details = DBChannelDetails::new( + rpc_channel_id, + temp_channel_id, + node_pubkey, + true, + user_config.channel_handshake_config.announced_channel, + ); + + // Saving node data to reconnect to it on restart + ln_coin.open_channels_nodes.lock().insert(node_pubkey, node_addr); + ln_coin + .persister + .save_nodes_addresses(ln_coin.open_channels_nodes) + .await?; + + if let Err(e) = ln_coin.db.add_channel_to_db(pending_channel_details).await { + error!("Unable to add new outbound channel {} to db: {}", rpc_channel_id, e); + } + + Ok(OpenChannelResponse { + rpc_channel_id, + node_address: req.node_address, + }) +} diff --git a/mm2src/coins/rpc_command/lightning/send_payment.rs b/mm2src/coins/rpc_command/lightning/send_payment.rs new file mode 100644 index 0000000000..e44491008d --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/send_payment.rs @@ -0,0 +1,113 @@ +use crate::lightning::ln_p2p::connect_to_ln_node; +use crate::lightning::ln_serialization::PublicKeyForRPC; +use crate::lightning::PaymentError; +use crate::{lp_coinfind_or_err, CoinFindError, H256Json, MmCoinEnum}; +use common::log::LogOnError; +use common::HttpStatusCode; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use lightning_invoice::Invoice; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type SendPaymentResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum SendPaymentError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "Couldn't parse destination pubkey: {}", _0)] + NoRouteFound(String), + #[display(fmt = "Payment error: {}", _0)] + PaymentError(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for SendPaymentError { + fn status_code(&self) -> StatusCode { + match self { + SendPaymentError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + SendPaymentError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + SendPaymentError::PaymentError(_) | SendPaymentError::NoRouteFound(_) | SendPaymentError::DbError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + }, + } + } +} + +impl From for SendPaymentError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => SendPaymentError::NoSuchCoin(coin), + } + } +} + +impl From for SendPaymentError { + fn from(err: SqlError) -> SendPaymentError { SendPaymentError::DbError(err.to_string()) } +} + +impl From for SendPaymentError { + fn from(err: PaymentError) -> SendPaymentError { SendPaymentError::PaymentError(err.to_string()) } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum Payment { + #[serde(rename = "invoice")] + Invoice { invoice: Invoice }, + #[serde(rename = "keysend")] + Keysend { + // The recieving node pubkey (node ID) + destination: PublicKeyForRPC, + // Amount to send in millisatoshis + amount_in_msat: u64, + // The number of blocks the payment will be locked for if not claimed by the destination, + // It's can be assumed that 6 blocks = 1 hour. We can claim the payment amount back after this cltv expires. + // Minmum value allowed is MIN_FINAL_CLTV_EXPIRY which is currently 24 for rust-lightning. + expiry: u32, + }, +} + +#[derive(Deserialize)] +pub struct SendPaymentReq { + pub coin: String, + pub payment: Payment, +} + +#[derive(Serialize)] +pub struct SendPaymentResponse { + payment_hash: H256Json, +} + +pub async fn send_payment(ctx: MmArc, req: SendPaymentReq) -> SendPaymentResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(SendPaymentError::UnsupportedCoin(e.ticker().to_string())), + }; + let open_channels_nodes = ln_coin.open_channels_nodes.lock().clone(); + for (node_pubkey, node_addr) in open_channels_nodes { + connect_to_ln_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()) + .await + .error_log_with_msg(&format!( + "Channel with node: {} can't be used to route this payment due to connection error.", + node_pubkey + )); + } + let payment_info = match req.payment { + Payment::Invoice { invoice } => ln_coin.pay_invoice(invoice).await?, + Payment::Keysend { + destination, + amount_in_msat, + expiry, + } => ln_coin.keysend(destination.into(), amount_in_msat, expiry).await?, + }; + + Ok(SendPaymentResponse { + payment_hash: payment_info.payment_hash.0.into(), + }) +} diff --git a/mm2src/coins/rpc_command/lightning/trusted_nodes.rs b/mm2src/coins/rpc_command/lightning/trusted_nodes.rs new file mode 100644 index 0000000000..6f0f2fd36e --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/trusted_nodes.rs @@ -0,0 +1,120 @@ +use crate::lightning::ln_serialization::PublicKeyForRPC; +use crate::lightning::ln_storage::LightningStorage; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type TrustedNodeResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum TrustedNodeError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "I/O error {}", _0)] + IOError(String), +} + +impl HttpStatusCode for TrustedNodeError { + fn status_code(&self) -> StatusCode { + match self { + TrustedNodeError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + TrustedNodeError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + TrustedNodeError::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for TrustedNodeError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => TrustedNodeError::NoSuchCoin(coin), + } + } +} + +impl From for TrustedNodeError { + fn from(err: std::io::Error) -> TrustedNodeError { TrustedNodeError::IOError(err.to_string()) } +} + +#[derive(Deserialize)] +pub struct AddTrustedNodeReq { + pub coin: String, + pub node_id: PublicKeyForRPC, +} + +#[derive(Serialize)] +pub struct AddTrustedNodeResponse { + pub added_node: PublicKeyForRPC, +} + +pub async fn add_trusted_node(ctx: MmArc, req: AddTrustedNodeReq) -> TrustedNodeResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), + }; + + if ln_coin.trusted_nodes.lock().insert(req.node_id.clone().into()) { + ln_coin.persister.save_trusted_nodes(ln_coin.trusted_nodes).await?; + } + + Ok(AddTrustedNodeResponse { + added_node: req.node_id, + }) +} + +#[derive(Deserialize)] +pub struct RemoveTrustedNodeReq { + pub coin: String, + pub node_id: PublicKeyForRPC, +} + +#[derive(Serialize)] +pub struct RemoveTrustedNodeResponse { + pub removed_node: PublicKeyForRPC, +} + +pub async fn remove_trusted_node( + ctx: MmArc, + req: RemoveTrustedNodeReq, +) -> TrustedNodeResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), + }; + + if ln_coin.trusted_nodes.lock().remove(&req.node_id.clone().into()) { + ln_coin.persister.save_trusted_nodes(ln_coin.trusted_nodes).await?; + } + + Ok(RemoveTrustedNodeResponse { + removed_node: req.node_id, + }) +} + +#[derive(Deserialize)] +pub struct ListTrustedNodesReq { + pub coin: String, +} + +#[derive(Serialize)] +pub struct ListTrustedNodesResponse { + trusted_nodes: Vec, +} + +pub async fn list_trusted_nodes(ctx: MmArc, req: ListTrustedNodesReq) -> TrustedNodeResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), + }; + + let trusted_nodes = ln_coin.trusted_nodes.lock().clone(); + + Ok(ListTrustedNodesResponse { + trusted_nodes: trusted_nodes.into_iter().map(PublicKeyForRPC).collect(), + }) +} diff --git a/mm2src/coins/rpc_command/lightning/update_channel.rs b/mm2src/coins/rpc_command/lightning/update_channel.rs new file mode 100644 index 0000000000..a108659ea0 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/update_channel.rs @@ -0,0 +1,83 @@ +use crate::lightning::ln_conf::ChannelOptions; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::{async_blocking, HttpStatusCode}; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type UpdateChannelResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum UpdateChannelError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "No such channel with rpc_channel_id {}", _0)] + NoSuchChannel(u64), + #[display(fmt = "Failure to channel {}: {}", _0, _1)] + FailureToUpdateChannel(u64, String), +} + +impl HttpStatusCode for UpdateChannelError { + fn status_code(&self) -> StatusCode { + match self { + UpdateChannelError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + UpdateChannelError::NoSuchChannel(_) | UpdateChannelError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + UpdateChannelError::FailureToUpdateChannel(_, _) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for UpdateChannelError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => UpdateChannelError::NoSuchCoin(coin), + } + } +} + +#[derive(Deserialize)] +pub struct UpdateChannelReq { + pub coin: String, + pub rpc_channel_id: u64, + pub channel_options: ChannelOptions, +} + +#[derive(Serialize)] +pub struct UpdateChannelResponse { + channel_options: ChannelOptions, +} + +/// Updates configuration for an open channel. +pub async fn update_channel(ctx: MmArc, req: UpdateChannelReq) -> UpdateChannelResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(UpdateChannelError::UnsupportedCoin(e.ticker().to_string())), + }; + + let channel_details = ln_coin + .get_channel_by_rpc_id(req.rpc_channel_id) + .await + .ok_or(UpdateChannelError::NoSuchChannel(req.rpc_channel_id))?; + + async_blocking(move || { + let mut channel_options = ln_coin + .conf + .channel_options + .unwrap_or_else(|| req.channel_options.clone()); + if channel_options != req.channel_options { + channel_options.update_according_to(req.channel_options.clone()); + } + drop_mutability!(channel_options); + let channel_ids = &[channel_details.channel_id]; + let counterparty_node_id = channel_details.counterparty.node_id; + ln_coin + .channel_manager + .update_channel_config(&counterparty_node_id, channel_ids, &channel_options.clone().into()) + .map_to_mm(|e| UpdateChannelError::FailureToUpdateChannel(req.rpc_channel_id, format!("{:?}", e)))?; + Ok(UpdateChannelResponse { channel_options }) + }) + .await +} diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs index eec08d40ac..945cea77fe 100644 --- a/mm2src/coins/rpc_command/mod.rs +++ b/mm2src/coins/rpc_command/mod.rs @@ -7,3 +7,4 @@ pub mod init_account_balance; pub mod init_create_account; pub mod init_scan_for_new_addresses; pub mod init_withdraw; +#[cfg(not(target_arch = "wasm32"))] pub mod lightning; diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs index 26732bd105..092b390c99 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -3,11 +3,12 @@ use crate::coin_errors::MyAddressError; use crate::solana::solana_common::{lamports_to_sol, PrepareTransferData, SufficientBalanceError}; use crate::solana::spl::SplTokenInfo; use crate::{BalanceError, BalanceFut, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, - RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionFut, TransactionType, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateOtherPubKeyErr, - ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; + PaymentInstructions, PaymentInstructionsErr, RawTransactionFut, RawTransactionRequest, + SearchForSwapTxSpendInput, SignatureResult, TradePreimageFut, TradePreimageResult, TradePreimageValue, + TransactionDetails, TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, + ValidatePaymentInput, VerificationResult, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use base58::ToBase58; use bincode::{deserialize, serialize}; @@ -443,7 +444,7 @@ impl MarketCoinOps for SolanaCoin { unimplemented!() } - fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { + fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result> { MmError::err(TxMarshalingErr::NotSupported( "tx_enum_from_bytes is not supported for Solana yet.".to_string(), )) @@ -476,6 +477,7 @@ impl SwapOps for SolanaCoin { amount: BigDecimal, swap_contract_address: &Option, _swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { unimplemented!() } @@ -489,18 +491,19 @@ impl SwapOps for SolanaCoin { amount: BigDecimal, swap_contract_address: &Option, _swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { unimplemented!() } fn send_maker_spends_taker_payment( &self, - taker_payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - secret: &[u8], - secret_hash: &[u8], - swap_contract_address: &Option, + _taker_payment_tx: &[u8], + _time_lock: u32, + _taker_pub: &[u8], + _secret: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() @@ -521,11 +524,11 @@ impl SwapOps for SolanaCoin { fn send_taker_refunds_payment( &self, - taker_payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - secret_hash: &[u8], - swap_contract_address: &Option, + _taker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() @@ -590,7 +593,7 @@ impl SwapOps for SolanaCoin { unimplemented!(); } - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } fn negotiate_swap_contract_addr( &self, @@ -602,6 +605,25 @@ impl SwapOps for SolanaCoin { fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { todo!() } fn validate_other_pubkey(&self, _raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + unimplemented!() + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + unimplemented!() + } + + fn is_supported_by_watchers(&self) -> bool { unimplemented!() } } #[allow(clippy::forget_ref, clippy::forget_copy, clippy::cast_ref_to_mut)] diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs index c1ec4808d7..f326ff7132 100644 --- a/mm2src/coins/solana/spl.rs +++ b/mm2src/coins/solana/spl.rs @@ -3,12 +3,12 @@ use crate::coin_errors::MyAddressError; use crate::solana::solana_common::{ui_amount_to_amount, PrepareTransferData, SufficientBalanceError}; use crate::solana::{solana_common, AccountError, SolanaCommonOps, SolanaFeeDetails}; use crate::{BalanceFut, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, - RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, SolanaCoin, - TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionFut, - TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WithdrawError, WithdrawFut, - WithdrawRequest, WithdrawResult}; + PaymentInstructions, PaymentInstructionsErr, RawTransactionFut, RawTransactionRequest, + SearchForSwapTxSpendInput, SignatureResult, SolanaCoin, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TransactionDetails, TransactionFut, TransactionType, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bincode::serialize; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; @@ -307,6 +307,7 @@ impl SwapOps for SplToken { amount: BigDecimal, swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { unimplemented!() } @@ -320,19 +321,20 @@ impl SwapOps for SplToken { amount: BigDecimal, swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { unimplemented!() } fn send_maker_spends_taker_payment( &self, - taker_payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - secret: &[u8], - secret_hash: &[u8], - swap_contract_address: &Option, - swap_unique_data: &[u8], + _taker_payment_tx: &[u8], + _time_lock: u32, + _taker_pub: &[u8], + _secret: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() } @@ -352,12 +354,12 @@ impl SwapOps for SplToken { fn send_taker_refunds_payment( &self, - taker_payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - secret_hash: &[u8], - swap_contract_address: &Option, - swap_unique_data: &[u8], + _taker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() } @@ -421,7 +423,7 @@ impl SwapOps for SplToken { unimplemented!(); } - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } fn negotiate_swap_contract_addr( &self, @@ -433,6 +435,25 @@ impl SwapOps for SplToken { fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { todo!() } fn validate_other_pubkey(&self, _raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + unimplemented!() + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + unimplemented!() + } + + fn is_supported_by_watchers(&self) -> bool { unimplemented!() } } #[allow(clippy::forget_ref, clippy::forget_copy, clippy::cast_ref_to_mut)] diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 6f6f7b9f8a..132536c3e9 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -9,14 +9,14 @@ use crate::utxo::sat_from_big_decimal; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, - RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, - SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradeFee, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionType, - TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateOtherPubKeyErr, + PaymentInstructions, PaymentInstructionsErr, RawTransactionError, RawTransactionFut, + RawTransactionRequest, RawTransactionRes, SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradeFee, + TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, + TransactionErr, TransactionFut, TransactionType, TxFeeDetails, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WithdrawError, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; - use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use common::executor::Timer; @@ -1448,6 +1448,7 @@ impl SwapOps for TendermintCoin { amount: BigDecimal, _swap_contract_address: &Option, _swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { self.send_htlc_for_denom( time_lock_duration, @@ -1468,6 +1469,7 @@ impl SwapOps for TendermintCoin { amount: BigDecimal, _swap_contract_address: &Option, _swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { self.send_htlc_for_denom( time_lock_duration, @@ -1611,12 +1613,12 @@ impl SwapOps for TendermintCoin { fn send_taker_refunds_payment( &self, - taker_payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - secret_hash: &[u8], - swap_contract_address: &Option, - swap_unique_data: &[u8], + _taker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { Box::new(futures01::future::err(TransactionErr::Plain( "Doesn't need transaction broadcast to refund IRIS HTLC".into(), @@ -1692,7 +1694,7 @@ impl SwapOps for TendermintCoin { todo!() } - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { let tx = try_s!(cosmrs::Tx::from_bytes(spend_tx)); let msg = try_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc_proto: crate::tendermint::htlc_proto::ClaimHtlcProtoRep = @@ -1722,6 +1724,25 @@ impl SwapOps for TendermintCoin { .or_mm_err(|| ValidateOtherPubKeyErr::InvalidPubKey(hex::encode(raw_pubkey)))?; Ok(()) } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { false } } #[async_trait] diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index c9d1ac422a..25fde5058e 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -4,12 +4,13 @@ use super::{TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATO use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFut, BigDecimal, CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, MyAddressError, - NegotiateSwapContractAddrErr, RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, - SignatureResult, SwapOps, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, - TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionType, TxFeeDetails, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateOtherPubKeyErr, - ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherOps, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WithdrawError, WithdrawFut, WithdrawRequest}; + NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, RawTransactionFut, + RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradeFee, TradePreimageFut, + TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, + TransactionFut, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, + ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WithdrawError, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use bitcrypto::sha256; use common::executor::abortable_queue::AbortableQueue; @@ -105,6 +106,7 @@ impl SwapOps for TendermintToken { amount: BigDecimal, _swap_contract_address: &Option, _swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { self.platform_coin.send_htlc_for_denom( time_lock_duration, @@ -125,6 +127,7 @@ impl SwapOps for TendermintToken { amount: BigDecimal, swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { self.platform_coin.send_htlc_for_denom( time_lock_duration, @@ -269,8 +272,8 @@ impl SwapOps for TendermintToken { self.platform_coin.search_for_swap_tx_spend_other(input).await } - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { - self.platform_coin.extract_secret(secret_hash, spend_tx) + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + self.platform_coin.extract_secret(secret_hash, spend_tx).await } fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result { @@ -291,6 +294,25 @@ impl SwapOps for TendermintToken { fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { self.platform_coin.validate_other_pubkey(raw_pubkey) } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { false } } #[async_trait] diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 6ed51a9827..719879f728 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -1,11 +1,11 @@ use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, SwapOps, TradeFee, TransactionEnum, TransactionFut}; use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, - NegotiateSwapContractAddrErr, SearchForSwapTxSpendInput, SignatureResult, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TxMarshalingErr, UnexpectedDerivationMethod, - ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, - VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WithdrawFut, WithdrawRequest}; + NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, SearchForSwapTxSpendInput, + SignatureResult, TradePreimageFut, TradePreimageResult, TradePreimageValue, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherOps, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use futures01::Future; use keys::KeyPair; @@ -111,6 +111,7 @@ impl SwapOps for TestCoin { amount: BigDecimal, swap_contract_address: &Option, _swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { unimplemented!() } @@ -124,18 +125,19 @@ impl SwapOps for TestCoin { amount: BigDecimal, swap_contract_address: &Option, _swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { unimplemented!() } fn send_maker_spends_taker_payment( &self, - taker_payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - secret: &[u8], - secret_hash: &[u8], - swap_contract_address: &Option, + _taker_payment_tx: &[u8], + _time_lock: u32, + _taker_pub: &[u8], + _secret: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() @@ -156,11 +158,11 @@ impl SwapOps for TestCoin { fn send_taker_refunds_payment( &self, - taker_payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - secret_hash: &[u8], - swap_contract_address: &Option, + _taker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() @@ -225,7 +227,7 @@ impl SwapOps for TestCoin { unimplemented!(); } - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } fn negotiate_swap_contract_addr( &self, @@ -241,6 +243,25 @@ impl SwapOps for TestCoin { } fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + unimplemented!() + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + unimplemented!() + } + + fn is_supported_by_watchers(&self) -> bool { unimplemented!() } } #[async_trait] diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 3d4d40c0c2..2be5967bd7 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -10,9 +10,10 @@ use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; use crate::{BlockHeightAndTime, CanRefundHtlc, CoinBalance, CoinProtocol, CoinWithDerivationMethod, - NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, - SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradePreimageValue, TransactionFut, TransactionType, - TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateOtherPubKeyErr, + NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, SwapOps, + TradePreimageValue, TransactionFut, TransactionType, TxFeeDetails, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WithdrawFut}; use common::log::warn; @@ -827,6 +828,7 @@ impl SwapOps for BchCoin { amount: BigDecimal, _swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { utxo_common::send_maker_payment( self.clone(), @@ -848,6 +850,7 @@ impl SwapOps for BchCoin { amount: BigDecimal, _swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { utxo_common::send_taker_payment( self.clone(), @@ -1013,7 +1016,7 @@ impl SwapOps for BchCoin { } #[inline] - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -1044,6 +1047,25 @@ impl SwapOps for BchCoin { fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { utxo_common::validate_other_pubkey(raw_pubkey) } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { true } } fn total_unspent_value<'a>(unspents: impl IntoIterator) -> u64 { diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index acdaf5a033..5037597f0f 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -23,11 +23,12 @@ use crate::utxo::utxo_builder::{BlockHeaderUtxoArcOps, MergeUtxoArcOps, UtxoCoin use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; use crate::{eth, CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, DelegationError, DelegationFut, - GetWithdrawSenderAddress, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, - SignatureResult, StakingInfosFut, SwapOps, TradePreimageValue, TransactionFut, TxMarshalingErr, - UnexpectedDerivationMethod, ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, - ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WithdrawFut, WithdrawSenderAddress}; + GetWithdrawSenderAddress, NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, + PrivKeyBuildPolicy, SearchForSwapTxSpendInput, SignatureResult, StakingInfosFut, SwapOps, + TradePreimageValue, TransactionFut, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, + VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WithdrawFut, WithdrawSenderAddress}; use crypto::Bip44Chain; use ethereum_types::H160; use futures::{FutureExt, TryFutureExt}; @@ -520,6 +521,7 @@ impl SwapOps for QtumCoin { amount: BigDecimal, _swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { utxo_common::send_maker_payment( self.clone(), @@ -541,6 +543,7 @@ impl SwapOps for QtumCoin { amount: BigDecimal, _swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { utxo_common::send_taker_payment( self.clone(), @@ -706,7 +709,7 @@ impl SwapOps for QtumCoin { } #[inline] - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -737,6 +740,25 @@ impl SwapOps for QtumCoin { fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { utxo_common::validate_other_pubkey(raw_pubkey) } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { true } } #[async_trait] diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index 0e1e0c5f62..e3bd9ddfb7 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -188,8 +188,8 @@ impl UtxoRpcClientEnum { ); } error!( - "Tx {} not found on chain, error: {}, retrying in 10 seconds. Retries left: {}", - tx_hash, e, tx_not_found_retries + "Tx {} not found on chain, error: {}, retrying in {} seconds. Retries left: {}", + tx_hash, e, check_every, tx_not_found_retries ); tx_not_found_retries -= 1; Timer::sleep(check_every as f64).await; @@ -200,7 +200,7 @@ impl UtxoRpcClientEnum { let block = match selfi.get_block_count().compat().await { Ok(b) => b, Err(e) => { - error!("Error {} getting block number, retrying in 10 seconds", e); + error!("Error {} getting block number, retrying in {} seconds", e, check_every); Timer::sleep(check_every as f64).await; continue; }, @@ -211,8 +211,8 @@ impl UtxoRpcClientEnum { } } error!( - "Error {:?} getting the transaction {:?}, retrying in 10 seconds", - e, tx_hash + "Error {:?} getting the transaction {:?}, retrying in {} seconds", + e, tx_hash, check_every ) }, } diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 516cb158eb..8b313e9ab8 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -14,13 +14,14 @@ use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualTxFee, Addit FeePolicy, GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, UtxoCommonOps, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps}; use crate::{BalanceFut, CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, - MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, PrivKeyNotAllowed, - RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradeFee, - TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, - TransactionEnum, TransactionErr, TransactionFut, TxFeeDetails, TxMarshalingErr, - UnexpectedDerivationMethod, ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentInput, - VerificationError, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; + MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, PaymentInstructions, + PaymentInstructionsErr, PrivKeyNotAllowed, RawTransactionFut, RawTransactionRequest, + SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, + TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, + TransactionFut, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentInput, VerificationError, + VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use bitcrypto::dhash160; use chain::constants::SEQUENCE_FINAL; @@ -1205,6 +1206,7 @@ impl SwapOps for SlpToken { amount: BigDecimal, _swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { let taker_pub = try_tx_fus!(Public::from_slice(taker_pub)); let amount = try_tx_fus!(sat_from_big_decimal(&amount, self.decimals())); @@ -1231,6 +1233,7 @@ impl SwapOps for SlpToken { amount: BigDecimal, _swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { let maker_pub = try_tx_fus!(Public::from_slice(maker_pub)); let amount = try_tx_fus!(sat_from_big_decimal(&amount, self.decimals())); @@ -1440,7 +1443,7 @@ impl SwapOps for SlpToken { } #[inline] - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -1461,6 +1464,25 @@ impl SwapOps for SlpToken { fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { utxo_common::validate_other_pubkey(raw_pubkey) } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { false } } #[async_trait] diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 7fb9962f6b..8ed59b8eaf 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2269,7 +2269,7 @@ pub fn tx_enum_from_bytes(coin: &UtxoCoinFields, bytes: &[u8]) -> Result, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { utxo_common::send_maker_payment( self.clone(), @@ -307,6 +309,7 @@ impl SwapOps for UtxoStandardCoin { amount: BigDecimal, _swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { utxo_common::send_taker_payment( self.clone(), @@ -472,7 +475,7 @@ impl SwapOps for UtxoStandardCoin { } #[inline] - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -503,6 +506,25 @@ impl SwapOps for UtxoStandardCoin { fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { utxo_common::validate_other_pubkey(raw_pubkey) } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { true } } #[async_trait] diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 1a6b7de127..f0fc5623fd 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -136,7 +136,7 @@ fn test_extract_secret() { let tx_hex = hex::decode("0100000001de7aa8d29524906b2b54ee2e0281f3607f75662cbc9080df81d1047b78e21dbc00000000d7473044022079b6c50820040b1fbbe9251ced32ab334d33830f6f8d0bf0a40c7f1336b67d5b0220142ccf723ddabb34e542ed65c395abc1fbf5b6c3e730396f15d25c49b668a1a401209da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365004c6b6304f62b0e5cb175210270e75970bb20029b3879ec76c4acd320a8d0589e003636264d01a7d566504bfbac6782012088a9142fb610d856c19fd57f2d0cffe8dff689074b3d8a882103f368228456c940ac113e53dad5c104cf209f2f102a409207269383b6ab9b03deac68ffffffff01d0dc9800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac40280e5c").unwrap(); let expected_secret = hex::decode("9da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365").unwrap(); let secret_hash = &*dhash160(&expected_secret); - let secret = coin.extract_secret(secret_hash, &tx_hex).unwrap(); + let secret = block_on(coin.extract_secret(secret_hash, &tx_hex)).unwrap(); assert_eq!(secret, expected_secret); } diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index d7fb492a2a..529b6af8d8 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -13,13 +13,13 @@ use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxDa UtxoCommonOps, UtxoFeeDetails, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; use crate::{BalanceError, BalanceFut, CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, - MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, PrivKeyActivationPolicy, - RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, SignatureError, SignatureResult, - SwapOps, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, - TransactionEnum, TransactionFut, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, - ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, - VerificationError, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WithdrawFut, WithdrawRequest}; + MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, PaymentInstructions, + PaymentInstructionsErr, PrivKeyActivationPolicy, RawTransactionFut, RawTransactionRequest, + SearchForSwapTxSpendInput, SignatureError, SignatureResult, SwapOps, TradeFee, TradePreimageFut, + TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionFut, + TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, + WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WithdrawFut, WithdrawRequest}; use crate::{Transaction, WithdrawError}; use async_trait::async_trait; use bitcrypto::dhash256; @@ -1040,6 +1040,7 @@ impl SwapOps for ZCoin { amount: BigDecimal, _swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { let selfi = self.clone(); let maker_key_pair = self.derive_htlc_key_pair(swap_unique_data); @@ -1071,6 +1072,7 @@ impl SwapOps for ZCoin { amount: BigDecimal, _swap_contract_address: &Option, swap_unique_data: &[u8], + _payment_instructions: &Option, ) -> TransactionFut { let selfi = self.clone(); let taker_keypair = self.derive_htlc_key_pair(swap_unique_data); @@ -1371,7 +1373,7 @@ impl SwapOps for ZCoin { } #[inline] - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -1395,6 +1397,25 @@ impl SwapOps for ZCoin { fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { utxo_common::validate_other_pubkey(raw_pubkey) } + + async fn payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { false } } #[async_trait] diff --git a/mm2src/coins/z_coin/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs index 51957d8218..fca156d0f7 100644 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ b/mm2src/coins/z_coin/z_coin_native_tests.rs @@ -43,13 +43,21 @@ fn zombie_coin_send_and_refund_maker_payment() { &secret_hash, "0.01".parse().unwrap(), &None, + &None, ) .wait() .unwrap(); println!("swap tx {}", hex::encode(&tx.tx_hash().0)); let refund_tx = coin - .send_maker_refunds_payment(&tx.tx_hex(), lock_time, &*taker_pub, &secret_hash, &priv_key, &None) + .send_maker_refunds_payment( + &tx.tx_hex().unwrap(), + lock_time, + &*taker_pub, + &secret_hash, + &priv_key, + &None, + ) .wait() .unwrap(); println!("refund tx {}", hex::encode(&refund_tx.tx_hash().0)); @@ -93,6 +101,7 @@ fn zombie_coin_send_and_spend_maker_payment() { &*secret_hash, "0.01".parse().unwrap(), &None, + &None, ) .wait() .unwrap(); diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 8e928c937e..047b0169a0 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -136,6 +136,7 @@ use std::os::raw::c_void; use std::panic::{set_hook, PanicInfo}; use std::ptr::read_volatile; use std::sync::atomic::Ordering; +use std::time::{Duration, SystemTime, SystemTimeError}; use uuid::Uuid; pub use http::StatusCode; @@ -630,7 +631,6 @@ pub fn now_ms() -> u64 { js_sys::Date::now() as u64 } #[cfg(target_arch = "wasm32")] pub fn now_float() -> f64 { use gstuff::duration_to_float; - use std::time::Duration; duration_to_float(Duration::from_millis(now_ms())) } @@ -937,3 +937,8 @@ impl Default for PagingOptionsEnum { #[inline(always)] pub fn get_utc_timestamp() -> i64 { Utc::now().timestamp() } + +#[inline(always)] +pub fn get_local_duration_since_epoch() -> Result { + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) +} diff --git a/mm2src/mm2_main/src/docker_tests.rs b/mm2src/mm2_main/src/docker_tests.rs index 1157b21d21..a6f6027047 100644 --- a/mm2src/mm2_main/src/docker_tests.rs +++ b/mm2src/mm2_main/src/docker_tests.rs @@ -26,9 +26,6 @@ extern crate serde_json; extern crate serde_derive; #[cfg(test)] #[macro_use] -extern crate serialization_derive; -#[cfg(test)] -#[macro_use] extern crate ser_error_derive; #[cfg(test)] extern crate test; @@ -397,7 +394,7 @@ mod docker_tests { let time_lock = (now_ms() / 1000) as u32 - 3600; let tx = coin - .send_taker_payment(0, time_lock, my_public_key, &[0; 20], 1u64.into(), &None, &[]) + .send_taker_payment(0, time_lock, my_public_key, &[0; 20], 1u64.into(), &None, &[], &None) .wait() .unwrap(); @@ -406,7 +403,7 @@ mod docker_tests { .unwrap(); let refund_tx = coin - .send_taker_refunds_payment(&tx.tx_hex(), time_lock, my_public_key, &[0; 20], &None, &[]) + .send_maker_refunds_payment(&tx.tx_hex(), time_lock, my_public_key, &[0; 20], &None, &[]) .wait() .unwrap(); @@ -454,7 +451,7 @@ mod docker_tests { let time_lock = (now_ms() / 1000) as u32 - 3600; let tx = coin - .send_maker_payment(0, time_lock, my_public_key, &[0; 20], 1u64.into(), &None, &[]) + .send_maker_payment(0, time_lock, my_public_key, &[0; 20], 1u64.into(), &None, &[], &None) .wait() .unwrap(); @@ -496,7 +493,16 @@ mod docker_tests { let secret_hash = dhash160(&secret); let time_lock = (now_ms() / 1000) as u32 - 3600; let tx = coin - .send_taker_payment(0, time_lock, my_pubkey, secret_hash.as_slice(), 1u64.into(), &None, &[]) + .send_taker_payment( + 0, + time_lock, + my_pubkey, + secret_hash.as_slice(), + 1u64.into(), + &None, + &[], + &None, + ) .wait() .unwrap(); @@ -546,7 +552,16 @@ mod docker_tests { let time_lock = (now_ms() / 1000) as u32 - 3600; let secret_hash = dhash160(&secret); let tx = coin - .send_maker_payment(0, time_lock, my_pubkey, secret_hash.as_slice(), 1u64.into(), &None, &[]) + .send_maker_payment( + 0, + time_lock, + my_pubkey, + secret_hash.as_slice(), + 1u64.into(), + &None, + &[], + &None, + ) .wait() .unwrap(); @@ -607,6 +622,7 @@ mod docker_tests { 1.into(), &coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -1287,7 +1303,7 @@ mod docker_tests { let time_lock = (now_ms() / 1000) as u32 - 3600; let tx = coin - .send_taker_payment(0, time_lock, my_public_key, &[0; 20], 1u64.into(), &None, &[]) + .send_taker_payment(0, time_lock, my_public_key, &[0; 20], 1u64.into(), &None, &[], &None) .wait() .unwrap(); diff --git a/mm2src/mm2_main/src/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/src/docker_tests/qrc20_tests.rs index 758ea3ec6f..40ee65729e 100644 --- a/mm2src/mm2_main/src/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/src/docker_tests/qrc20_tests.rs @@ -185,6 +185,7 @@ fn test_taker_spends_maker_payment() { amount.clone(), &maker_coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -280,6 +281,7 @@ fn test_maker_spends_taker_payment() { amount.clone(), &taker_coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -364,6 +366,7 @@ fn test_maker_refunds_payment() { amount.clone(), &coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -426,6 +429,7 @@ fn test_taker_refunds_payment() { amount.clone(), &coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -485,6 +489,7 @@ fn test_check_if_my_payment_sent() { amount.clone(), &coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -538,6 +543,7 @@ fn test_search_for_swap_tx_spend_taker_spent() { amount, &maker_coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -610,6 +616,7 @@ fn test_search_for_swap_tx_spend_maker_refunded() { amount, &maker_coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -681,6 +688,7 @@ fn test_search_for_swap_tx_spend_not_spent() { amount, &maker_coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -733,6 +741,7 @@ fn test_wait_for_tx_spend() { amount, &maker_coin.swap_contract_address(), &[], + &None, ) .wait() .unwrap(); @@ -1048,6 +1057,7 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ expected_max_taker_vol.to_decimal(), &None, &[], + &None, ) .wait() .expect("!send_taker_payment"); @@ -1435,7 +1445,7 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_maker() { let time_lock = (now_ms() / 1000) as u32 - 3600; let tx = coin - .send_maker_payment(0, time_lock, my_public_key, &[0; 20], 1u64.into(), &None, &[]) + .send_maker_payment(0, time_lock, my_public_key, &[0; 20], 1u64.into(), &None, &[], &None) .wait() .unwrap(); @@ -1476,7 +1486,7 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_taker() { let time_lock = (now_ms() / 1000) as u32 - 3600; let tx = coin - .send_taker_payment(0, time_lock, my_public_key, &[0; 20], 1u64.into(), &None, &[]) + .send_taker_payment(0, time_lock, my_public_key, &[0; 20], 1u64.into(), &None, &[], &None) .wait() .unwrap(); @@ -1485,7 +1495,7 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_taker() { .unwrap(); let refund_tx = coin - .send_taker_refunds_payment(&tx.tx_hex(), time_lock, my_public_key, &[0; 20], &None, &[]) + .send_maker_refunds_payment(&tx.tx_hex(), time_lock, my_public_key, &[0; 20], &None, &[]) .wait() .unwrap(); diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 64364a5636..d576311e3a 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -58,6 +58,7 @@ // use crate::mm2::lp_network::{broadcast_p2p_msg, Libp2pPeerId}; +use bitcrypto::{dhash160, sha256}; use coins::{lp_coinfind, MmCoinEnum, TradeFee, TransactionEnum}; use common::log::{debug, warn}; use common::time_cache::DuplicateCache; @@ -72,7 +73,6 @@ use mm2_err_handle::prelude::*; use mm2_libp2p::{decode_signed, encode_and_sign, pub_sub_topic, TopicPrefix}; use mm2_number::{BigDecimal, BigRational, MmNumber}; use parking_lot::Mutex as PaMutex; -use primitives::hash::{H160, H264}; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; use serde::Serialize; use serde_json::{self as json, Value as Json}; @@ -84,7 +84,6 @@ use std::sync::{Arc, Mutex, Weak}; use std::time::Duration; use uuid::Uuid; -use bitcrypto::{dhash160, sha256}; #[cfg(feature = "custom-swap-locktime")] use std::sync::atomic::{AtomicU64, Ordering}; @@ -133,13 +132,13 @@ cfg_wasm32! { pub type SwapDbLocked<'a> = DbLocked<'a, SwapDb>; } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] pub enum SwapMsg { Negotiation(NegotiationDataMsg), NegotiationReply(NegotiationDataMsg), Negotiated(bool), - TakerFee(Vec), - MakerPayment(Vec), + TakerFee(SwapTxDataMsg), + MakerPayment(SwapTxDataMsg), TakerPayment(Vec), } @@ -148,8 +147,8 @@ pub struct SwapMsgStore { negotiation: Option, negotiation_reply: Option, negotiated: Option, - taker_fee: Option>, - maker_payment: Option>, + taker_fee: Option, + maker_payment: Option, taker_payment: Option>, accept_only_from: bits256, } @@ -193,6 +192,10 @@ pub fn broadcast_swap_message(ctx: &MmArc, topic: String, msg: T, /// Broadcast the tx message once pub fn broadcast_p2p_tx_msg(ctx: &MmArc, topic: String, msg: &TransactionEnum, p2p_privkey: &Option) { + if !msg.supports_tx_helper() { + return; + } + let (p2p_private, from) = match p2p_privkey { Some(keypair) => (keypair.private_bytes(), Some(keypair.libp2p_peer_id())), None => (ctx.secp256k1_key_pair().private().secret.take(), None), @@ -239,8 +242,8 @@ pub async fn process_msg(ctx: MmArc, topic: &str, msg: &[u8]) { SwapMsg::Negotiation(data) => msg_store.negotiation = Some(data), SwapMsg::NegotiationReply(data) => msg_store.negotiation_reply = Some(data), SwapMsg::Negotiated(negotiated) => msg_store.negotiated = Some(negotiated), - SwapMsg::TakerFee(taker_fee) => msg_store.taker_fee = Some(taker_fee), - SwapMsg::MakerPayment(maker_payment) => msg_store.maker_payment = Some(maker_payment), + SwapMsg::TakerFee(data) => msg_store.taker_fee = Some(data), + SwapMsg::MakerPayment(data) => msg_store.maker_payment = Some(data), SwapMsg::TakerPayment(taker_payment) => msg_store.taker_payment = Some(taker_payment), } } else { @@ -688,18 +691,54 @@ impl NegotiationDataMsg { } } -/// Data to be exchanged and validated on swap start, the replacement of LP_pubkeys_data, LP_choosei_data, etc. -#[derive(Debug, Default, Deserializable, Eq, PartialEq, Serializable)] -struct SwapNegotiationData { - started_at: u64, - payment_locktime: u64, - secret_hash: H160, - persistent_pubkey: H264, +#[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] +pub struct PaymentWithInstructions { + data: Vec, + // Next step instructions for the other side whether taker or maker. + // An example for this is a maker/taker sending the taker/maker a lightning invoice to be payed. + next_step_instructions: Vec, +} + +#[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum SwapTxDataMsg { + Regular(Vec), + WithInstructions(PaymentWithInstructions), +} + +impl SwapTxDataMsg { + #[inline] + pub fn data(&self) -> &[u8] { + match self { + SwapTxDataMsg::Regular(data) => data, + SwapTxDataMsg::WithInstructions(p) => &p.data, + } + } + + #[inline] + pub fn instructions(&self) -> Option<&[u8]> { + match self { + SwapTxDataMsg::Regular(_) => None, + SwapTxDataMsg::WithInstructions(p) => Some(&p.next_step_instructions), + } + } + + #[inline] + pub fn new(data: Vec, instructions: Option>) -> Self { + match instructions { + Some(next_step_instructions) => SwapTxDataMsg::WithInstructions(PaymentWithInstructions { + data, + next_step_instructions, + }), + None => SwapTxDataMsg::Regular(data), + } + } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct TransactionIdentifier { - /// Raw bytes of signed transaction in hexadecimal string, this should be sent as is to send_raw_transaction RPC to broadcast the transaction + /// Raw bytes of signed transaction in hexadecimal string, this should be sent as is to send_raw_transaction RPC to broadcast the transaction. + /// Some payments like lightning payments don't have a tx_hex, for such payments tx_hex will be equal to tx_hash. tx_hex: BytesJson, /// Transaction hash in hexadecimal format tx_hash: BytesJson, @@ -1233,6 +1272,20 @@ impl SecretHashAlgo { } } +// Todo: Maybe add a secret_hash_algo method to the SwapOps trait instead +#[cfg(not(target_arch = "wasm32"))] +fn detect_secret_hash_algo(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) -> SecretHashAlgo { + match (maker_coin, taker_coin) { + (MmCoinEnum::Tendermint(_) | MmCoinEnum::TendermintToken(_) | MmCoinEnum::LightningCoin(_), _) => { + SecretHashAlgo::SHA256 + }, + // If taker is lightning coin the SHA256 of the secret will be sent as part of the maker signed invoice + (_, MmCoinEnum::Tendermint(_) | MmCoinEnum::TendermintToken(_)) => SecretHashAlgo::SHA256, + (_, _) => SecretHashAlgo::DHASH160, + } +} + +#[cfg(target_arch = "wasm32")] fn detect_secret_hash_algo(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) -> SecretHashAlgo { match (maker_coin, taker_coin) { (MmCoinEnum::Tendermint(_) | MmCoinEnum::TendermintToken(_), _) => SecretHashAlgo::SHA256, @@ -1254,7 +1307,6 @@ mod lp_swap_tests { use crypto::privkey::key_pair_from_seed; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_test_helpers::for_tests::{morty_conf, rick_conf, MORTY_ELECTRUM_ADDRS, RICK_ELECTRUM_ADDRS}; - use serialization::{deserialize, serialize}; #[test] fn test_dex_fee_amount() { @@ -1288,14 +1340,6 @@ mod lp_swap_tests { assert_eq!(dex_fee_threshold, actual_fee); } - #[test] - fn test_serde_swap_negotiation_data() { - let data = SwapNegotiationData::default(); - let bytes = serialize(&data); - let deserialized = deserialize(bytes.as_slice()).unwrap(); - assert_eq!(data, deserialized); - } - #[test] fn test_lp_atomic_locktime() { let maker_coin = "KMD"; @@ -1547,6 +1591,77 @@ mod lp_swap_tests { assert_eq!(deserialized, v3); } + #[test] + fn check_payment_data_serde() { + const MSG_DATA_INSTRUCTIONS: [u8; 300] = [1; 300]; + + #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] + enum SwapMsgOld { + Negotiation(NegotiationDataMsg), + NegotiationReply(NegotiationDataMsg), + Negotiated(bool), + TakerFee(Vec), + MakerPayment(Vec), + TakerPayment(Vec), + } + + // old message format should be deserialized to PaymentDataMsg::Regular + let old = SwapMsgOld::MakerPayment(MSG_DATA_INSTRUCTIONS.to_vec()); + + let expected = SwapMsg::MakerPayment(SwapTxDataMsg::Regular(MSG_DATA_INSTRUCTIONS.to_vec())); + + let serialized = rmp_serde::to_vec(&old).unwrap(); + + let deserialized: SwapMsg = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + + assert_eq!(deserialized, expected); + + // PaymentDataMsg::Regular should be deserialized to old message format + let v1 = SwapMsg::MakerPayment(SwapTxDataMsg::Regular(MSG_DATA_INSTRUCTIONS.to_vec())); + + let expected = old; + + let serialized = rmp_serde::to_vec(&v1).unwrap(); + + let deserialized: SwapMsgOld = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + + assert_eq!(deserialized, expected); + + // PaymentDataMsg::Regular should be deserialized to PaymentDataMsg::Regular + let v1 = SwapMsg::MakerPayment(SwapTxDataMsg::Regular(MSG_DATA_INSTRUCTIONS.to_vec())); + + let serialized = rmp_serde::to_vec(&v1).unwrap(); + + let deserialized: SwapMsg = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + + assert_eq!(deserialized, v1); + + // PaymentDataMsg::WithInstructions should be deserialized to PaymentDataMsg::WithInstructions + let v2 = SwapMsg::MakerPayment(SwapTxDataMsg::WithInstructions(PaymentWithInstructions { + data: MSG_DATA_INSTRUCTIONS.to_vec(), + next_step_instructions: MSG_DATA_INSTRUCTIONS.to_vec(), + })); + + let serialized = rmp_serde::to_vec(&v2).unwrap(); + + let deserialized: SwapMsg = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + + assert_eq!(deserialized, v2); + + // PaymentDataMsg::WithInstructions shouldn't be deserialized to old message format, new nodes with payment instructions can't swap with old nodes without it. + let v2 = SwapMsg::MakerPayment(SwapTxDataMsg::WithInstructions(PaymentWithInstructions { + data: MSG_DATA_INSTRUCTIONS.to_vec(), + next_step_instructions: MSG_DATA_INSTRUCTIONS.to_vec(), + })); + + let serialized = rmp_serde::to_vec(&v2).unwrap(); + + let deserialized: Result = + rmp_serde::from_read_ref(serialized.as_slice()); + + assert!(deserialized.is_err()); + } + fn utxo_activation_params(electrums: &[&str]) -> UtxoActivationParams { UtxoActivationParams { mode: UtxoRpcMode::Electrum { diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 2afbe34b25..b984d320aa 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -3,20 +3,20 @@ use super::check_balance::{check_base_coin_balance_for_swap, check_my_coin_balan use super::pubkey_banning::ban_pubkey_on_failed_swap; use super::swap_lock::{SwapLock, SwapLockOps}; use super::trade_preimage::{TradePreimageRequest, TradePreimageRpcError, TradePreimageRpcResult}; -use super::{broadcast_my_swap_status, broadcast_swap_message_every, check_other_coin_balance_for_swap, - dex_fee_amount_from_taker_coin, get_locked_amount, recv_swap_msg, swap_topic, - wait_for_maker_payment_conf_until, wait_for_taker_payment_conf_until, AtomicSwap, LockedAmount, - MySwapInfo, NegotiationDataMsg, NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, - SavedSwap, SavedSwapIo, SavedTradeFee, SwapConfirmationsSettings, SwapError, SwapMsg, SwapsContext, +use super::{broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_message_every, + check_other_coin_balance_for_swap, detect_secret_hash_algo, dex_fee_amount_from_taker_coin, + get_locked_amount, recv_swap_msg, swap_topic, tx_helper_topic, wait_for_maker_payment_conf_until, + wait_for_taker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, + NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, + SavedTradeFee, SecretHashAlgo, SwapConfirmationsSettings, SwapError, SwapMsg, SwapTxDataMsg, SwapsContext, TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; use crate::mm2::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_ordermatch::{MakerOrderBuilder, OrderConfirmationsSettings}; use crate::mm2::lp_price::fetch_swap_coins_price; -use crate::mm2::lp_swap::{broadcast_p2p_tx_msg, detect_secret_hash_algo, tx_helper_topic, SecretHashAlgo}; use crate::mm2::MM_VERSION; -use coins::{CanRefundHtlc, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, SearchForSwapTxSpendInput, TradeFee, - TradePreimageValue, TransactionEnum, ValidatePaymentInput}; +use coins::{CanRefundHtlc, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, PaymentInstructions, PaymentInstructionsErr, + SearchForSwapTxSpendInput, TradeFee, TradePreimageValue, TransactionEnum, ValidatePaymentInput}; use common::log::{debug, error, info, warn}; use common::{bits256, executor::Timer, now_ms, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::privkey::SerializableSecp256k1Keypair; @@ -173,6 +173,7 @@ pub struct MakerSwapMut { taker_payment_spend: Option, taker_payment_spend_confirmed: bool, maker_payment_refund: Option, + payment_instructions: Option, } #[cfg(test)] @@ -276,6 +277,9 @@ impl MakerSwap { } }, MakerSwapEvent::NegotiateFailed(err) => self.errors.lock().push(err), + MakerSwapEvent::MakerPaymentInstructionsReceived(instructions) => { + self.w().payment_instructions = Some(instructions) + }, MakerSwapEvent::TakerFeeValidated(tx) => self.w().taker_fee = Some(tx), MakerSwapEvent::TakerFeeValidateFailed(err) => self.errors.lock().push(err), MakerSwapEvent::MakerPaymentSent(tx) => self.w().maker_payment = Some(tx), @@ -362,6 +366,7 @@ impl MakerSwap { taker_payment_spend: None, maker_payment_refund: None, taker_payment_spend_confirmed: false, + payment_instructions: None, }), ctx, secret, @@ -405,6 +410,20 @@ impl MakerSwap { } } + async fn get_my_payment_data(&self) -> Result> { + // If maker payment is a lightning payment the payment hash will be sent in the message + // It's not really needed here unlike in TakerFee msg since the hash is included in the invoice/payment_instructions but it's kept for symmetry + let payment_data = self.r().maker_payment.as_ref().unwrap().tx_hex.0.clone(); + let instructions = self + .taker_coin + .payment_instructions( + &SecretHashAlgo::SHA256.hash_secret(self.secret.as_slice()), + &self.taker_amount, + ) + .await?; + Ok(SwapTxDataMsg::new(payment_data, instructions)) + } + async fn start(&self) -> Result<(Option, Vec), String> { // do not use self.r().data here as it is not initialized at this step yet let preimage_value = TradePreimageValue::Exact(self.maker_amount.clone()); @@ -634,7 +653,22 @@ impl MakerSwap { }, }; drop(send_abort_handle); - let taker_fee = match self.taker_coin.tx_enum_from_bytes(&payload) { + let mut swap_events = vec![]; + if let Some(instructions) = payload.instructions() { + match self + .maker_coin + .validate_instructions(instructions, &self.secret_hash(), self.maker_amount.clone()) + { + Ok(instructions) => swap_events.push(MakerSwapEvent::MakerPaymentInstructionsReceived(instructions)), + Err(e) => { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::TakerFeeValidateFailed(e.to_string().into()), + ])); + }, + }; + } + + let taker_fee = match self.taker_coin.tx_enum_from_bytes(payload.data()) { Ok(tx) => tx, Err(e) => { return Ok((Some(MakerSwapCommand::Finish), vec![ @@ -681,13 +715,12 @@ impl MakerSwap { } let fee_ident = TransactionIdentifier { - tx_hex: taker_fee.tx_hex().into(), + tx_hex: BytesJson::from(taker_fee.tx_hex()), tx_hash: hash, }; + swap_events.push(MakerSwapEvent::TakerFeeValidated(fee_ident)); - Ok((Some(MakerSwapCommand::SendPayment), vec![ - MakerSwapEvent::TakerFeeValidated(fee_ident), - ])) + Ok((Some(MakerSwapCommand::SendPayment), swap_events)) } async fn maker_payment(&self) -> Result<(Option, Vec), String> { @@ -726,6 +759,7 @@ impl MakerSwap { self.maker_amount.clone(), &self.r().data.maker_coin_swap_contract_address, &unique_data, + &self.r().payment_instructions, ); match payment_fut.compat().await { @@ -751,7 +785,7 @@ impl MakerSwap { info!("Maker payment tx {:02x}", tx_hash); let tx_ident = TransactionIdentifier { - tx_hex: transaction.tx_hex().into(), + tx_hex: BytesJson::from(transaction.tx_hex()), tx_hash, }; @@ -761,8 +795,18 @@ impl MakerSwap { } async fn wait_for_taker_payment(&self) -> Result<(Option, Vec), String> { - let maker_payment_hex = self.r().maker_payment.as_ref().unwrap().tx_hex.0.clone(); - let msg = SwapMsg::MakerPayment(maker_payment_hex); + let payment_data_msg = match self.get_my_payment_data().await { + Ok(data) => data, + Err(e) => { + return Ok((Some(MakerSwapCommand::RefundMakerPayment), vec![ + MakerSwapEvent::MakerPaymentDataSendFailed(e.to_string().into()), + MakerSwapEvent::MakerPaymentWaitRefundStarted { + wait_until: self.wait_refund_until(), + }, + ])); + }, + }; + let msg = SwapMsg::MakerPayment(payment_data_msg); let abort_send_handle = broadcast_swap_message_every(self.ctx.clone(), swap_topic(&self.uuid), msg, 600., self.p2p_privkey); @@ -794,6 +838,7 @@ impl MakerSwap { &self.uuid, wait_duration, ); + // Todo: taker_payment should be a message on lightning network not a swap message let payload = match recv_fut.await { Ok(p) => p, Err(e) => { @@ -824,7 +869,7 @@ impl MakerSwap { let tx_hash = taker_payment.tx_hash(); info!("Taker payment tx {:02x}", tx_hash); let tx_ident = TransactionIdentifier { - tx_hex: taker_payment.tx_hex().into(), + tx_hex: BytesJson::from(taker_payment.tx_hex()), tx_hash, }; @@ -954,7 +999,7 @@ impl MakerSwap { let tx_hash = transaction.tx_hash(); info!("Taker payment spend tx {:02x}", tx_hash); let tx_ident = TransactionIdentifier { - tx_hex: transaction.tx_hex().into(), + tx_hex: BytesJson::from(transaction.tx_hex()), tx_hash, }; @@ -1054,7 +1099,7 @@ impl MakerSwap { let tx_hash = transaction.tx_hash(); info!("Maker payment refund tx {:02x}", tx_hash); let tx_ident = TransactionIdentifier { - tx_hex: transaction.tx_hex().into(), + tx_hex: BytesJson::from(transaction.tx_hex()), tx_hash, }; @@ -1228,7 +1273,7 @@ impl MakerSwap { let maybe_maker_payment = self.r().maker_payment.clone(); let maker_payment = match maybe_maker_payment { - Some(tx) => tx.tx_hex.0.clone(), + Some(tx) => tx.tx_hex.0, None => { let maybe_maker_payment = try_s!( self.maker_coin @@ -1385,6 +1430,7 @@ pub enum MakerSwapEvent { StartFailed(SwapError), Negotiated(TakerNegotiationData), NegotiateFailed(SwapError), + MakerPaymentInstructionsReceived(PaymentInstructions), TakerFeeValidated(TransactionIdentifier), TakerFeeValidateFailed(SwapError), MakerPaymentSent(TransactionIdentifier), @@ -1414,6 +1460,7 @@ impl MakerSwapEvent { MakerSwapEvent::StartFailed(_) => "Start failed...".to_owned(), MakerSwapEvent::Negotiated(_) => "Negotiated...".to_owned(), MakerSwapEvent::NegotiateFailed(_) => "Negotiate failed...".to_owned(), + MakerSwapEvent::MakerPaymentInstructionsReceived(_) => "Maker payment instructions received...".to_owned(), MakerSwapEvent::TakerFeeValidated(_) => "Taker fee validated...".to_owned(), MakerSwapEvent::TakerFeeValidateFailed(_) => "Taker fee validate failed...".to_owned(), MakerSwapEvent::MakerPaymentSent(_) => "Maker payment sent...".to_owned(), @@ -1483,6 +1530,7 @@ impl MakerSavedEvent { MakerSwapEvent::Started(_) => Some(MakerSwapCommand::Negotiate), MakerSwapEvent::StartFailed(_) => Some(MakerSwapCommand::Finish), MakerSwapEvent::Negotiated(_) => Some(MakerSwapCommand::WaitForTakerFee), + MakerSwapEvent::MakerPaymentInstructionsReceived(_) => Some(MakerSwapCommand::WaitForTakerFee), MakerSwapEvent::NegotiateFailed(_) => Some(MakerSwapCommand::Finish), MakerSwapEvent::TakerFeeValidated(_) => Some(MakerSwapCommand::SendPayment), MakerSwapEvent::TakerFeeValidateFailed(_) => Some(MakerSwapCommand::Finish), diff --git a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs index 1374155cfa..eb54835f1f 100644 --- a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs +++ b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs @@ -260,6 +260,8 @@ fn convert_taker_to_maker_events( | TakerSwapEvent::StartFailed(_) | TakerSwapEvent::Negotiated(_) | TakerSwapEvent::NegotiateFailed(_) + // Todo: This may be used when implementing convert_taker_to_maker_events for lightning + | TakerSwapEvent::TakerPaymentInstructionsReceived(_) | TakerSwapEvent::MakerPaymentWaitConfirmStarted | TakerSwapEvent::MakerPaymentValidatedAndConfirmed | TakerSwapEvent::MakerPaymentSpent(_) @@ -377,12 +379,9 @@ async fn recreate_taker_swap(ctx: MmArc, maker_swap: MakerSavedSwap) -> Recreate // Then we can continue to process success Maker events. let wait_refund_until = negotiated_event.taker_payment_locktime + 3700; - taker_swap.events.extend(convert_maker_to_taker_events( - event_it, - maker_coin, - secret_hash, - wait_refund_until, - )); + taker_swap + .events + .extend(convert_maker_to_taker_events(event_it, maker_coin, secret_hash, wait_refund_until).await); Ok(taker_swap) } @@ -392,7 +391,7 @@ async fn recreate_taker_swap(ctx: MmArc, maker_swap: MakerSavedSwap) -> Recreate /// since they are used outside of this function to generate `TakerSwap` and the initial [`TakerSwapEvent::Started`] and [`TakerSwapEvent::Negotiated`] events. /// /// The `maker_coin` and `secret_hash` function arguments are used to extract a secret from `TakerPaymentSpent`. -fn convert_maker_to_taker_events( +async fn convert_maker_to_taker_events( event_it: impl Iterator, maker_coin: MmCoinEnum, secret_hash: BytesJson, @@ -422,6 +421,7 @@ fn convert_maker_to_taker_events( return events; }, MakerSwapEvent::MakerPaymentSent(tx_ident) => { + // Todo: check this if a new event is added for other side instructions, also look in the mirror maker_swap.rs push_event!(TakerSwapEvent::MakerPaymentReceived(tx_ident)); // Please note we have not to push `MakerPaymentValidatedAndConfirmed` since we could actually decline it. push_event!(TakerSwapEvent::MakerPaymentWaitConfirmStarted); @@ -448,7 +448,7 @@ fn convert_maker_to_taker_events( return events; }, MakerSwapEvent::TakerPaymentSpent(tx_ident) => { - let secret = match maker_coin.extract_secret(&secret_hash.0, &tx_ident.tx_hex) { + let secret = match maker_coin.extract_secret(&secret_hash.0, &tx_ident.tx_hex).await { Ok(secret) => H256Json::from(secret.as_slice()), Err(e) => { push_event!(TakerSwapEvent::TakerPaymentWaitForSpendFailed(ERRL!("{}", e).into())); @@ -466,6 +466,8 @@ fn convert_maker_to_taker_events( | MakerSwapEvent::StartFailed(_) | MakerSwapEvent::Negotiated(_) | MakerSwapEvent::NegotiateFailed(_) + // Todo: This may be used when implementing convert_maker_to_taker_events for lightning + | MakerSwapEvent::MakerPaymentInstructionsReceived(_) | MakerSwapEvent::TakerPaymentWaitConfirmStarted | MakerSwapEvent::TakerPaymentValidatedAndConfirmed | MakerSwapEvent::TakerPaymentSpendConfirmStarted @@ -527,7 +529,7 @@ mod tests { fn test_recreate_taker_swap() { TestCoin::extract_secret.mock_safe(|_coin, _secret_hash, _spend_tx| { let secret = hex::decode("23a6bb64bc0ab2cc14cb84277d8d25134b814e5f999c66e578c9bba3c5e2d3a4").unwrap(); - MockResult::Return(Ok(secret)) + MockResult::Return(Box::pin(async move { Ok(secret) })) }); let maker_saved_swap: MakerSavedSwap = diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index 44de441776..a20200036d 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -10,6 +10,7 @@ use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_libp2p::{decode_signed, pub_sub_topic, TopicPrefix}; use mm2_number::BigDecimal; +use rpc::v1::types::Bytes as BytesJson; use std::cmp::min; use std::sync::Arc; use uuid::Uuid; @@ -295,13 +296,14 @@ impl State for WaitForTakerPaymentSpend { let tx_hash = tx.tx_hash(); info!("Taker payment spend tx {:02x}", tx_hash); let tx_ident = TransactionIdentifier { - tx_hex: tx.tx_hex().into(), + tx_hex: BytesJson::from(tx.tx_hex()), tx_hash, }; let secret = match watcher_ctx .taker_coin .extract_secret(&watcher_ctx.data.secret_hash, &tx_ident.tx_hex.0) + .await { Ok(bytes) => H256Json::from(bytes.as_slice()), Err(err) => { diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 21cab46e04..df5ff1d6bc 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -9,15 +9,15 @@ use super::{broadcast_my_swap_status, broadcast_swap_message, broadcast_swap_mes get_locked_amount, recv_swap_msg, swap_topic, wait_for_maker_payment_conf_until, wait_for_taker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, - SavedTradeFee, SwapConfirmationsSettings, SwapError, SwapMsg, SwapsContext, TransactionIdentifier, - WAIT_CONFIRM_INTERVAL}; + SavedTradeFee, SwapConfirmationsSettings, SwapError, SwapMsg, SwapTxDataMsg, SwapsContext, + TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_ordermatch::{MatchBy, OrderConfirmationsSettings, TakerAction, TakerOrderBuilder}; use crate::mm2::lp_price::fetch_swap_coins_price; use crate::mm2::lp_swap::{broadcast_p2p_tx_msg, tx_helper_topic, TakerSwapWatcherData}; use crate::mm2::MM_VERSION; -use coins::{lp_coinfind, CanRefundHtlc, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, SearchForSwapTxSpendInput, - TradeFee, TradePreimageValue, ValidatePaymentInput}; +use coins::{lp_coinfind, CanRefundHtlc, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, PaymentInstructions, + PaymentInstructionsErr, SearchForSwapTxSpendInput, TradeFee, TradePreimageValue, ValidatePaymentInput}; use common::executor::Timer; use common::log::{debug, error, info, warn}; use common::{bits256, now_ms, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -142,6 +142,7 @@ impl TakerSavedEvent { TakerSwapEvent::NegotiateFailed(_) => Some(TakerSwapCommand::Finish), TakerSwapEvent::TakerFeeSent(_) => Some(TakerSwapCommand::WaitForMakerPayment), TakerSwapEvent::TakerFeeSendFailed(_) => Some(TakerSwapCommand::Finish), + TakerSwapEvent::TakerPaymentInstructionsReceived(_) => Some(TakerSwapCommand::ValidateMakerPayment), TakerSwapEvent::MakerPaymentReceived(_) => Some(TakerSwapCommand::ValidateMakerPayment), TakerSwapEvent::MakerPaymentWaitConfirmStarted => Some(TakerSwapCommand::ValidateMakerPayment), TakerSwapEvent::MakerPaymentValidatedAndConfirmed => Some(TakerSwapCommand::SendTakerPayment), @@ -489,6 +490,7 @@ pub struct TakerSwapMut { taker_payment_refund: Option, secret_hash: BytesJson, secret: H256Json, + payment_instructions: Option, } #[cfg(test)] @@ -566,6 +568,7 @@ pub enum TakerSwapEvent { NegotiateFailed(SwapError), TakerFeeSent(TransactionIdentifier), TakerFeeSendFailed(SwapError), + TakerPaymentInstructionsReceived(PaymentInstructions), MakerPaymentReceived(TransactionIdentifier), MakerPaymentWaitConfirmStarted, MakerPaymentValidatedAndConfirmed, @@ -595,6 +598,7 @@ impl TakerSwapEvent { TakerSwapEvent::NegotiateFailed(_) => "Negotiate failed...".to_owned(), TakerSwapEvent::TakerFeeSent(_) => "Taker fee sent...".to_owned(), TakerSwapEvent::TakerFeeSendFailed(_) => "Taker fee send failed...".to_owned(), + TakerSwapEvent::TakerPaymentInstructionsReceived(_) => "Taker payment instructions received...".to_owned(), TakerSwapEvent::MakerPaymentReceived(_) => "Maker payment received...".to_owned(), TakerSwapEvent::MakerPaymentWaitConfirmStarted => "Maker payment wait confirm started...".to_owned(), TakerSwapEvent::MakerPaymentValidatedAndConfirmed => "Maker payment validated and confirmed...".to_owned(), @@ -713,6 +717,9 @@ impl TakerSwap { TakerSwapEvent::NegotiateFailed(err) => self.errors.lock().push(err), TakerSwapEvent::TakerFeeSent(tx) => self.w().taker_fee = Some(tx), TakerSwapEvent::TakerFeeSendFailed(err) => self.errors.lock().push(err), + TakerSwapEvent::TakerPaymentInstructionsReceived(instructions) => { + self.w().payment_instructions = Some(instructions) + }, TakerSwapEvent::MakerPaymentReceived(tx) => self.w().maker_payment = Some(tx), TakerSwapEvent::MakerPaymentWaitConfirmStarted => (), TakerSwapEvent::MakerPaymentValidatedAndConfirmed => { @@ -805,6 +812,7 @@ impl TakerSwap { taker_payment_refund: None, secret_hash: BytesJson::default(), secret: H256Json::default(), + payment_instructions: None, }), ctx, #[cfg(test)] @@ -841,6 +849,28 @@ impl TakerSwap { } } + /// # Panic + /// + /// Panic if taker_fee of [`TakerSwapMut`] is [`Option::None`]. + async fn get_taker_fee_data(&self) -> Result> { + // If taker fee is a lightning payment the payment hash will be sent in the message + let taker_fee_data = self + .r() + .taker_fee + .as_ref() + .expect("TakerSwapMut::taker_fee must be some value to use get_taker_fee_data") + .tx_hex + .0 + .clone(); + let secret_hash = self.r().secret_hash.0.clone(); + let maker_amount = self.maker_amount.clone().into(); + let instructions = self + .maker_coin + .payment_instructions(&secret_hash, &maker_amount) + .await?; + Ok(SwapTxDataMsg::new(taker_fee_data, instructions)) + } + async fn start(&self) -> Result<(Option, Vec), String> { // do not use self.r().data here as it is not initialized at this step yet let stage = FeeApproxStage::StartSwap; @@ -1124,7 +1154,7 @@ impl TakerSwap { let tx_hash = transaction.tx_hash(); info!("Taker fee tx hash {:02x}", tx_hash); let tx_ident = TransactionIdentifier { - tx_hex: transaction.tx_hex().into(), + tx_hex: BytesJson::from(transaction.tx_hex()), tx_hash, }; @@ -1135,8 +1165,17 @@ impl TakerSwap { async fn wait_for_maker_payment(&self) -> Result<(Option, Vec), String> { const MAKER_PAYMENT_WAIT_TIMEOUT: u64 = 600; - let tx_hex = self.r().taker_fee.as_ref().unwrap().tx_hex.0.clone(); - let msg = SwapMsg::TakerFee(tx_hex); + + let payment_data_msg = match self.get_taker_fee_data().await { + Ok(data) => data, + Err(e) => { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::MakerPaymentValidateFailed(e.to_string().into()), + ])) + }, + }; + + let msg = SwapMsg::TakerFee(payment_data_msg); let abort_send_handle = broadcast_swap_message_every( self.ctx.clone(), swap_topic(&self.uuid), @@ -1162,7 +1201,23 @@ impl TakerSwap { }, }; drop(abort_send_handle); - let maker_payment = match self.maker_coin.tx_enum_from_bytes(&payload) { + let mut swap_events = vec![]; + if let Some(instructions) = payload.instructions() { + match self.taker_coin.validate_instructions( + instructions, + &self.r().secret_hash.0, + self.taker_amount.clone().into(), + ) { + Ok(instructions) => swap_events.push(TakerSwapEvent::TakerPaymentInstructionsReceived(instructions)), + Err(e) => { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::MakerPaymentValidateFailed(e.to_string().into()), + ])); + }, + } + } + + let maker_payment = match self.maker_coin.tx_enum_from_bytes(payload.data()) { Ok(p) => p, Err(e) => { return Ok((Some(TakerSwapCommand::Finish), vec![ @@ -1176,14 +1231,14 @@ impl TakerSwap { let tx_hash = maker_payment.tx_hash(); info!("Got maker payment {:02x}", tx_hash); let tx_ident = TransactionIdentifier { - tx_hex: maker_payment.tx_hex().into(), + tx_hex: BytesJson::from(maker_payment.tx_hex()), tx_hash, }; - Ok((Some(TakerSwapCommand::ValidateMakerPayment), vec![ - TakerSwapEvent::MakerPaymentReceived(tx_ident), - TakerSwapEvent::MakerPaymentWaitConfirmStarted, - ])) + swap_events.push(TakerSwapEvent::MakerPaymentReceived(tx_ident)); + swap_events.push(TakerSwapEvent::MakerPaymentWaitConfirmStarted); + + Ok((Some(TakerSwapCommand::ValidateMakerPayment), swap_events)) } async fn validate_maker_payment(&self) -> Result<(Option, Vec), String> { @@ -1295,6 +1350,7 @@ impl TakerSwap { self.taker_amount.to_decimal(), &self.r().data.taker_coin_swap_contract_address, &unique_data, + &self.r().payment_instructions, ); match payment_fut.compat().await { @@ -1317,14 +1373,18 @@ impl TakerSwap { }; let tx_hash = transaction.tx_hash(); + let tx_hex = BytesJson::from(transaction.tx_hex()); info!("Taker payment tx hash {:02x}", tx_hash); let tx_ident = TransactionIdentifier { - tx_hex: transaction.tx_hex().into(), + tx_hex: tx_hex.clone(), tx_hash, }; let mut swap_events = vec![TakerSwapEvent::TakerPaymentSent(tx_ident)]; - if self.ctx.use_watchers() { + if self.ctx.use_watchers() + && self.taker_coin.is_supported_by_watchers() + && self.maker_coin.is_supported_by_watchers() + { let taker_spends_maker_payment_fut = self.taker_coin.create_taker_spends_maker_payment_preimage( &self.r().maker_payment.as_ref().unwrap().tx_hex, self.maker_payment_lock.load(Ordering::Relaxed) as u32, @@ -1377,10 +1437,16 @@ impl TakerSwap { } async fn wait_for_taker_payment_spend(&self) -> Result<(Option, Vec), String> { - const WAIT_FOR_TAKER_PAYMENT_INTERVAL: f64 = 600.; + const BROADCAST_SWAP_MESSAGE_INTERVAL: f64 = 600.; + let tx_hex = self.r().taker_payment.as_ref().unwrap().tx_hex.0.clone(); let mut watcher_broadcast_abort_handle = None; - if self.ctx.use_watchers() { + // Watchers cannot be used for lightning swaps for now + // Todo: Check if watchers can work in some cases with lightning and implement it if it's possible, this part will probably work if only the taker is lightning since the preimage is available + if self.ctx.use_watchers() + && self.taker_coin.is_supported_by_watchers() + && self.maker_coin.is_supported_by_watchers() + { if let (Some(taker_spends_maker_payment), Some(taker_refunds_payment)) = ( self.r().taker_spends_maker_payment_preimage.clone(), self.r().taker_refunds_payment.clone(), @@ -1392,17 +1458,19 @@ impl TakerSwap { self.ctx.clone(), watcher_topic(&self.r().data.taker_coin), swpmsg_watcher, - WAIT_FOR_TAKER_PAYMENT_INTERVAL, + BROADCAST_SWAP_MESSAGE_INTERVAL, self.p2p_privkey, )); } } + + // Todo: taker_payment should be a message on lightning network not a swap message let msg = SwapMsg::TakerPayment(tx_hex); let send_abort_handle = broadcast_swap_message_every( self.ctx.clone(), swap_topic(&self.uuid), msg, - WAIT_FOR_TAKER_PAYMENT_INTERVAL, + BROADCAST_SWAP_MESSAGE_INTERVAL, self.p2p_privkey, ); @@ -1452,13 +1520,12 @@ impl TakerSwap { let tx_hash = tx.tx_hash(); info!("Taker payment spend tx {:02x}", tx_hash); let tx_ident = TransactionIdentifier { - tx_hex: tx.tx_hex().into(), + tx_hex: BytesJson::from(tx.tx_hex()), tx_hash, }; - let secret = match self - .taker_coin - .extract_secret(&self.r().secret_hash.0, &tx_ident.tx_hex.0) - { + + let secret_hash = self.r().secret_hash.clone(); + let secret = match self.taker_coin.extract_secret(&secret_hash.0, &tx_ident.tx_hex).await { Ok(bytes) => H256Json::from(bytes.as_slice()), Err(e) => { return Ok((Some(TakerSwapCommand::Finish), vec![ @@ -1519,7 +1586,7 @@ impl TakerSwap { let tx_hash = transaction.tx_hash(); info!("Maker payment spend tx {:02x}", tx_hash); let tx_ident = TransactionIdentifier { - tx_hex: transaction.tx_hex().into(), + tx_hex: BytesJson::from(transaction.tx_hex()), tx_hash, }; @@ -1549,7 +1616,7 @@ impl TakerSwap { } let refund_fut = self.taker_coin.send_taker_refunds_payment( - &self.r().taker_payment.clone().unwrap().tx_hex.0, + &self.r().taker_payment.clone().unwrap().tx_hex, self.r().data.taker_payment_lock as u32, &*self.r().other_taker_coin_htlc_pub, &self.r().secret_hash.0, @@ -1585,7 +1652,7 @@ impl TakerSwap { let tx_hash = transaction.tx_hash(); info!("Taker refund tx hash {:02x}", tx_hash); let tx_ident = TransactionIdentifier { - tx_hex: transaction.tx_hex().into(), + tx_hex: BytesJson::from(transaction.tx_hex()), tx_hash, }; @@ -1738,7 +1805,7 @@ impl TakerSwap { let maybe_taker_payment = self.r().taker_payment.clone(); let taker_payment = match maybe_taker_payment { - Some(tx) => tx.tx_hex.0.clone(), + Some(tx) => tx.tx_hex.0, None => { let maybe_sent = try_s!( self.taker_coin @@ -1755,6 +1822,7 @@ impl TakerSwap { .await ); match maybe_sent { + // Todo: Refactor this when implementing recover_funds for lightning swaps Some(tx) => tx.tx_hex(), None => return ERR!("Taker payment is not found, swap is not recoverable"), } @@ -1817,7 +1885,9 @@ impl TakerSwap { Some(spend) => match spend { FoundSwapTxSpend::Spent(tx) => { check_maker_payment_is_not_spent!(); - let secret = try_s!(self.taker_coin.extract_secret(&self.r().secret_hash.0, &tx.tx_hex())); + let secret_hash = self.r().secret_hash.clone(); + let tx_hex = tx.tx_hex(); + let secret = try_s!(self.taker_coin.extract_secret(&secret_hash.0, &tx_hex).await); let fut = self.maker_coin.send_taker_spends_maker_payment( &maker_payment, @@ -2385,7 +2455,7 @@ mod taker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - TestCoin::extract_secret.mock_safe(|_, _, _| MockResult::Return(Ok(vec![]))); + TestCoin::extract_secret.mock_safe(|_, _, _| MockResult::Return(Box::pin(async move { Ok(vec![]) }))); static mut MY_PAYMENT_SENT_CALLED: bool = false; TestCoin::check_if_my_payment_sent.mock_safe(|_, _, _, _, _, _, _, _| { @@ -2500,7 +2570,7 @@ mod taker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - TestCoin::extract_secret.mock_safe(|_, _, _| MockResult::Return(Ok(vec![]))); + TestCoin::extract_secret.mock_safe(|_, _, _| MockResult::Return(Box::pin(async move { Ok(vec![]) }))); static mut SEARCH_TX_SPEND_CALLED: bool = false; TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _| { diff --git a/mm2src/mm2_main/src/mm2_bin.rs b/mm2src/mm2_main/src/mm2_bin.rs index 81ed8097ad..0b4512f806 100644 --- a/mm2src/mm2_main/src/mm2_bin.rs +++ b/mm2src/mm2_main/src/mm2_bin.rs @@ -10,7 +10,6 @@ #[macro_use] extern crate gstuff; #[macro_use] extern crate serde_json; #[macro_use] extern crate serde_derive; -#[macro_use] extern crate serialization_derive; #[macro_use] extern crate ser_error_derive; #[path = "mm2.rs"] mod mm2; diff --git a/mm2src/mm2_main/src/mm2_lib.rs b/mm2src/mm2_main/src/mm2_lib.rs index b434c474a7..0afd80b500 100644 --- a/mm2src/mm2_main/src/mm2_lib.rs +++ b/mm2src/mm2_main/src/mm2_lib.rs @@ -11,7 +11,6 @@ #[macro_use] extern crate gstuff; #[macro_use] extern crate serde_json; #[macro_use] extern crate serde_derive; -#[macro_use] extern crate serialization_derive; #[macro_use] extern crate ser_error_derive; #[path = "mm2.rs"] mod mm2; diff --git a/mm2src/mm2_main/src/mm2_tests.rs b/mm2src/mm2_main/src/mm2_tests.rs index 731790847f..4a9ce02423 100644 --- a/mm2src/mm2_main/src/mm2_tests.rs +++ b/mm2src/mm2_main/src/mm2_tests.rs @@ -43,13 +43,11 @@ async fn enable_z_coin(mm: &MarketMakerIt, coin: &str) -> CoinActivationResult { let status = init_z_coin_status(mm, init.result.task_id).await; let status: RpcV2Response = json::from_value(status).unwrap(); - if let InitZcoinStatus::Ok(rpc_result) = status.result { - match rpc_result { - MmRpcResult::Ok { result } => break result, - MmRpcResult::Err(e) => panic!("{} initialization error {:?}", coin, e), - } + match status.result { + InitZcoinStatus::Ok(result) => break result, + InitZcoinStatus::Error(e) => panic!("{} initialization error {:?}", coin, e), + _ => Timer::sleep(1.).await, } - Timer::sleep(1.).await; } } @@ -196,15 +194,11 @@ async fn enable_z_coin_light( let status = init_z_coin_status(mm, init.result.task_id).await; println!("Status {}", json::to_string(&status).unwrap()); let status: RpcV2Response = json::from_value(status).unwrap(); - if let InitZcoinStatus::Ok(rpc_result) = status.result { - match rpc_result { - MmRpcResult::Ok { result } => { - break result; - }, - MmRpcResult::Err(e) => panic!("{} initialization error {:?}", coin, e), - } + match status.result { + InitZcoinStatus::Ok(result) => break result, + InitZcoinStatus::Error(e) => panic!("{} initialization error {:?}", coin, e), + _ => Timer::sleep(1.).await, } - Timer::sleep(1.).await; } } @@ -226,13 +220,11 @@ async fn enable_utxo_v2_electrum( let status = init_utxo_status(mm, init.result.task_id).await; let status: RpcV2Response = json::from_value(status).unwrap(); log!("init_utxo_status: {:?}", status); - if let InitUtxoStatus::Ready(rpc_result) = status.result { - match rpc_result { - MmRpcResult::Ok { result } => break result, - MmRpcResult::Err(e) => panic!("{} initialization error {:?}", coin, e), - } + match status.result { + InitUtxoStatus::Ok(result) => break result, + InitUtxoStatus::Error(e) => panic!("{} initialization error {:?}", coin, e), + _ => Timer::sleep(1.).await, } - Timer::sleep(1.).await; } } diff --git a/mm2src/mm2_main/src/mm2_tests/lightning_tests.rs b/mm2src/mm2_main/src/mm2_tests/lightning_tests.rs index 19711a08fc..15561795df 100644 --- a/mm2src/mm2_main/src/mm2_tests/lightning_tests.rs +++ b/mm2src/mm2_main/src/mm2_tests/lightning_tests.rs @@ -57,7 +57,9 @@ fn start_lightning_nodes(enable_0_confs: bool) -> (MarketMakerIt, MarketMakerIt, } } } - } + }, + {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"required_confirmations":0,"protocol":{"type":"UTXO"}}, + {"coin":"MORTY","asset":"MORTY","rpcport":11608,"txversion":4,"overwintered":1,"required_confirmations":0,"protocol":{"type":"UTXO"}} ]); let mm_node_1 = MarketMakerIt::start( @@ -69,13 +71,14 @@ fn start_lightning_nodes(enable_0_confs: bool) -> (MarketMakerIt, MarketMakerIt, "passphrase": node_1_seed.to_string(), "coins": coins, "rpc_password": "pass", + "i_am_seed": true, }), "pass".into(), local_start!("bob"), ) .unwrap(); let (_dump_log, _dump_dashboard) = mm_node_1.mm_dump(); - log!("bob log path: {}", mm_node_1.log_path.display()); + log!("Node 1 log path: {}", mm_node_1.log_path.display()); let electrum = block_on(enable_electrum(&mm_node_1, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); log!("Node 1 tBTC address: {}", electrum.address); @@ -92,13 +95,14 @@ fn start_lightning_nodes(enable_0_confs: bool) -> (MarketMakerIt, MarketMakerIt, "passphrase": node_2_seed.to_string(), "coins": coins, "rpc_password": "pass", + "seednodes": [mm_node_1.my_seed_addr()], }), "pass".into(), local_start!("alice"), ) .unwrap(); let (_dump_log, _dump_dashboard) = mm_node_2.mm_dump(); - log!("alice log path: {}", mm_node_2.log_path.display()); + log!("Node 2 log path: {}", mm_node_2.log_path.display()); let electrum = block_on(enable_electrum(&mm_node_2, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); log!("Node 2 tBTC address: {}", electrum.address); @@ -188,21 +192,25 @@ fn test_enable_lightning() { #[test] #[cfg(not(target_arch = "wasm32"))] -fn test_connect_to_lightning_node() { +fn test_connect_to_node() { let (mm_node_1, mm_node_2, node_1_id, _) = start_lightning_nodes(false); let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip.to_string()); let connect = block_on(mm_node_2.rpc(&json! ({ "userpass": mm_node_2.userpass, "mmrpc": "2.0", - "method": "connect_to_lightning_node", + "method": "lightning::nodes::connect_to_node", "params": { "coin": "tBTC-TEST-lightning", "node_address": node_1_address, }, }))) .unwrap(); - assert!(connect.0.is_success(), "!connect_to_lightning_node: {}", connect.1); + assert!( + connect.0.is_success(), + "!lightning::nodes::connect_to_node: {}", + connect.1 + ); let connect_res: Json = json::from_str(&connect.1).unwrap(); let expected = format!("Connected successfully to node : {}", node_1_address); assert_eq!(connect_res["result"], expected); @@ -222,7 +230,7 @@ fn test_open_channel() { let open_channel = block_on(mm_node_2.rpc(&json! ({ "userpass": mm_node_2.userpass, "mmrpc": "2.0", - "method": "open_channel", + "method": "lightning::channels::open_channel", "params": { "coin": "tBTC-TEST-lightning", "node_address": node_1_address, @@ -233,14 +241,18 @@ fn test_open_channel() { }, }))) .unwrap(); - assert!(open_channel.0.is_success(), "!open_channel: {}", open_channel.1); + assert!( + open_channel.0.is_success(), + "!lightning::channels::open_channel: {}", + open_channel.1 + ); block_on(mm_node_2.wait_for_log(60., |log| log.contains("Transaction broadcasted successfully"))).unwrap(); let list_channels_node_1 = block_on(mm_node_1.rpc(&json! ({ "userpass": mm_node_1.userpass, "mmrpc": "2.0", - "method": "list_open_channels_by_filter", + "method": "lightning::channels::list_open_channels_by_filter", "params": { "coin": "tBTC-TEST-lightning", }, @@ -248,7 +260,7 @@ fn test_open_channel() { .unwrap(); assert!( list_channels_node_1.0.is_success(), - "!list_channels: {}", + "!lightning::channels::list_open_channels_by_filter: {}", list_channels_node_1.1 ); let list_channels_node_1_res: Json = json::from_str(&list_channels_node_1.1).unwrap(); @@ -269,7 +281,7 @@ fn test_open_channel() { let list_channels_node_2 = block_on(mm_node_2.rpc(&json! ({ "userpass": mm_node_2.userpass, "mmrpc": "2.0", - "method": "list_open_channels_by_filter", + "method": "lightning::channels::list_open_channels_by_filter", "params": { "coin": "tBTC-TEST-lightning", }, @@ -277,7 +289,7 @@ fn test_open_channel() { .unwrap(); assert!( list_channels_node_2.0.is_success(), - "!list_channels: {}", + "!lightning::channels::list_open_channels_by_filter: {}", list_channels_node_2.1 ); let list_channels_node_2_res: Json = json::from_str(&list_channels_node_2.1).unwrap(); @@ -310,19 +322,23 @@ fn test_send_payment() { let add_trusted_node = block_on(mm_node_1.rpc(&json! ({ "userpass": mm_node_1.userpass, "mmrpc": "2.0", - "method": "add_trusted_node", + "method": "lightning::nodes::add_trusted_node", "params": { "coin": "tBTC-TEST-lightning", "node_id": node_2_id }, }))) .unwrap(); - assert!(add_trusted_node.0.is_success(), "!open_channel: {}", add_trusted_node.1); + assert!( + add_trusted_node.0.is_success(), + "!lightning::nodes::add_trusted_node: {}", + add_trusted_node.1 + ); let open_channel = block_on(mm_node_2.rpc(&json! ({ "userpass": mm_node_2.userpass, "mmrpc": "2.0", - "method": "open_channel", + "method": "lightning::channels::open_channel", "params": { "coin": "tBTC-TEST-lightning", "node_address": node_1_address, @@ -333,14 +349,18 @@ fn test_send_payment() { }, }))) .unwrap(); - assert!(open_channel.0.is_success(), "!open_channel: {}", open_channel.1); + assert!( + open_channel.0.is_success(), + "!lightning::channels::open_channel: {}", + open_channel.1 + ); block_on(mm_node_2.wait_for_log(60., |log| log.contains("Received message ChannelReady"))).unwrap(); let send_payment = block_on(mm_node_2.rpc(&json! ({ "userpass": mm_node_2.userpass, "mmrpc": "2.0", - "method": "send_payment", + "method": "lightning::payments::send_payment", "params": { "coin": "tBTC-TEST-lightning", "payment": { @@ -352,7 +372,11 @@ fn test_send_payment() { }, }))) .unwrap(); - assert!(send_payment.0.is_success(), "!send_payment: {}", send_payment.1); + assert!( + send_payment.0.is_success(), + "!lightning::payments::send_payment: {}", + send_payment.1 + ); let send_payment_res: Json = json::from_str(&send_payment.1).unwrap(); log!("send_payment_res {:?}", send_payment_res); @@ -364,7 +388,7 @@ fn test_send_payment() { let get_payment_details = block_on(mm_node_2.rpc(&json! ({ "userpass": mm_node_2.userpass, "mmrpc": "2.0", - "method": "get_payment_details", + "method": "lightning::payments::get_payment_details", "params": { "coin": "tBTC-TEST-lightning", "payment_hash": payment_hash @@ -373,7 +397,7 @@ fn test_send_payment() { .unwrap(); assert!( get_payment_details.0.is_success(), - "!get_payment_details: {}", + "!lightning::payments::get_payment_details: {}", get_payment_details.1 ); @@ -387,7 +411,7 @@ fn test_send_payment() { let get_payment_details = block_on(mm_node_1.rpc(&json! ({ "userpass": mm_node_1.userpass, "mmrpc": "2.0", - "method": "get_payment_details", + "method": "lightning::payments::get_payment_details", "params": { "coin": "tBTC-TEST-lightning", "payment_hash": payment_hash @@ -396,7 +420,7 @@ fn test_send_payment() { .unwrap(); assert!( get_payment_details.0.is_success(), - "!get_payment_details: {}", + "!lightning::payments::get_payment_details: {}", get_payment_details.1 ); @@ -410,6 +434,119 @@ fn test_send_payment() { block_on(mm_node_2.stop()).unwrap(); } +#[test] +// This test is ignored because it requires refilling the tBTC and RICK addresses with test coins periodically. +#[ignore] +#[cfg(not(target_arch = "wasm32"))] +fn test_lightning_taker_swap() { + let (mut mm_node_1, mut mm_node_2, node_1_id, node_2_id) = start_lightning_nodes(true); + let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip.to_string()); + + let add_trusted_node = block_on(mm_node_1.rpc(&json! ({ + "userpass": mm_node_1.userpass, + "mmrpc": "2.0", + "method": "lightning::nodes::add_trusted_node", + "params": { + "coin": "tBTC-TEST-lightning", + "node_id": node_2_id + }, + }))) + .unwrap(); + assert!( + add_trusted_node.0.is_success(), + "!lightning::nodes::add_trusted_node: {}", + add_trusted_node.1 + ); + + let open_channel = block_on(mm_node_2.rpc(&json! ({ + "userpass": mm_node_2.userpass, + "mmrpc": "2.0", + "method": "lightning::channels::open_channel", + "params": { + "coin": "tBTC-TEST-lightning", + "node_address": node_1_address, + "amount": { + "type": "Exact", + "value": 0.0002, + }, + }, + }))) + .unwrap(); + assert!( + open_channel.0.is_success(), + "!lightning::channels::open_channel: {}", + open_channel.1 + ); + + block_on(mm_node_2.wait_for_log(60., |log| log.contains("Received message ChannelReady"))).unwrap(); + + // Enable coins on mm_node_1 side. Print the replies in case we need the "address". + log!( + "enable_coins (mm_node_1): {:?}", + block_on(enable_coins_rick_morty_electrum(&mm_node_1)) + ); + + // Enable coins on mm_node_2 side. Print the replies in case we need the "address". + log!( + "enable_coins (mm_node_2): {:?}", + block_on(enable_coins_rick_morty_electrum(&mm_node_2)) + ); + + // mm_node_1 is maker + let set_price = block_on(mm_node_1.rpc(&json! ({ + "userpass": mm_node_1.userpass, + "method": "setprice", + "base": "RICK", + "rel": "tBTC-TEST-lightning", + "price": 0.000001, + "volume": 0.1 + }))) + .unwrap(); + assert!(set_price.0.is_success(), "!setprice: {}", set_price.1); + + let orderbook = block_on(mm_node_2.rpc(&json! ({ + "userpass": mm_node_2.userpass, + "method": "orderbook", + "base": "RICK", + "rel": "tBTC-TEST-lightning", + }))) + .unwrap(); + assert!(orderbook.0.is_success(), "!orderbook: {}", orderbook.1); + + block_on(Timer::sleep(1.)); + + // mm_node_2 is taker + let buy = block_on(mm_node_2.rpc(&json! ({ + "userpass": mm_node_2.userpass, + "method": "buy", + "base": "RICK", + "rel": "tBTC-TEST-lightning", + "price": 0.000001, + "volume": 0.1 + }))) + .unwrap(); + assert!(buy.0.is_success(), "!buy: {}", buy.1); + let buy_json: Json = serde_json::from_str(&buy.1).unwrap(); + let uuid = buy_json["result"]["uuid"].as_str().unwrap().to_owned(); + + // ensure the swaps are started + block_on(mm_node_2.wait_for_log(5., |log| { + log.contains("Entering the taker_swap_loop RICK/tBTC-TEST-lightning") + })) + .unwrap(); + block_on(mm_node_1.wait_for_log(5., |log| { + log.contains("Entering the maker_swap_loop RICK/tBTC-TEST-lightning") + })) + .unwrap(); + + block_on(mm_node_1.wait_for_log(900., |log| log.contains(&format!("[swap uuid={}] Finished", uuid)))).unwrap(); + + block_on(mm_node_2.wait_for_log(900., |log| log.contains(&format!("[swap uuid={}] Finished", uuid)))).unwrap(); + + block_on(mm_node_1.stop()).unwrap(); + block_on(mm_node_2.stop()).unwrap(); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_sign_verify_message_lightning() { diff --git a/mm2src/mm2_main/src/mm2_tests/structs.rs b/mm2src/mm2_main/src/mm2_tests/structs.rs index 014db20f6f..7529199440 100644 --- a/mm2src/mm2_main/src/mm2_tests/structs.rs +++ b/mm2src/mm2_main/src/mm2_tests/structs.rs @@ -632,6 +632,7 @@ pub struct CoinActivationResult { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct UtxoStandardActivationResult { + pub ticker: String, pub current_block: u64, pub wallet_balance: EnableCoinBalance, } @@ -652,7 +653,8 @@ pub enum MmRpcResult { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, tag = "status", content = "details")] pub enum InitZcoinStatus { - Ok(MmRpcResult), + Ok(CoinActivationResult), + Error(Json), InProgress(Json), UserActionRequired(Json), } @@ -660,7 +662,8 @@ pub enum InitZcoinStatus { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, tag = "status", content = "details")] pub enum InitUtxoStatus { - Ready(MmRpcResult), + Ok(UtxoStandardActivationResult), + Error(Json), InProgress(Json), UserActionRequired(Json), } @@ -668,7 +671,8 @@ pub enum InitUtxoStatus { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, tag = "status", content = "details")] pub enum WithdrawStatus { - Ready(MmRpcResult), + Ok(TransactionDetails), + Error(Json), InProgress(Json), UserActionRequired(Json), } diff --git a/mm2src/mm2_main/src/mm2_tests/z_coin_tests.rs b/mm2src/mm2_main/src/mm2_tests/z_coin_tests.rs index a1a309fe00..1b8242fbed 100644 --- a/mm2src/mm2_main/src/mm2_tests/z_coin_tests.rs +++ b/mm2src/mm2_main/src/mm2_tests/z_coin_tests.rs @@ -28,13 +28,11 @@ async fn withdraw(mm: &MarketMakerIt, coin: &str, to: &str, amount: &str) -> Tra let status = withdraw_status(mm, init.result.task_id).await; println!("Withdraw status {}", json::to_string(&status).unwrap()); let status: RpcV2Response = json::from_value(status).unwrap(); - if let WithdrawStatus::Ready(rpc_result) = status.result { - match rpc_result { - MmRpcResult::Ok { result } => break result, - MmRpcResult::Err(e) => panic!("{} withdraw error {:?}", coin, e), - } + match status.result { + WithdrawStatus::Ok(result) => break result, + WithdrawStatus::Error(e) => panic!("{} withdraw error {:?}", coin, e), + _ => Timer::sleep(1.).await, } - Timer::sleep(1.).await; } } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 413b5f72dc..c06a0c84f4 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -43,10 +43,7 @@ use serde_json::{self as json, Value as Json}; use std::net::SocketAddr; cfg_native! { - use coins::lightning::{add_trusted_node, close_channel, connect_to_lightning_node, generate_invoice, get_channel_details, - get_claimable_balances, get_payment_details, list_closed_channels_by_filter, list_open_channels_by_filter, - list_payments_by_filter, list_trusted_nodes, open_channel, remove_trusted_node, send_payment, update_channel, - LightningCoin}; + use coins::lightning::LightningCoin; use coins::z_coin::ZCoin; } @@ -136,6 +133,12 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, account_balance).await, "add_delegation" => handle_mmrpc(ctx, request, add_delegation).await, @@ -172,9 +175,6 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, withdraw).await, #[cfg(not(target_arch = "wasm32"))] native_only_methods => match native_only_methods { - "add_trusted_node" => handle_mmrpc(ctx, request, add_trusted_node).await, - "close_channel" => handle_mmrpc(ctx, request, close_channel).await, - "connect_to_lightning_node" => handle_mmrpc(ctx, request, connect_to_lightning_node).await, "enable_lightning" => handle_mmrpc(ctx, request, enable_l2::).await, #[cfg(all(not(target_os = "ios"), not(target_os = "android")))] "enable_solana_with_tokens" => { @@ -182,18 +182,6 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, enable_token::).await, - "generate_invoice" => handle_mmrpc(ctx, request, generate_invoice).await, - "get_channel_details" => handle_mmrpc(ctx, request, get_channel_details).await, - "get_claimable_balances" => handle_mmrpc(ctx, request, get_claimable_balances).await, - "get_payment_details" => handle_mmrpc(ctx, request, get_payment_details).await, - "list_closed_channels_by_filter" => handle_mmrpc(ctx, request, list_closed_channels_by_filter).await, - "list_open_channels_by_filter" => handle_mmrpc(ctx, request, list_open_channels_by_filter).await, - "list_payments_by_filter" => handle_mmrpc(ctx, request, list_payments_by_filter).await, - "list_trusted_nodes" => handle_mmrpc(ctx, request, list_trusted_nodes).await, - "open_channel" => handle_mmrpc(ctx, request, open_channel).await, - "remove_trusted_node" => handle_mmrpc(ctx, request, remove_trusted_node).await, - "send_payment" => handle_mmrpc(ctx, request, send_payment).await, - "update_channel" => handle_mmrpc(ctx, request, update_channel).await, "z_coin_tx_history" => handle_mmrpc(ctx, request, coins::my_tx_history_v2::z_coin_tx_history_rpc).await, _ => MmError::err(DispatcherError::NoSuchMethod), }, @@ -283,3 +271,40 @@ async fn gui_storage_dispatcher( _ => MmError::err(DispatcherError::NoSuchMethod), } } + +/// `lightning` dispatcher. +/// +/// # Note +/// +/// `lightning_method` is a method name with the `lightning::` prefix removed. +#[cfg(not(target_arch = "wasm32"))] +async fn lightning_dispatcher( + request: MmRpcRequest, + ctx: MmArc, + lightning_method: &str, +) -> DispatcherResult>> { + use coins::rpc_command::lightning::{channels, nodes, payments}; + + match lightning_method { + "channels::close_channel" => handle_mmrpc(ctx, request, channels::close_channel).await, + "channels::get_channel_details" => handle_mmrpc(ctx, request, channels::get_channel_details).await, + "channels::get_claimable_balances" => handle_mmrpc(ctx, request, channels::get_claimable_balances).await, + "channels::list_closed_channels_by_filter" => { + handle_mmrpc(ctx, request, channels::list_closed_channels_by_filter).await + }, + "channels::list_open_channels_by_filter" => { + handle_mmrpc(ctx, request, channels::list_open_channels_by_filter).await + }, + "channels::open_channel" => handle_mmrpc(ctx, request, channels::open_channel).await, + "channels::update_channel" => handle_mmrpc(ctx, request, channels::update_channel).await, + "nodes::add_trusted_node" => handle_mmrpc(ctx, request, nodes::add_trusted_node).await, + "nodes::connect_to_node" => handle_mmrpc(ctx, request, nodes::connect_to_node).await, + "nodes::list_trusted_nodes" => handle_mmrpc(ctx, request, nodes::list_trusted_nodes).await, + "nodes::remove_trusted_node" => handle_mmrpc(ctx, request, nodes::remove_trusted_node).await, + "payments::generate_invoice" => handle_mmrpc(ctx, request, payments::generate_invoice).await, + "payments::get_payment_details" => handle_mmrpc(ctx, request, payments::get_payment_details).await, + "payments::list_payments_by_filter" => handle_mmrpc(ctx, request, payments::list_payments_by_filter).await, + "payments::send_payment" => handle_mmrpc(ctx, request, payments::send_payment).await, + _ => MmError::err(DispatcherError::NoSuchMethod), + } +} diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index d3d2cdb7ac..03efc4266f 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -1703,7 +1703,7 @@ pub async fn init_withdraw(mm: &MarketMakerIt, coin: &str, to: &str, amount: &st let request = mm .rpc(&json! ({ "userpass": mm.userpass, - "method": "init_withdraw", + "method": "task::withdraw::init", "mmrpc": "2.0", "params": { "coin": coin, @@ -1713,7 +1713,12 @@ pub async fn init_withdraw(mm: &MarketMakerIt, coin: &str, to: &str, amount: &st })) .await .unwrap(); - assert_eq!(request.0, StatusCode::OK, "'init_withdraw' failed: {}", request.1); + assert_eq!( + request.0, + StatusCode::OK, + "'task::withdraw::init' failed: {}", + request.1 + ); json::from_str(&request.1).unwrap() } @@ -1736,7 +1741,7 @@ pub async fn withdraw_status(mm: &MarketMakerIt, task_id: u64) -> Json { let request = mm .rpc(&json! ({ "userpass": mm.userpass, - "method": "withdraw_status", + "method": "task::withdraw::status", "mmrpc": "2.0", "params": { "task_id": task_id, @@ -1744,7 +1749,12 @@ pub async fn withdraw_status(mm: &MarketMakerIt, task_id: u64) -> Json { })) .await .unwrap(); - assert_eq!(request.0, StatusCode::OK, "'withdraw_status' failed: {}", request.1); + assert_eq!( + request.0, + StatusCode::OK, + "'task::withdraw::status' failed: {}", + request.1 + ); json::from_str(&request.1).unwrap() } @@ -1752,7 +1762,7 @@ pub async fn init_z_coin_native(mm: &MarketMakerIt, coin: &str) -> Json { let request = mm .rpc(&json! ({ "userpass": mm.userpass, - "method": "init_z_coin", + "method": "task::enable_z_coin::init", "mmrpc": "2.0", "params": { "ticker": coin, @@ -1765,7 +1775,12 @@ pub async fn init_z_coin_native(mm: &MarketMakerIt, coin: &str) -> Json { })) .await .unwrap(); - assert_eq!(request.0, StatusCode::OK, "'init_z_coin' failed: {}", request.1); + assert_eq!( + request.0, + StatusCode::OK, + "'task::enable_z_coin::init' failed: {}", + request.1 + ); json::from_str(&request.1).unwrap() } @@ -1790,7 +1805,12 @@ pub async fn init_z_coin_light(mm: &MarketMakerIt, coin: &str, electrums: &[&str })) .await .unwrap(); - assert_eq!(request.0, StatusCode::OK, "'init_z_coin' failed: {}", request.1); + assert_eq!( + request.0, + StatusCode::OK, + "'task::enable_z_coin::init' failed: {}", + request.1 + ); json::from_str(&request.1).unwrap() } @@ -1806,7 +1826,12 @@ pub async fn init_z_coin_status(mm: &MarketMakerIt, task_id: u64) -> Json { })) .await .unwrap(); - assert_eq!(request.0, StatusCode::OK, "'init_z_coin_status' failed: {}", request.1); + assert_eq!( + request.0, + StatusCode::OK, + "'task::enable_z_coin::status' failed: {}", + request.1 + ); json::from_str(&request.1).unwrap() } @@ -1937,7 +1962,7 @@ pub async fn init_utxo_electrum(mm: &MarketMakerIt, coin: &str, servers: Vec Json { let request = mm .rpc(&json! ({ "userpass": mm.userpass, - "method": "init_utxo_status", + "method": "task::enable_utxo::status", "mmrpc": "2.0", "params": { "task_id": task_id, @@ -1969,7 +1999,12 @@ pub async fn init_utxo_status(mm: &MarketMakerIt, task_id: u64) -> Json { })) .await .unwrap(); - assert_eq!(request.0, StatusCode::OK, "'init_utxo_status' failed: {}", request.1); + assert_eq!( + request.0, + StatusCode::OK, + "'task::enable_utxo::status' failed: {}", + request.1 + ); json::from_str(&request.1).unwrap() }