Skip to content

Commit

Permalink
feat: fund channel flow
Browse files Browse the repository at this point in the history
We introduce a new flow to fund the first position and with it the first channel. The idea is that the user does not have to have any on-chain funds before opening the first position.
The flow is divided into two parts:
1) open a position using on-chain funds:
if the user has already on-chain funds, then he can opt to use said funds and open the channel immediately
2) open a position using external funds:
the user has the option to fund the wallet using an external wallet such as a lightning wallet, another on-chain wallet, or maybe even using a hardware wallet in the future.
In this patch, we only introduce funding using an on-chain transaction. The app waits until it sees the funds incoming and then proceeds with the channel creation.
  • Loading branch information
bonomat committed May 22, 2024
1 parent 4d69f37 commit 8644cb8
Show file tree
Hide file tree
Showing 30 changed files with 1,620 additions and 73 deletions.
1 change: 0 additions & 1 deletion coordinator/src/trade/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,6 @@ impl TradeExecutor {

let contract_input = ContractInput {
offer_collateral,

accept_collateral,
fee_rate,
contract_infos: vec![ContractInputInfo {
Expand Down
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 @@ -191,6 +191,9 @@ impl Senders {
native::event::EventInternal::DlcChannelEvent(_) => {
// ignored
}
native::event::EventInternal::FundingChannelNotification(_) => {
// ignored
}
}
Ok(())
}
Expand Down
4 changes: 3 additions & 1 deletion crates/xxi-node/src/dlc_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ impl<D: BdkStorage, S: TenTenOneStorage, N> dlc_manager::Wallet for DlcWallet<D,
value: amount,
};

let available_candidates = candidates.iter().map(|can| can.value).sum::<u64>();

let mut coin_selector = CoinSelector::new(&candidates, base_weight_wu as u32);

let dust_limit = 0;
Expand All @@ -280,7 +282,7 @@ impl<D: BdkStorage, S: TenTenOneStorage, N> dlc_manager::Wallet for DlcWallet<D,
.run_bnb(metric, COIN_SELECTION_MAX_ROUNDS)
.map_err(|e| {
dlc_manager::error::Error::WalletError(
(format!("Wallet does not hold enough UTXOs to cover amount {amount} with fee rate {fee_rate}. {e:#}")).into(),
(format!("Wallet does not hold enough UTXOs to cover amount {amount} sats with fee rate {fee_rate} sats/vbyte because we only have {available_candidates} sats. {e:#}")).into(),
)
})?;

Expand Down
69 changes: 69 additions & 0 deletions crates/xxi-node/src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use bitcoin::address::NetworkUnchecked;
use bitcoin::secp256k1::PublicKey;
use bitcoin::secp256k1::XOnlyPublicKey;
use bitcoin::Address;
use bitcoin::Amount;
use bitcoin::Network;
use bitcoin::Txid;
use futures::future::RemoteHandle;
Expand Down Expand Up @@ -57,6 +58,8 @@ pub mod peer_manager;
pub use crate::message_handler::tentenone_message_name;
pub use ::dlc_manager as rust_dlc_manager;
use ::dlc_manager::ReferenceId;
use bdk_esplora::esplora_client::OutputStatus;
use bdk_esplora::esplora_client::Tx;
pub use dlc_manager::signed_channel_state_name;
pub use dlc_manager::DlcManager;
use lightning::ln::peer_handler::ErroringMessageHandler;
Expand Down Expand Up @@ -335,6 +338,72 @@ impl<D: BdkStorage, S: TenTenOneStorage + 'static, N: Storage + Sync + Send + 's
.map(|(peer, _)| to_secp_pk_30(peer))
.collect()
}

pub async fn get_unspent_txs(&self, address: &Address) -> Result<Vec<(Tx, Amount)>> {
let txs = self.get_utxo_for_address(address).await?;
let mut statuses = vec![];
for tx in txs {
if let Some(index) = tx
.vout
.iter()
.position(|vout| vout.scriptpubkey == address.script_pubkey())
{
match self.get_status_for_vout(&tx.txid, index as u64).await {
Ok(Some(status)) => {
if status.spent {
tracing::warn!(
txid = tx.txid.to_string(),
vout = index,
"Ignoring output as it is already spent"
)
} else {
let amount =
Amount::from_sat(tx.vout.get(index).expect("to exist").value);
statuses.push((tx, amount));
}
}
Ok(None) => {
tracing::warn!(
txid = tx.txid.to_string(),
vout = index,
"No status found for tx"
);
}
Err(error) => {
tracing::error!(
txid = tx.txid.to_string(),
vout = index,
"Failed at checking tx status {error:?}"
);
}
}
} else {
tracing::error!(
txid = tx.txid.to_string(),
address = address.to_string(),
"Output not found. This should not happen, but if it does, it indicates something is wrong.");
}
}
Ok(statuses)
}

async fn get_utxo_for_address(&self, address: &Address) -> Result<Vec<Tx>> {
let vec = self
.blockchain
.esplora_client_async
.scripthash_txs(&address.script_pubkey(), None)
.await?;
Ok(vec)
}

async fn get_status_for_vout(&self, tx_id: &Txid, vout: u64) -> Result<Option<OutputStatus>> {
let status = self
.blockchain
.esplora_client_async
.get_output_status(tx_id, vout)
.await?;
Ok(status)
}
}

