Skip to content

Commit

Permalink
feat: Watch for lightning invoice payment
Browse files Browse the repository at this point in the history
This will submit the order once a hodl invoice is paid.

In the next step we have to extend the post order api to revail the pre image so that the coordinator can claim the payment and open a single sided channel.
  • Loading branch information
holzeis committed May 22, 2024
1 parent fa7aea4 commit 0172a94
Show file tree
Hide file tree
Showing 22 changed files with 332 additions and 235 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 30 additions & 21 deletions coordinator/src/node/invoice.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,48 @@
use bitcoin::Amount;
use futures_util::TryStreamExt;
use lnd_bridge::InvoiceState;
use lnd_bridge::LndBridge;
use tokio::sync::broadcast;
use xxi_node::commons;
use xxi_node::commons::Message;

/// Watches a hodl invoice with the given r_hash
pub fn spawn_invoice_watch(lnd_bridge: LndBridge, invoice_params: commons::HodlInvoiceParams) {
pub fn spawn_invoice_watch(
trader_sender: broadcast::Sender<Message>,
lnd_bridge: LndBridge,
invoice_params: commons::HodlInvoiceParams,
) {
tokio::spawn(async move {
let trader_pubkey = invoice_params.trader_pubkey;
let r_hash = invoice_params.r_hash;
let mut stream = lnd_bridge.subscribe_to_invoice(r_hash.clone());

'watch_invoice: loop {
match stream.try_next().await {
Ok(Some(invoice)) => {
match invoice.state {
InvoiceState::Open => {
tracing::debug!(%trader_pubkey, r_hash, "Watching hodl invoice.");
continue 'watch_invoice;
}
InvoiceState::Settled => {
tracing::info!(%trader_pubkey, r_hash, "Accepted hodl invoice has been settled.");
break 'watch_invoice;
}
InvoiceState::Canceled => {
tracing::warn!(%trader_pubkey, r_hash, "Pending hodl invoice has been canceled.");
break 'watch_invoice;
}
InvoiceState::Accepted => {
tracing::info!(%trader_pubkey, r_hash, "Pending hodl invoice has been accepted.");
// TODO(holzeis): Notify the client about the accepted invoice.
// wait for the invoice to get settled.
continue 'watch_invoice;
Ok(Some(invoice)) => match invoice.state {
InvoiceState::Open => {
tracing::debug!(%trader_pubkey, invoice.r_hash, "Watching hodl invoice.");
continue 'watch_invoice;
}
InvoiceState::Settled => {
tracing::info!(%trader_pubkey, invoice.r_hash, "Accepted hodl invoice has been settled.");
break 'watch_invoice;
}
InvoiceState::Canceled => {
tracing::warn!(%trader_pubkey, invoice.r_hash, "Pending hodl invoice has been canceled.");
break 'watch_invoice;
}
InvoiceState::Accepted => {
tracing::info!(%trader_pubkey, invoice.r_hash, "Pending hodl invoice has been accepted.");
if let Err(e) = trader_sender.send(Message::PaymentReceived {
r_hash: invoice.r_hash.clone(),
amount: Amount::from_sat(invoice.amt_paid_sat),
}) {
tracing::error!(%trader_pubkey, r_hash = invoice.r_hash, "Failed to send payment received event to app. Error: {e:#}")
}
continue 'watch_invoice;
}
}
},
Ok(None) => {
tracing::error!(%trader_pubkey, r_hash, "Websocket sender died.");
break 'watch_invoice;
Expand Down
9 changes: 8 additions & 1 deletion coordinator/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ fn parse_offset_datetime(date_str: String) -> Result<Option<OffsetDateTime>> {
Ok(Some(date_time))
}

#[instrument(skip_all, err(Debug))]
pub async fn get_leaderboard(
State(state): State<Arc<AppState>>,
params: Query<LeaderBoardQueryParams>,
Expand Down Expand Up @@ -593,6 +594,7 @@ pub async fn get_leaderboard(
}))
}

#[instrument(skip_all, err(Debug))]
async fn post_error(
State(state): State<Arc<AppState>>,
app_error: Json<ReportedError>,
Expand All @@ -608,6 +610,7 @@ async fn post_error(
Ok(())
}

#[instrument(skip_all, err(Debug))]
async fn create_invoice(
State(state): State<Arc<AppState>>,
Json(invoice_params): Json<SignedValue<commons::HodlInvoiceParams>>,
Expand All @@ -630,7 +633,11 @@ async fn create_invoice(
.map_err(|e| AppError::InternalServerError(format!("{e:#}")))?;

// watch for the created hodl invoice
invoice::spawn_invoice_watch(state.lnd_bridge.clone(), invoice_params);
invoice::spawn_invoice_watch(
state.tx_orderbook_feed.clone(),
state.lnd_bridge.clone(),
invoice_params,
);

Ok(Json(response.payment_request))
}
3 changes: 3 additions & 0 deletions crates/tests-e2e/src/test_subscriber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ impl Senders {
native::event::EventInternal::FundingChannelNotification(_) => {
// ignored
}
native::event::EventInternal::PaymentReceived => {
// ignored
}
}
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions crates/xxi-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ description = "A common library for the 10101 node"
anyhow = { version = "1", features = ["backtrace"] }
async-trait = "0.1.71"
axum = { version = "0.6", features = ["ws"], optional = true }
base64 = "0.22.1"
bdk = { version = "1.0.0-alpha.6", features = ["std"] }
bdk_coin_select = "0.2.0"
bdk_esplora = { version = "0.8.0" }
Expand Down
8 changes: 8 additions & 0 deletions crates/xxi-node/src/commons/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ pub enum Message {
order_id: Uuid,
error: TradingError,
},
PaymentReceived {
r_hash: String,
#[serde(with = "bitcoin::amount::serde::as_sat")]
amount: Amount,
},
RolloverError {
error: TradingError,
},
Expand Down Expand Up @@ -123,6 +128,9 @@ impl Display for Message {
Message::RolloverError { .. } => {
write!(f, "RolloverError")
}
Message::PaymentReceived { .. } => {
write!(f, "PaymentReceived")
}
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/xxi-node/src/commons/pre_image.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use base64::engine::general_purpose;
use base64::Engine;
use rand::Rng;
use sha256::digest;

Expand All @@ -6,6 +8,12 @@ pub struct PreImage {
pub hash: String,
}

impl PreImage {
pub fn get_base64_encoded_pre_image(&self) -> String {
general_purpose::URL_SAFE.encode(self.pre_image)
}
}

pub fn create_pre_image() -> PreImage {
let pre_image = inner_create_pre_image();
let hash = inner_hash_pre_image(&pre_image);
Expand Down
29 changes: 15 additions & 14 deletions mobile/lib/common/routes.dart
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
import 'package:flutter/material.dart';
import 'package:get_10101/common/background_task_dialog_screen.dart';
import 'package:get_10101/common/global_keys.dart';
import 'package:get_10101/common/scaffold_with_nav_bar.dart';
import 'package:get_10101/common/settings/app_info_screen.dart';
import 'package:get_10101/common/settings/channel_screen.dart';
import 'package:get_10101/common/settings/collab_close_screen.dart';
import 'package:get_10101/common/settings/emergency_kit_screen.dart';
import 'package:get_10101/common/settings/force_close_screen.dart';
import 'package:get_10101/common/settings/seed_screen.dart';
import 'package:get_10101/common/settings/settings_screen.dart';
import 'package:get_10101/common/settings/share_logs_screen.dart';
import 'package:get_10101/common/settings/user_screen.dart';
import 'package:get_10101/common/settings/wallet_settings.dart';
import 'package:get_10101/common/status_screen.dart';
import 'package:get_10101/common/background_task_dialog_screen.dart';
import 'package:get_10101/features/trade/application/order_service.dart';
import 'package:get_10101/features/trade/channel_creation_flow/channel_configuration_screen.dart';
import 'package:get_10101/features/trade/channel_creation_flow/channel_funding_screen.dart';
import 'package:get_10101/features/trade/trade_screen.dart';
import 'package:get_10101/features/wallet/domain/destination.dart';
import 'package:get_10101/features/wallet/domain/wallet_type.dart';
import 'package:get_10101/features/wallet/receive_screen.dart';
import 'package:get_10101/features/wallet/scanner_screen.dart';
import 'package:get_10101/features/wallet/send/send_onchain_screen.dart';
import 'package:get_10101/features/wallet/wallet_screen.dart';
import 'package:get_10101/features/welcome/error_screen.dart';
import 'package:get_10101/features/welcome/loading_screen.dart';
import 'package:get_10101/common/scaffold_with_nav_bar.dart';
import 'package:get_10101/common/settings/app_info_screen.dart';
import 'package:get_10101/common/settings/collab_close_screen.dart';
import 'package:get_10101/common/settings/force_close_screen.dart';
import 'package:get_10101/common/settings/settings_screen.dart';
import 'package:get_10101/common/settings/share_logs_screen.dart';
import 'package:get_10101/features/welcome/onboarding.dart';
import 'package:get_10101/features/trade/trade_screen.dart';
import 'package:get_10101/features/wallet/domain/wallet_type.dart';
import 'package:get_10101/features/wallet/receive_screen.dart';
import 'package:get_10101/features/wallet/scanner_screen.dart';
import 'package:get_10101/features/welcome/seed_import_screen.dart';
import 'package:get_10101/common/settings/seed_screen.dart';
import 'package:get_10101/features/wallet/wallet_screen.dart';
import 'package:get_10101/features/welcome/welcome_screen.dart';
import 'package:go_router/go_router.dart';

Expand Down Expand Up @@ -242,7 +243,7 @@ GoRouter createRoutes() {
final data = state.extra! as Map<String, dynamic>;
return ChannelFundingScreen(
amount: data["amount"],
address: data["address"],
funding: data["funding"] as ExternalFunding,
);
},
routes: const [],
Expand Down
22 changes: 16 additions & 6 deletions mobile/lib/features/trade/application/order_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ import 'package:get_10101/features/trade/domain/leverage.dart';
import 'package:get_10101/features/trade/domain/order.dart';
import 'package:get_10101/ffi.dart' as rust;

class ExternalFunding {
final String bitcoinAddress;
final String paymentRequest;

const ExternalFunding({required this.bitcoinAddress, required this.paymentRequest});

static ExternalFunding fromApi(rust.ExternalFunding funding) {
return ExternalFunding(
bitcoinAddress: funding.bitcoinAddress, paymentRequest: funding.paymentRequest);
}
}

class OrderService {
Future<String> submitMarketOrder(Leverage leverage, Usd quantity, ContractSymbol contractSymbol,
Direction direction, bool stable) async {
Expand Down Expand Up @@ -43,7 +55,7 @@ class OrderService {

// starts a process to watch for funding an address before creating the order
// returns the address to watch for
Future<String> submitUnfundedChannelOpeningMarketOrder(
Future<ExternalFunding> submitUnfundedChannelOpeningMarketOrder(
Leverage leverage,
Usd quantity,
ContractSymbol contractSymbol,
Expand All @@ -60,15 +72,13 @@ class OrderService {
orderType: const rust.OrderType.market(),
stable: stable);

var address = await rust.api.getNewAddress();

await rust.api.submitUnfundedChannelOpeningOrder(
fundingAddress: address,
final funding = await rust.api.submitUnfundedChannelOpeningOrder(
order: order,
coordinatorReserve: coordinatorReserve.sats,
traderReserve: traderReserve.sats,
estimatedMargin: margin.sats);
return address;

return ExternalFunding.fromApi(funding);
}

Future<List<Order>> fetchOrders() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:get_10101/common/dlc_channel_service.dart';
import 'package:get_10101/common/domain/model.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/common/value_data_row.dart';
import 'package:get_10101/features/trade/application/order_service.dart';
import 'package:get_10101/features/trade/channel_creation_flow/channel_funding_screen.dart';
import 'package:get_10101/features/trade/channel_creation_flow/custom_framed_container.dart';
import 'package:get_10101/features/trade/channel_creation_flow/fee_expansion_widget.dart';
Expand Down Expand Up @@ -419,9 +420,9 @@ class _ChannelConfiguration extends State<ChannelConfiguration> {
ChannelOpeningParams(
coordinatorReserve: counterpartyCollateral,
traderReserve: ownTotalCollateral))
.then((address) {
.then((ExternalFunding funding) {
GoRouter.of(context).push(ChannelFundingScreen.route, extra: {
"address": address,
"funding": funding,
"amount": totalAmountToBeFunded
});
}).onError((error, stackTrace) {
Expand Down
Loading

0 comments on commit 0172a94

Please sign in to comment.