async fn update_fee_rate_estimates(
Expand Down
Binary file added mobile/assets/coming_soon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions mobile/lib/common/application/event_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class EventService {
subscribers[eventType] = List.empty(growable: true);
}

logger.i("Subscribed: $subscriber for event: $event $eventType");
subscribers[eventType]!.add(subscriber);
}
}
Expand Down
18 changes: 12 additions & 6 deletions mobile/lib/common/custom_qr_code.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ import 'package:qr_flutter/qr_flutter.dart';

class CustomQrCode extends StatelessWidget {
final String data;
final String embeddedImagePath;
final ImageProvider embeddedImage;
final double embeddedImageSizeWidth;
final double embeddedImageSizeHeight;
final double dimension;

const CustomQrCode({
Key? key,
required this.data,
required this.embeddedImagePath,
required this.embeddedImage,
this.dimension = 350.0,
this.embeddedImageSizeHeight = 50,
this.embeddedImageSizeWidth = 50,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: 350,
dimension: dimension,
child: QrImageView(
data: data,
eyeStyle: const QrEyeStyle(
Expand All @@ -25,9 +31,9 @@ class CustomQrCode extends StatelessWidget {
dataModuleShape: QrDataModuleShape.square,
color: Colors.black,
),
embeddedImage: AssetImage(embeddedImagePath),
embeddedImageStyle: const QrEmbeddedImageStyle(
size: Size(50, 50),
embeddedImage: embeddedImage,
embeddedImageStyle: QrEmbeddedImageStyle(
size: Size(embeddedImageSizeHeight, embeddedImageSizeWidth),
),
version: QrVersions.auto,
padding: const EdgeInsets.all(5),
Expand Down
48 changes: 48 additions & 0 deletions mobile/lib/common/domain/funding_channel_task.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:get_10101/bridge_generated/bridge_definitions.dart' as bridge;

enum FundingChannelTaskStatus {
pending,
funded,
orderCreated,
failed;

static (FundingChannelTaskStatus, String?) fromApi(dynamic taskStatus) {
if (taskStatus is bridge.FundingChannelTask_Pending) {
return (FundingChannelTaskStatus.pending, null);
}

if (taskStatus is bridge.FundingChannelTask_Funded) {
return (FundingChannelTaskStatus.funded, null);
}

if (taskStatus is bridge.FundingChannelTask_Failed) {
final error = taskStatus.field0;
return (FundingChannelTaskStatus.failed, error);
}

if (taskStatus is bridge.FundingChannelTask_OrderCreated) {
final orderId = taskStatus.field0;
return (FundingChannelTaskStatus.orderCreated, orderId);
}

return (FundingChannelTaskStatus.pending, null);
}

static bridge.FundingChannelTask apiDummy() {
return const bridge.FundingChannelTask_Pending();
}

@override
String toString() {
switch (this) {
case FundingChannelTaskStatus.pending:
return "Pending";
case FundingChannelTaskStatus.failed:
return "Failed";
case FundingChannelTaskStatus.funded:
return "Funded";
case FundingChannelTaskStatus.orderCreated:
return "OrderCreated";
}
}
}
21 changes: 21 additions & 0 deletions mobile/lib/common/funding_channel_task_change_notifier.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:get_10101/common/application/event_service.dart';
import 'package:get_10101/bridge_generated/bridge_definitions.dart' as bridge;
import 'package:get_10101/common/domain/funding_channel_task.dart';
import 'package:get_10101/logger/logger.dart';

class FundingChannelChangeNotifier extends ChangeNotifier implements Subscriber {
FundingChannelTaskStatus? status;
String? error;

@override
void notify(bridge.Event event) async {
if (event is bridge.Event_FundingChannelNotification) {
logger.d("Received a funding channel task notification. ${event.field0}");
var fromApi = FundingChannelTaskStatus.fromApi(event.field0);
status = fromApi.$1;
error = fromApi.$2;
notifyListeners();
}
}
}
7 changes: 7 additions & 0 deletions mobile/lib/common/init_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import 'package:get_10101/common/background_task_change_notifier.dart';
import 'package:get_10101/common/dlc_channel_change_notifier.dart';
import 'package:get_10101/common/dlc_channel_service.dart';
import 'package:get_10101/common/domain/dlc_channel.dart';
import 'package:get_10101/common/domain/funding_channel_task.dart';
import 'package:get_10101/common/domain/tentenone_config.dart';
import 'package:get_10101/common/funding_channel_task_change_notifier.dart';
import 'package:get_10101/features/brag/meme_service.dart';
import 'package:get_10101/features/trade/order_change_notifier.dart';
import 'package:get_10101/features/trade/position_change_notifier.dart';
Expand Down Expand Up @@ -54,6 +56,7 @@ List<SingleChildWidget> createProviders() {
ChangeNotifierProvider(create: (context) => ServiceStatusNotifier()),
ChangeNotifierProvider(create: (context) => DlcChannelChangeNotifier(dlcChannelService)),
ChangeNotifierProvider(create: (context) => BackgroundTaskChangeNotifier()),
ChangeNotifierProvider(create: (context) => FundingChannelChangeNotifier()),
ChangeNotifierProvider(create: (context) => TenTenOneConfigChangeNotifier(channelInfoService)),
ChangeNotifierProvider(create: (context) => PollChangeNotifier(pollService)),
Provider(create: (context) => config),
Expand All @@ -78,6 +81,7 @@ void subscribeToNotifiers(BuildContext context) {
final tradeValuesChangeNotifier = context.read<TradeValuesChangeNotifier>();
final serviceStatusNotifier = context.read<ServiceStatusNotifier>();
final backgroundTaskChangeNotifier = context.read<BackgroundTaskChangeNotifier>();
final fundingChannelChangeNotifier = context.read<FundingChannelChangeNotifier>();
final tentenoneConfigChangeNotifier = context.read<TenTenOneConfigChangeNotifier>();
final dlcChannelChangeNotifier = context.read<DlcChannelChangeNotifier>();

Expand Down Expand Up @@ -111,6 +115,9 @@ void subscribeToNotifiers(BuildContext context) {
eventService.subscribe(
backgroundTaskChangeNotifier, bridge.Event.backgroundNotification(BackgroundTask.apiDummy()));

eventService.subscribe(fundingChannelChangeNotifier,
bridge.Event.fundingChannelNotification(FundingChannelTaskStatus.apiDummy()));

eventService.subscribe(
tentenoneConfigChangeNotifier, bridge.Event.authenticated(TenTenOneConfig.apiDummy()));

Expand Down
24 changes: 24 additions & 0 deletions mobile/lib/common/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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/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/wallet/domain/destination.dart';
import 'package:get_10101/features/wallet/send/send_onchain_screen.dart';
import 'package:get_10101/features/welcome/error_screen.dart';
Expand Down Expand Up @@ -225,6 +227,28 @@ GoRouter createRoutes() {
),
],
),
GoRoute(
path: ChannelConfigurationScreen.route,
builder: (BuildContext context, GoRouterState state) {
final data = state.extra! as Map<String, dynamic>;
return ChannelConfigurationScreen(
direction: data["direction"],
);
},
routes: [
GoRoute(
path: ChannelFundingScreen.subRouteName,
builder: (BuildContext context, GoRouterState state) {
final data = state.extra! as Map<String, dynamic>;
return ChannelFundingScreen(
amount: data["amount"],
address: data["address"],
);
},
routes: const [],
)
],
)
]),
]);
}
5 changes: 4 additions & 1 deletion mobile/lib/common/task_status_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ class _TaskStatusDialog extends State<TaskStatusDialog> {
width: MediaQuery.of(context).size.width * 0.65,
child: ElevatedButton(
onPressed: () {
GoRouter.of(context).pop();
var goRouter = GoRouter.of(context);
if (goRouter.canPop()) {
goRouter.pop();
}

if (widget.onClose != null) {
widget.onClose!();
Expand Down
30 changes: 30 additions & 0 deletions mobile/lib/features/trade/application/order_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,36 @@ class OrderService {
traderReserve: traderReserve.sats);
}

// starts a process to watch for funding an address before creating the order
// returns the address to watch for
Future<String> submitUnfundedChannelOpeningMarketOrder(
Leverage leverage,
Usd quantity,
ContractSymbol contractSymbol,
Direction direction,
bool stable,
Amount coordinatorReserve,
Amount traderReserve,
Amount margin) async {
rust.NewOrder order = rust.NewOrder(
leverage: leverage.leverage,
quantity: quantity.asDouble(),
contractSymbol: contractSymbol.toApi(),
direction: direction.toApi(),
orderType: const rust.OrderType.market(),
stable: stable);

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

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

Future<List<Order>> fetchOrders() async {
List<rust.Order> apiOrders = await rust.api.getOrders();
List<Order> orders = apiOrders.map((order) => Order.fromApi(order)).toList();
Expand Down
4 changes: 2 additions & 2 deletions mobile/lib/features/trade/channel_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,8 @@ class _ChannelConfiguration extends State<ChannelConfiguration> {
? () {
GoRouter.of(context).pop();
widget.onConfirmation(ChannelOpeningParams(
coordinatorCollateral: counterpartyCollateral,
traderCollateral: ownTotalCollateral));
coordinatorReserve: counterpartyCollateral,
traderReserve: ownTotalCollateral));
}
: null,
style: ElevatedButton.styleFrom(
Expand Down
Loading

0 comments on commit 8644cb8

Please sign in to comment.