From aab945d6dded17661a025a44927a4d82ec286d89 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Fri, 14 Jul 2023 10:37:44 -0600 Subject: [PATCH] Add vanilla channels and support for swaps Co-authored-by: Alekos Filini --- .gitmodules | 2 +- README.md | 6 +- rust-lightning | 2 +- src/cli.rs | 819 +++++++++++++++--- src/main.rs | 764 ++++++++++------ src/swap.rs | 120 +++ tests/common.sh | 276 +++++- tests/scripts/close_coop.sh | 4 +- tests/scripts/close_coop_nobtc_acceptor.sh | 4 +- tests/scripts/close_coop_other_side.sh | 4 +- tests/scripts/close_coop_vanilla.sh | 30 + tests/scripts/close_coop_zero_balance.sh | 2 +- tests/scripts/close_force.sh | 4 +- tests/scripts/close_force_nobtc_acceptor.sh | 4 +- tests/scripts/close_force_other_side.sh | 4 +- tests/scripts/close_force_pending_htlc.sh | 6 +- tests/scripts/multi_open_close.sh | 8 +- tests/scripts/multihop.sh | 11 +- tests/scripts/multiple_payments.sh | 8 +- tests/scripts/open_after_double_send.sh | 4 +- tests/scripts/restart.sh | 4 +- tests/scripts/send_payment.sh | 4 +- tests/scripts/send_vanilla_payment.sh | 28 + tests/scripts/swap_roundtrip.sh | 34 + tests/scripts/swap_roundtrip_buy.sh | 40 + tests/scripts/swap_roundtrip_fail.sh | 27 + .../swap_roundtrip_fail_amount_maker.sh | 25 + .../swap_roundtrip_fail_amount_taker.sh | 26 + tests/scripts/swap_roundtrip_multihop_buy.sh | 62 ++ tests/scripts/swap_roundtrip_multihop_sell.sh | 64 ++ tests/scripts/swap_roundtrip_timeout.sh | 27 + tests/scripts/vanilla_keysend.sh | 26 + 32 files changed, 1997 insertions(+), 452 deletions(-) create mode 100644 src/swap.rs create mode 100644 tests/scripts/close_coop_vanilla.sh create mode 100644 tests/scripts/send_vanilla_payment.sh create mode 100644 tests/scripts/swap_roundtrip.sh create mode 100644 tests/scripts/swap_roundtrip_buy.sh create mode 100644 tests/scripts/swap_roundtrip_fail.sh create mode 100644 tests/scripts/swap_roundtrip_fail_amount_maker.sh create mode 100644 tests/scripts/swap_roundtrip_fail_amount_taker.sh create mode 100644 tests/scripts/swap_roundtrip_multihop_buy.sh create mode 100644 tests/scripts/swap_roundtrip_multihop_sell.sh create mode 100644 tests/scripts/swap_roundtrip_timeout.sh create mode 100644 tests/scripts/vanilla_keysend.sh diff --git a/.gitmodules b/.gitmodules index ad54e79..a00e77f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,7 +4,7 @@ shallow = true [submodule "rust-lightning"] path = rust-lightning - url = git@github.com:RGB-Tools/rust-lightning.git + url = git@github.com:danielabrozzoni/rust-lightning.git shallow = true [submodule "rgb-wallet"] path = rgb-wallet diff --git a/README.md b/README.md index a8ffd49..23e29ce 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ listchannels ``` ### Sending assets -To send RGB assets over the LN network, call the `keysend` command followed by: +To send RGB assets over the LN network, call the `coloredkeysend` command followed by: - the receiving peer's pubkey - the bitcoin amount in satoshis - the RGB asset's contract ID @@ -231,10 +231,10 @@ To send RGB assets over the LN network, call the `keysend` command followed by: Example: ``` -keysend 03ddf2eedb06d5bbd128ccd4f558cb4a7428bfbe359259c718db7d2a8eead169fb 2000000 rgb1lfxs4dmqs7a90vrz0yaje60fakuvu9u9esx882shy437yxazmysqamnv2r 10 +coloredkeysend 03ddf2eedb06d5bbd128ccd4f558cb4a7428bfbe359259c718db7d2a8eead169fb 2000000 rgb1lfxs4dmqs7a90vrz0yaje60fakuvu9u9esx882shy437yxazmysqamnv2r 10 ``` -At the moment, only the `keysend` command has been modified to support RGB +At the moment, only the `coloredkeysend` command has been modified to support RGB functionality. The invoice-based `sendpayment` will be added in the future. ### Closing channels diff --git a/rust-lightning b/rust-lightning index 8f9059d..bfef26e 160000 --- a/rust-lightning +++ b/rust-lightning @@ -1 +1 @@ -Subproject commit 8f9059d17e83b76446cc950606b42b884bc38cb6 +Subproject commit bfef26e4c83fef1d87fa020ca21b5406b2e7a129 diff --git a/src/cli.rs b/src/cli.rs index e039936..d9f6a37 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,9 +8,10 @@ use crate::proxy::{get_consignment, post_consignment}; use crate::rgb_utils::get_asset_owned_values; use crate::rgb_utils::get_rgb_total_amount; use crate::rgb_utils::RgbUtilities; +use crate::swap::{get_current_timestamp, SwapString, SwapType}; use crate::{ ChannelManager, HTLCStatus, MillisatAmount, NetworkGraph, OnionMessenger, PaymentInfo, - PaymentInfoStorage, PeerManager, + PaymentInfoStorage, PeerManager, Router, }; use crate::{FEE_RATE, UTXO_SIZE_SAT}; @@ -28,6 +29,7 @@ use bp::seals::txout::ExplicitSeal; use lightning::chain::keysinterface::{EntropySource, KeysManager}; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry}; use lightning::ln::msgs::NetAddress; +use lightning::ln::PaymentSecret; use lightning::ln::{PaymentHash, PaymentPreimage}; use lightning::onion_message::{CustomOnionMessageContents, Destination, OnionMessageContents}; use lightning::rgb_utils::drop_rgb_runtime; @@ -37,9 +39,16 @@ use lightning::rgb_utils::{ get_rgb_channel_info, write_rgb_channel_info, RgbInfo, RgbUtxo, RgbUtxos, }; use lightning::routing::gossip::NodeId; +use lightning::routing::gossip::RoutingFees; +use lightning::routing::router::RouteHint; +use lightning::routing::router::RouteHintHop; +use lightning::routing::router::{ + Hints, Path as LnPath, Route, Router as RouterTrait, DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, +}; use lightning::routing::router::{PaymentParameters, RouteParameters}; use lightning::util::config::{ChannelHandshakeConfig, ChannelHandshakeLimits, UserConfig}; use lightning::util::ser::{Writeable, Writer}; +use lightning::util::IS_SWAP_SCID; use lightning_invoice::payment::pay_invoice; use lightning_invoice::{utils, Currency, Invoice}; use reqwest::Client as RestClient; @@ -55,9 +64,8 @@ use rgbwallet::RgbTransport; use seals::txout::blind::BlindSeal; use seals::txout::TxPtr; use serde::{Deserialize, Serialize}; -use strict_encoding::{FieldName, TypeName}; - -use std::convert::TryFrom; +use std::collections::HashMap; +use std::convert::{TryFrom, TryInto}; use std::env; use std::fs; use std::io; @@ -69,6 +77,7 @@ use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::Duration; +use strict_encoding::{FieldName, TypeName}; const MIN_CREATE_UTXOS_SATS: u64 = 10000; const UTXO_NUM: u8 = 4; @@ -78,7 +87,7 @@ const OPENCHANNEL_MAX_SAT: u64 = 16777215; const DUST_LIMIT_MSAT: u64 = 546000; -const HTLC_MIN_MSAT: u64 = 3000000; +pub(crate) const HTLC_MIN_MSAT: u64 = 3000000; const INVOICE_MIN_MSAT: u64 = HTLC_MIN_MSAT; @@ -121,11 +130,13 @@ impl Writeable for UserOnionMessageContents { pub(crate) async fn poll_for_user_input( peer_manager: Arc, channel_manager: Arc, keys_manager: Arc, network_graph: Arc, - onion_messenger: Arc, inbound_payments: PaymentInfoStorage, - outbound_payments: PaymentInfoStorage, ldk_data_dir: String, network: Network, - logger: Arc, bitcoind_client: Arc, - proxy_client: Arc, proxy_url: &str, proxy_endpoint: &str, - wallet_arc: Arc>>, electrum_url: String, + onion_messenger: Arc, router: Arc, + inbound_payments: PaymentInfoStorage, outbound_payments: PaymentInfoStorage, + ldk_data_dir: String, network: Network, logger: Arc, + bitcoind_client: Arc, proxy_client: Arc, proxy_url: &str, + proxy_endpoint: &str, wallet_arc: Arc>>, electrum_url: String, + whitelisted_trades: Arc>>, + maker_trades: Arc>>, ) { println!( "LDK startup successful. Enter \"help\" to view available commands. Press Ctrl-D to quit." @@ -661,20 +672,38 @@ pub(crate) async fn poll_for_user_input( println!("Refresh complete"); } - "openchannel" => { + "openchannel" | "opencoloredchannel" => { + let is_colored = word == "opencoloredchannel"; let peer_pubkey_and_ip_addr = words.next(); let channel_value_sat = words.next(); let push_value_msat = words.next(); - let contract_id = words.next(); - let channel_value_rgb = words.next(); - if peer_pubkey_and_ip_addr.is_none() + let (contract_id, channel_value_rgb) = + if is_colored { (words.next(), words.next()) } else { (None, None) }; + let announce_channel = match words.next() { + Some("--public") | Some("--public=true") => true, + Some("--public=false") => false, + Some(_) => { + println!("ERROR: invalid `--public` command format. Valid formats: `--public`, `--public=true` `--public=false`"); + continue; + } + None => false, + }; + if is_colored + && (peer_pubkey_and_ip_addr.is_none() + || channel_value_sat.is_none() + || push_value_msat.is_none() || contract_id.is_none() + || channel_value_rgb.is_none()) + { + println!("ERROR: opencoloredchannel has 5 required arguments: `openchannel pubkey@host:port chan_amt_satoshis push_amt_msatoshis rgb_contract_id chan_amt_rgb` [--public]"); + continue; + } else if peer_pubkey_and_ip_addr.is_none() || channel_value_sat.is_none() || push_value_msat.is_none() - || contract_id.is_none() || channel_value_rgb.is_none() { - println!("ERROR: openchannel has 5 required arguments: `openchannel pubkey@host:port chan_amt_satoshis push_amt_msatoshis rgb_contract_id chan_amt_rgb` [--public]"); + println!("ERROR: openchannel has 3 required arguments: `openchannel pubkey@host:port chan_amt_satoshis push_amt_msatoshis` [--public]"); continue; } + let peer_pubkey_and_ip_addr = peer_pubkey_and_ip_addr.unwrap(); let (pubkey, peer_addr) = match parse_peer_info(peer_pubkey_and_ip_addr.to_string()) { @@ -712,7 +741,7 @@ pub(crate) async fn poll_for_user_input( continue; } let push_amt_msat = push_amt_msat.unwrap(); - if push_amt_msat < DUST_LIMIT_MSAT { + if is_colored && push_amt_msat < DUST_LIMIT_MSAT { println!( "ERROR: push amount must be equal or higher than the dust limit ({})", DUST_LIMIT_MSAT @@ -720,41 +749,51 @@ pub(crate) async fn poll_for_user_input( continue; } - let contract_id = ContractId::from_str(contract_id.unwrap()); - if contract_id.is_err() { - println!("ERROR: contract_id must be a valid RGB asset ID"); - continue; - } - let contract_id = contract_id.unwrap(); + let rgb_info = if !is_colored { + None + } else { + let contract_id = ContractId::from_str(contract_id.unwrap()); + if contract_id.is_err() { + println!("ERROR: contract_id must be a valid RGB asset ID"); + continue; + } + let contract_id = contract_id.unwrap(); - let chan_amt_rgb: Result = channel_value_rgb.unwrap().parse(); - if chan_amt_rgb.is_err() { - println!("ERROR: channel RGB amount must be a number"); - continue; - } - let chan_amt_rgb = chan_amt_rgb.unwrap(); + let chan_amt_rgb: Result = channel_value_rgb.unwrap().parse(); + if chan_amt_rgb.is_err() { + println!("ERROR: channel RGB amount must be a number"); + continue; + } + let chan_amt_rgb = chan_amt_rgb.unwrap(); - let runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + let runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); - let total_rgb_amount = match get_rgb_total_amount( - contract_id, - &runtime, - wallet_arc.clone(), - electrum_url.clone(), - ) { - Ok(a) => a, - Err(e) => { - println!("{e}"); + let total_rgb_amount = match get_rgb_total_amount( + contract_id, + &runtime, + wallet_arc.clone(), + electrum_url.clone(), + ) { + Ok(a) => a, + Err(e) => { + println!("{e}"); + continue; + } + }; + drop(runtime); + drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + + if chan_amt_rgb > total_rgb_amount { + println!("ERROR: do not have enough RGB assets"); continue; } - }; - drop(runtime); - drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); - if chan_amt_rgb > total_rgb_amount { - println!("ERROR: do not have enough RGB assets"); - continue; - } + Some(RgbInfo { + contract_id, + local_rgb_amount: chan_amt_rgb, + remote_rgb_amount: 0, + }) + }; if connect_peer_if_necessary(pubkey, peer_addr, peer_manager.clone()) .await @@ -763,16 +802,6 @@ pub(crate) async fn poll_for_user_input( continue; }; - let announce_channel = match words.next() { - Some("--public") | Some("--public=true") => true, - Some("--public=false") => false, - Some(_) => { - println!("ERROR: invalid `--public` command format. Valid formats: `--public`, `--public=true` `--public=false`"); - continue; - } - None => false, - }; - let open_channel_result = open_channel( pubkey, chan_amt_sat, @@ -780,26 +809,29 @@ pub(crate) async fn poll_for_user_input( announce_channel, channel_manager.clone(), proxy_endpoint, + is_colored, + Arc::clone(&wallet_arc), + &electrum_url, ); if open_channel_result.is_err() { continue; } - let peer_data_path = format!("{}/channel_peer_data", ldk_data_dir.clone()); + let peer_data_path = format!("{}/channel_peer_data", ldk_data_dir); let _ = disk::persist_channel_peer( Path::new(&peer_data_path), peer_pubkey_and_ip_addr, ); - let temporary_channel_id = open_channel_result.unwrap(); - let channel_rgb_info_path = - format!("{}/{}", ldk_data_dir.clone(), hex::encode(temporary_channel_id)); - let rgb_info = RgbInfo { - contract_id, - local_rgb_amount: chan_amt_rgb, - remote_rgb_amount: 0, - }; - write_rgb_channel_info(&PathBuf::from(&channel_rgb_info_path), &rgb_info); + if let Some(rgb_info) = rgb_info { + let temporary_channel_id = open_channel_result.unwrap(); + let channel_rgb_info_path = format!( + "{}/{}", + ldk_data_dir.clone(), + hex::encode(temporary_channel_id) + ); + write_rgb_channel_info(&PathBuf::from(&channel_rgb_info_path), &rgb_info); + } } "sendpayment" => { let invoice_str = words.next(); @@ -833,7 +865,8 @@ pub(crate) async fn poll_for_user_input( PathBuf::from(&ldk_data_dir), ); } - "keysend" => { + "keysend" | "coloredkeysend" => { + let is_colored = word == "coloredkeysend"; let keysend_cmd = "`keysend `"; let dest_pubkey = match words.next() { Some(dest) => match hex_utils::to_compressed_pubkey(dest) { @@ -863,36 +896,41 @@ pub(crate) async fn poll_for_user_input( continue; } }; - if amt_msat < HTLC_MIN_MSAT { - println!("ERROR: amount_msat cannot be less than {HTLC_MIN_MSAT}"); - continue; - } - let contract_id = match words.next() { - Some(contract_id_str) => match ContractId::from_str(contract_id_str) { - Ok(cid) => cid, - Err(_) => { - println!("ERROR: invalid contract ID: {contract_id_str}"); - continue; - } - }, - None => { - println!("ERROR: keysend requires a contract ID: {keysend_cmd}"); - continue; - } - }; - let amt_rgb_str = match words.next() { - Some(amt) => amt, - None => { - println!("ERROR: keysend requires an RGB amount: {keysend_cmd}"); - continue; - } - }; - let amt_rgb: u64 = match amt_rgb_str.parse() { - Ok(amt) => amt, - Err(e) => { - println!("ERROR: couldn't parse amt_rgb: {e}"); + let rgb_payment = if is_colored { + if amt_msat < HTLC_MIN_MSAT { + println!("ERROR: amount_msat cannot be less than {HTLC_MIN_MSAT}"); continue; } + let contract_id = match words.next() { + Some(contract_id_str) => match ContractId::from_str(contract_id_str) { + Ok(cid) => cid, + Err(_) => { + println!("ERROR: invalid contract ID: {contract_id_str}"); + continue; + } + }, + None => { + println!("ERROR: keysend requires a contract ID: {keysend_cmd}"); + continue; + } + }; + let amt_rgb_str = match words.next() { + Some(amt) => amt, + None => { + println!("ERROR: keysend requires an RGB amount: {keysend_cmd}"); + continue; + } + }; + let amt_rgb: u64 = match amt_rgb_str.parse() { + Ok(amt) => amt, + Err(e) => { + println!("ERROR: couldn't parse amt_rgb: {e}"); + continue; + } + }; + Some((contract_id, amt_rgb)) + } else { + None }; keysend( &*channel_manager, @@ -900,12 +938,12 @@ pub(crate) async fn poll_for_user_input( amt_msat, &*keys_manager, outbound_payments.clone(), - contract_id, - amt_rgb, + rgb_payment, PathBuf::from(&ldk_data_dir), ); } - "getinvoice" => { + "getinvoice" | "getcoloredinvoice" => { + let is_colored = word == "getcoloredinvoice"; let getinvoice_cmd = "`getinvoice `"; let amt_str = words.next(); @@ -913,12 +951,17 @@ pub(crate) async fn poll_for_user_input( let contract_id_str = words.next(); let amt_rgb_str = words.next(); - if amt_str.is_none() - || expiry_secs_str.is_none() - || contract_id_str.is_none() - || amt_rgb_str.is_none() + if is_colored + && (amt_str.is_none() + || expiry_secs_str.is_none() || contract_id_str.is_none() + || amt_rgb_str.is_none()) { - println!("ERROR: getinvoice has 4 required arguments: {getinvoice_cmd}"); + println!( + "ERROR: getcoloredinvoice has 4 required arguments: {getinvoice_cmd}" + ); + continue; + } else if !is_colored && (amt_str.is_none() || expiry_secs_str.is_none()) { + println!("ERROR: getinvoice has 2 required arguments: getinvoice "); continue; } @@ -939,21 +982,27 @@ pub(crate) async fn poll_for_user_input( continue; } - let contract_id_str = contract_id_str.unwrap(); - let contract_id = match ContractId::from_str(contract_id_str) { - Ok(cid) => cid, - Err(_) => { - println!("ERROR: invalid contract ID: {contract_id_str}"); - continue; - } - }; + let (contract_id, amt_rgb) = if is_colored { + let contract_id_str = contract_id_str.unwrap(); + let contract_id = match ContractId::from_str(contract_id_str) { + Ok(cid) => cid, + Err(_) => { + println!("ERROR: invalid contract ID: {contract_id_str}"); + continue; + } + }; - let amt_rgb: u64 = match amt_rgb_str.unwrap().parse() { - Ok(amt) => amt, - Err(e) => { - println!("ERROR: couldn't parse amt_rgb: {e}"); - continue; - } + let amt_rgb: u64 = match amt_rgb_str.unwrap().parse() { + Ok(amt) => amt, + Err(e) => { + println!("ERROR: couldn't parse amt_rgb: {e}"); + continue; + } + }; + + (Some(contract_id), Some(amt_rgb)) + } else { + (None, None) }; get_invoice( @@ -1018,6 +1067,265 @@ pub(crate) async fn poll_for_user_input( "listchannels" => { list_channels(&channel_manager, &network_graph, ldk_data_dir.clone()) } + // Called by the service/exchange to initiate the trade. The swaptype is seen from + // the poin of view of the user, so "buy" means that the user (=taker) is buying + // assets for bitcoin, and sell that the user is selling assets for bitcoin + "makerinit" => { + let amt_asset = words.next(); + let asset_id = words.next(); + let swaptype = words.next(); + let timeout = words.next(); + let price = words.next(); + + if timeout.is_none() { + println!("ERROR: makerinit requires at least 4 args: makerinit []"); + continue; + } + + let asset_id = match ContractId::from_str(asset_id.unwrap()) { + Ok(cid) => cid, + Err(_) => { + println!("ERROR: invalid contract ID: {}", asset_id.unwrap()); + continue; + } + }; + let timeout = match timeout.unwrap().parse() { + Ok(t) if t > 0 => t, + Ok(_) | Err(_) => { + println!("ERROR: Invalid expiry_secs value"); + continue; + } + }; + let price = match price { + Some(x) if !x.trim().is_empty() => x.parse::().map_err(|_| { + println!("ERROR: invalid price_msats_per_asset"); + () + }), + _ => fetch_price(&asset_id).await.map_err(|e| { + println!("ERROR: invalid price_msats_per_asset: {}", e); + () + }), + }; + if price.is_err() { + continue; + } + let price = price.unwrap(); + if price == 0 { + println!("ERROR: invalid price_msats_per_asset"); + continue; + } + let amt_asset = match amt_asset.unwrap().parse::() { + Ok(amt) if amt > 0 => amt, + Ok(_) | Err(_) => { + println!("ERROR: invalid amt_asset"); + continue; + } + }; + let swaptype = match swaptype { + Some("buy") => SwapType::BuyAsset { + amount_rgb: amt_asset, + amount_msats: amt_asset * price, + }, + Some("sell") => SwapType::SellAsset { + amount_rgb: amt_asset, + amount_msats: amt_asset * price, + }, + _ => { + println!("ERROR: invalid swap type, use either `buy` or `sell`"); + continue; + } + }; + + // The user is buying assets = we (the service) are selling assets. Do we have + // enough? + if swaptype.is_buy() { + let runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + + let total_rgb_amount = match get_rgb_total_amount( + asset_id, + &runtime, + wallet_arc.clone(), + electrum_url.clone(), + ) { + Ok(a) => a, + Err(e) => { + println!("{e}"); + continue; + } + }; + drop(runtime); + drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + + if amt_asset > total_rgb_amount { + println!("ERROR: do not have enough RGB assets"); + continue; + } + } + + let (payment_hash, payment_secret) = + maker_init(&channel_manager, &swaptype, timeout, asset_id, &maker_trades); + let expiry = get_current_timestamp() + timeout as u64; + let swapstring = format!( + "{}:{}:{}:{}:{}:{}", + amt_asset, + asset_id, + swaptype.side(), + price, + expiry, + hex_utils::hex_str(&payment_hash.0) + ); + println!("SUCCESS! swap_string = {}", swapstring); + + let payment_secret = hex_utils::hex_str(&payment_secret.0); + println!("payment_secret: {}", payment_secret); + println!( + "To execute the swap run `makerexecute {} {} `", + swapstring, payment_secret + ); + } + "taker" => { + let swapstring = words.next(); + + if swapstring.is_none() { + println!("ERROR: taker requires 1 arg: taker "); + continue; + } + + let swapstring = match SwapString::from_str(swapstring.unwrap()) { + Ok(v) => v, + Err(e) => { + println!("ERROR: {}", e); + continue; + } + }; + + if get_current_timestamp() > swapstring.expiry { + println!("ERROR: the swap offer has already expired"); + continue; + } + + // We are selling assets, do we have enough? + if !swapstring.swap_type.is_buy() { + let runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + + let total_rgb_amount = match get_rgb_total_amount( + swapstring.asset_id, + &runtime, + wallet_arc.clone(), + electrum_url.clone(), + ) { + Ok(a) => a, + Err(e) => { + println!("{e}"); + continue; + } + }; + drop(runtime); + drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + + if swapstring.swap_type.amount_rgb() > total_rgb_amount { + println!("ERROR: do not have enough RGB assets"); + continue; + } + } + + whitelisted_trades.lock().unwrap().insert( + swapstring.payment_hash, + (swapstring.asset_id, swapstring.swap_type), + ); + println!("SUCCESS: Trade whitelisted!"); + println!("our_pk: {}", channel_manager.get_our_node_id()); + } + "tradeslist" => { + let filter = words.next(); + + let print_trade = |(k, v): (&PaymentHash, &(ContractId, SwapType))| { + println!("\t\t{{"); + + println!("\t\t\tpayment_hash: {}", hex_utils::hex_str(&k.0)); + println!("\t\t\tcontract_id: {}", v.0); + println!("\t\t\tside: {}", v.1.side()); + println!("\t\t\tamount_msats: {}", v.1.amount_msats()); + println!("\t\t\tamount_rgb: {}", v.1.amount_rgb()); + + println!("\t\t}},"); + }; + + println!("{{"); + if filter.is_none() || filter == Some("taker") { + let lock = whitelisted_trades.lock().unwrap(); + + println!("\ttaker: ["); + for tuple in lock.iter() { + print_trade(tuple); + } + println!("\t],"); + } + if filter.is_none() || filter == Some("maker") { + let lock = maker_trades.lock().unwrap(); + + println!("\tmaker: ["); + for tuple in lock.iter() { + print_trade(tuple); + } + println!("\t],"); + } + println!("}}"); + } + "makerexecute" => { + let swapstring = words.next(); + let payment_secret = words.next(); + let peer_pubkey = words.next(); + + if peer_pubkey.is_none() { + println!("ERROR: makerexecute requires 3 args: makerexecute "); + continue; + } + + let swapstring = match SwapString::from_str(swapstring.unwrap()) { + Ok(v) => v, + Err(e) => { + println!("ERROR: {}", e); + continue; + } + }; + + if get_current_timestamp() > swapstring.expiry { + println!("ERROR: the swap offer has already expired"); + continue; + } + + let payment_secret = hex_utils::to_vec(payment_secret.unwrap()) + .and_then(|vec| vec.try_into().ok()) + .map(|slice| PaymentSecret(slice)); + let payment_secret = match payment_secret { + Some(v) => v, + _ => { + println!("ERROR: invalid payment hash"); + continue; + } + }; + let peer_pubkey = + match bitcoin::secp256k1::PublicKey::from_str(peer_pubkey.unwrap()) { + Ok(pubkey) => pubkey, + Err(e) => { + println!("ERROR: {}", e.to_string()); + continue; + } + }; + + maker_execute( + &channel_manager, + &router, + peer_pubkey, + swapstring.swap_type, + swapstring.asset_id, + swapstring.payment_hash, + payment_secret, + outbound_payments.clone(), + PathBuf::from(&ldk_data_dir), + ); + } "listpayments" => { list_payments(inbound_payments.clone(), outbound_payments.clone()) } @@ -1205,20 +1513,34 @@ fn help() { println!(" help\tShows a list of commands."); println!(" quit\tClose the application."); println!("\n Channels:"); - println!(" openchannel pubkey@host:port [--public]"); + println!(" opencoloredchannel pubkey@host:port [--public]"); + println!( + " openchannel pubkey@host:port [--public]" + ); println!(" closechannel "); println!(" forceclosechannel "); println!(" listchannels"); + println!("\n Routing:"); + println!(" getroute []"); + println!("\n Swaps:"); + println!( + " makerinit []" + ); + println!(" taker "); + println!(" tradeslist []"); + println!(" makerexecute "); println!("\n Peers:"); println!(" connectpeer pubkey@host:port"); println!(" disconnectpeer "); println!(" listpeers"); println!("\n Payments:"); println!(" sendpayment "); - println!(" keysend "); + println!(" coloredkeysend "); + println!(" keysend "); println!(" listpayments"); println!("\n Invoices:"); - println!(" getinvoice "); + println!(" getcoloredinvoice "); + println!(" getinvoice "); println!(" invoicestatus "); println!("\n Onchain:"); println!(" getaddress"); @@ -1381,6 +1703,218 @@ fn list_payments(inbound_payments: PaymentInfoStorage, outbound_payments: Paymen println!("]"); } +fn get_route( + channel_manager: &ChannelManager, router: &Router, start: PublicKey, dest: PublicKey, + asset_id: Option, final_value_msat: Option, hints: Vec, +) -> Option { + let inflight_htlcs = channel_manager.compute_inflight_htlcs(); + let payment_params = PaymentParameters { + payee_pubkey: dest, + features: None, + route_hints: Hints::Clear(hints), + expiry_time: None, + max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, + max_path_count: 1, + max_channel_saturation_power_of_half: 2, + previously_failed_channels: vec![], + final_cltv_expiry_delta: 14, + }; + let route = router.find_route( + &start, + &RouteParameters { + payment_params, + final_value_msat: final_value_msat.unwrap_or(HTLC_MIN_MSAT), + }, + None, + &inflight_htlcs, + asset_id, + ); + + route.ok() +} + +fn maker_init( + channel_manager: &ChannelManager, swaptype: &SwapType, timeout_secs: u32, + contract_id: ContractId, + maker_trades: &Arc>>, +) -> (PaymentHash, PaymentSecret) { + let (payment_hash, payment_secret) = channel_manager + .create_inbound_payment(Some(swaptype.amount_msats()), timeout_secs, None) + .unwrap(); + maker_trades.lock().unwrap().insert(payment_hash, (contract_id, *swaptype)); + + (payment_hash, payment_secret) +} + +fn maker_execute( + channel_manager: &ChannelManager, router: &Router, taker_pk: PublicKey, swaptype: SwapType, + asset_id: ContractId, payment_hash: PaymentHash, payment_secret: PaymentSecret, + payment_storage: PaymentInfoStorage, ldk_data_dir: PathBuf, +) { + let payment_preimage = match channel_manager.get_payment_preimage(payment_hash, payment_secret) + { + Ok(p) => p, + Err(e) => { + println!("ERROR: unable to find payment preimage, have you provided the correct swapstring and payment secret? {:?}", e); + return; + } + }; + // The cli takes the swaptype from the point of view of the user (=who wants to do the trade) + // In this function we're the market maker (=who sends the payment, completing the user trade) + // We swap the swaptype to hopefully make this function easier to read + let swaptype = swaptype.opposite(); + + let receive_hints = channel_manager + .list_usable_channels() + .iter() + .map(|details| { + let config = details.counterparty.forwarding_info.as_ref().unwrap(); + RouteHint(vec![RouteHintHop { + src_node_id: details.counterparty.node_id, + short_channel_id: details.short_channel_id.unwrap(), + cltv_expiry_delta: config.cltv_expiry_delta, + htlc_maximum_msat: None, + htlc_minimum_msat: None, + fees: RoutingFees { + base_msat: config.fee_base_msat, + proportional_millionths: config.fee_proportional_millionths, + }, + }]) + }) + .collect(); + + let first_leg = get_route( + channel_manager, + router, + channel_manager.get_our_node_id(), + taker_pk, + if swaptype.is_buy() { None } else { Some(asset_id) }, + if swaptype.is_buy() { Some(swaptype.amount_msats()) } else { None }, + vec![], + ); + let second_leg = get_route( + channel_manager, + router, + taker_pk, + channel_manager.get_our_node_id(), + if swaptype.is_buy() { Some(asset_id) } else { None }, + if swaptype.is_buy() { Some(HTLC_MIN_MSAT) } else { Some(swaptype.amount_msats()) }, + receive_hints, + ); + + let (mut first_leg, mut second_leg) = match (first_leg, second_leg) { + (Some(f), Some(s)) => (f, s), + (Some(_), _) => { + println!("ERROR: unable to find from the taker to us"); + return; + } + (_, Some(_)) => { + println!("ERROR: unable to find path to the taker"); + return; + } + _ => { + println!("ERROR: no path found"); + return; + } + }; + + // Set swap flag + second_leg.paths[0].hops[0].short_channel_id |= IS_SWAP_SCID; + + // Generally in the last hop the fee_amount is set to the payment amount, so we need to + // override it depending on what type of swap we are doing + if let SwapType::BuyAsset { .. } = swaptype { + first_leg.paths[0].hops.last_mut().expect("Path not to be empty").fee_msat = HTLC_MIN_MSAT; + } else { + first_leg.paths[0].hops.last_mut().expect("Path not to be empty").fee_msat = 0; + } + + let fullpaths = first_leg.paths[0] + .hops + .clone() + .into_iter() + .map(|mut hop| { + if let SwapType::SellAsset { amount_rgb, .. } = swaptype { + hop.rgb_amount = Some(amount_rgb); + hop.payment_amount = HTLC_MIN_MSAT; + } + hop + }) + .chain(second_leg.paths[0].hops.clone().into_iter().map(|mut hop| { + if let SwapType::BuyAsset { amount_rgb, .. } = swaptype { + hop.rgb_amount = Some(amount_rgb); + hop.payment_amount = HTLC_MIN_MSAT; + } + hop + })) + .collect::>(); + + let route = Route { + paths: vec![LnPath { hops: fullpaths, blinded_tail: None }], + payment_params: Some(PaymentParameters::for_keysend(channel_manager.get_our_node_id(), 40)), + }; + + if let SwapType::SellAsset { amount_rgb, .. } = swaptype { + write_rgb_payment_info_file(&ldk_data_dir, &payment_hash, asset_id, amount_rgb, None); + } + + let status = match channel_manager.send_spontaneous_payment( + &route, + Some(payment_preimage), + RecipientOnionFields::spontaneous_empty(), + PaymentId(payment_hash.0), + ) { + Ok(_payment_hash) => { + println!("EVENT: initiated swap"); + print!("> "); + HTLCStatus::Pending + } + Err(e) => { + println!("ERROR: failed to send payment: {:?}", e); + print!("> "); + HTLCStatus::Failed + } + }; + + let mut payments = payment_storage.lock().unwrap(); + payments.insert( + payment_hash, + PaymentInfo { + preimage: None, + secret: None, + status, + amt_msat: MillisatAmount(Some(swaptype.amount_msats())), + }, + ); +} + +/// Return the price in msats/asset +async fn fetch_price(asset_id: &ContractId) -> Result> { + #[derive(Debug, serde::Deserialize)] + struct BitfinexPrice { + last_price: String, + } + + // TODO: map the asset to the right ticker. here we assume it's always USDt + // See: https://github.com/RGB-Tools/rgb-lightning-sample/issues/6 + let body = reqwest::get("https://api.bitfinex.com/v1/pubticker/btcust") + .await? + .json::() + .await?; + + let last_price = body.last_price.parse::()?; + if last_price == 0.0 { + return Err(Box::::from( + "BTC/USDt price fetched from bitfinex is zero", + )); + } + let price = (1.0 / last_price * 1e11) as u64; + + println!("Using price from Bitfinex: {} mSAT = 1 {}", price, asset_id); + + Ok(price) +} + pub(crate) async fn connect_peer_if_necessary( pubkey: PublicKey, peer_addr: SocketAddr, peer_manager: Arc, ) -> Result<(), ()> { @@ -1442,8 +1976,14 @@ fn do_disconnect_peer( fn open_channel( peer_pubkey: PublicKey, channel_amt_sat: u64, push_amt_msat: u64, announced_channel: bool, - channel_manager: Arc, proxy_endpoint: &str, + channel_manager: Arc, proxy_endpoint: &str, is_colored: bool, + wallet: Arc>>, electrum_url: &str, ) -> Result<[u8; 32], ()> { + { + let wallet = wallet.lock().unwrap(); + sync_wallet(&wallet, electrum_url.to_string()); + } + let config = UserConfig { channel_handshake_limits: ChannelHandshakeLimits { // lnd's max to_self_delay is 2016, so we want to be compatible. @@ -1458,7 +1998,9 @@ fn open_channel( ..Default::default() }; - let consignment_endpoint = RgbTransport::from_str(proxy_endpoint).unwrap(); + let consignment_endpoint = + if is_colored { Some(RgbTransport::from_str(proxy_endpoint).unwrap()) } else { None }; + match channel_manager.create_channel( peer_pubkey, channel_amt_sat, @@ -1483,12 +2025,15 @@ fn send_payment( ldk_data_dir: PathBuf, ) { let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner()); - write_rgb_payment_info_file( - &ldk_data_dir, - &payment_hash, - invoice.rgb_contract_id().unwrap(), - invoice.rgb_amount().unwrap(), - ); + if let Some(contract_id) = invoice.rgb_contract_id() { + write_rgb_payment_info_file( + &ldk_data_dir, + &payment_hash, + contract_id, + invoice.rgb_amount().unwrap(), + Some(invoice.rgb_amount().unwrap()), + ); + } let status = match pay_invoice(invoice, Retry::Timeout(Duration::from_secs(10)), channel_manager) { Ok(_payment_id) => { @@ -1520,12 +2065,20 @@ fn send_payment( fn keysend( channel_manager: &ChannelManager, payee_pubkey: PublicKey, amt_msat: u64, entropy_source: &E, - payment_storage: PaymentInfoStorage, contract_id: ContractId, amt_rgb: u64, + payment_storage: PaymentInfoStorage, rgb_payment: Option<(ContractId, u64)>, ldk_data_dir: PathBuf, ) { let payment_preimage = PaymentPreimage(entropy_source.get_secure_random_bytes()); let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).into_inner()); - write_rgb_payment_info_file(&ldk_data_dir, &payment_hash, contract_id, amt_rgb); + if let Some((contract_id, amt_rgb)) = rgb_payment { + write_rgb_payment_info_file( + &ldk_data_dir, + &payment_hash, + contract_id, + amt_rgb, + Some(amt_rgb), + ); + } let route_params = RouteParameters { payment_params: PaymentParameters::for_keysend(payee_pubkey, 40), @@ -1565,7 +2118,7 @@ fn keysend( fn get_invoice( amt_msat: u64, payment_storage: PaymentInfoStorage, channel_manager: &ChannelManager, keys_manager: Arc, network: Network, expiry_secs: u32, - logger: Arc, contract_id: ContractId, amt_rgb: u64, + logger: Arc, contract_id: Option, amt_rgb: Option, ) { let mut payments = payment_storage.lock().unwrap(); let currency = match network { @@ -1583,8 +2136,8 @@ fn get_invoice( "ldk-tutorial-node".to_string(), expiry_secs, None, - Some(contract_id), - Some(amt_rgb), + contract_id, + amt_rgb, ) { Ok(inv) => { println!("SUCCESS: generated invoice: {}", inv); diff --git a/src/main.rs b/src/main.rs index ed78433..0cce276 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,12 +9,15 @@ mod error; mod hex_utils; mod proxy; mod rgb_utils; +mod swap; use crate::bdk_utils::{broadcast_tx, get_bdk_wallet, get_bdk_wallet_seckey, sync_wallet}; use crate::bitcoind_client::BitcoindClient; +use crate::cli::HTLC_MIN_MSAT; use crate::disk::FilesystemLogger; use crate::proxy::post_consignment; use crate::rgb_utils::{get_asset_owned_values, update_transition_beneficiary, RgbUtilities}; +use crate::swap::SwapType; use bdk::bitcoin::psbt::PartiallySignedTransaction; use bdk::bitcoin::OutPoint; use bdk::bitcoin::Txid; @@ -46,12 +49,13 @@ use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler, Simple use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning::onion_message::SimpleArcOnionMessenger; use lightning::rgb_utils::{ - drop_rgb_runtime, get_rgb_channel_info, get_rgb_runtime, read_rgb_transfer_info, RgbUtxo, - RgbUtxos, STATIC_BLINDING, + drop_rgb_runtime, get_rgb_channel_info, get_rgb_runtime, is_channel_rgb, is_transfer_colored, + read_rgb_transfer_info, RgbInfo, RgbUtxo, RgbUtxos, STATIC_BLINDING, }; use lightning::routing::gossip; use lightning::routing::gossip::{NodeId, P2PGossipSync}; use lightning::routing::router::DefaultRouter; +use lightning::routing::scoring::ProbabilisticScorerUsingTime; use lightning::util::config::UserConfig; use lightning::util::ser::ReadableArgs; use lightning_background_processor::{process_events_async, GossipSync}; @@ -67,8 +71,8 @@ use rgb::validation::ConsignmentApi; use rgb_core::Assign; use rgb_schemata::{nia_rgb20, nia_schema}; use rgbstd::containers::{Bindle, BuilderSeal, Transfer as RgbTransfer}; -use rgbstd::contract::GraphSeal; -use rgbstd::interface::{rgb20, TypedState}; +use rgbstd::contract::{ContractId, GraphSeal}; +use rgbstd::interface::{rgb20, TransitionBuilder, TypedState}; use rgbstd::persistence::{Inventory, Stash}; use rgbstd::validation::Validity; use rgbstd::{Chain, Txid as RgbTxid}; @@ -142,6 +146,12 @@ pub(crate) type PeerManager = SimpleArcPeerManager< FilesystemLogger, >; +pub(crate) type Scorer = + ProbabilisticScorerUsingTime, Arc, std::time::Instant>; + +pub(crate) type Router = + DefaultRouter, Arc, Arc>>; + pub(crate) type ChannelManager = SimpleArcChannelManager; @@ -155,6 +165,8 @@ async fn handle_ldk_events( outbound_payments: &PaymentInfoStorage, network: Network, event: Event, ldk_data_dir: String, proxy_client: Arc, proxy_url: String, wallet_arc: Arc>>, electrum_url: String, + whitelisted_trades: &Arc>>, + maker_trades: &Arc>>, ) { match event { Event::FundingGenerationReady { @@ -164,6 +176,15 @@ async fn handle_ldk_events( output_script, .. } => { + struct RgbState { + rgb_change_amount: u64, + channel_rgb_amount: u64, + rgb_inputs: Vec, + rgb_info: RgbInfo, + asset_transition_builder: TransitionBuilder, + assignment_id: u16, + } + let addr = WitnessProgram::from_scriptpubkey( &output_script[..], match network { @@ -177,47 +198,64 @@ async fn handle_ldk_events( .to_scriptpubkey(); let script = Script::from_byte_iter(addr.into_iter().map(Ok)).expect("valid script"); - let (rgb_info, _) = - get_rgb_channel_info(&temporary_channel_id, &PathBuf::from(&ldk_data_dir)); - - let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + let ldk_data_dir = PathBuf::from(&ldk_data_dir); + let is_colored = is_channel_rgb(&temporary_channel_id, &ldk_data_dir); + let mut beneficiaries = vec![]; + let rgb_state = if is_colored { + let (rgb_info, _) = + get_rgb_channel_info(&temporary_channel_id, &PathBuf::from(&ldk_data_dir)); - let channel_rgb_amount: u64 = rgb_info.local_rgb_amount; - let asset_owned_values = get_asset_owned_values( - rgb_info.contract_id, - &runtime, - wallet_arc.clone(), - electrum_url, - ) - .expect("known contract"); + let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); - let mut asset_transition_builder = runtime - .transition_builder( + let channel_rgb_amount: u64 = rgb_info.local_rgb_amount; + let asset_owned_values = get_asset_owned_values( rgb_info.contract_id, - TypeName::try_from("RGB20").unwrap(), - None::<&str>, + &runtime, + wallet_arc.clone(), + electrum_url, ) - .expect("ok"); - let assignment_id = asset_transition_builder - .assignments_type(&FieldName::from("beneficiary")) - .expect("valid assignment"); - let mut beneficiaries = vec![]; + .expect("known contract"); + + let asset_transition_builder = runtime + .transition_builder( + rgb_info.contract_id, + TypeName::try_from("RGB20").unwrap(), + None::<&str>, + ) + .expect("ok"); + let assignment_id = asset_transition_builder + .assignments_type(&FieldName::from("beneficiary")) + .expect("valid assignment"); - let mut rgb_inputs: Vec = vec![]; - let mut input_amount: u64 = 0; - for (_opout, (outpoint, amount)) in asset_owned_values { - if input_amount >= channel_rgb_amount { - break; + let mut rgb_inputs: Vec = vec![]; + let mut input_amount: u64 = 0; + for (_opout, (outpoint, amount)) in asset_owned_values { + if input_amount >= channel_rgb_amount { + break; + } + rgb_inputs.push(OutPoint { + txid: Txid::from_str(&outpoint.txid.to_string()).unwrap(), + vout: outpoint.vout.into_u32(), + }); + input_amount += amount; } - rgb_inputs.push(OutPoint { - txid: Txid::from_str(&outpoint.txid.to_string()).unwrap(), - vout: outpoint.vout.into_u32(), - }); - input_amount += amount; - } - let rgb_change_amount = input_amount - channel_rgb_amount; + let rgb_change_amount = input_amount - channel_rgb_amount; + drop(runtime); + drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + + Some(RgbState { + channel_rgb_amount, + rgb_change_amount, + rgb_inputs, + rgb_info, + asset_transition_builder, + assignment_id, + }) + } else { + None + }; - let rgb_utxos_path = format!("{}/rgb_utxos", ldk_data_dir); + let rgb_utxos_path = format!("{}/rgb_utxos", ldk_data_dir.display()); let serialized_utxos = fs::read_to_string(&rgb_utxos_path).expect("able to read rgb utxos file"); let mut rgb_utxos: RgbUtxos = @@ -225,14 +263,20 @@ async fn handle_ldk_events( let unspendable_utxos: Vec = rgb_utxos .utxos .iter() - .filter(|u| !rgb_inputs.contains(&u.outpoint) || !u.colored) + .filter(|u| { + rgb_state + .as_ref() + .map(|state| !state.rgb_inputs.contains(&u.outpoint) || !u.colored) + .unwrap_or(true) + }) .map(|u| u.outpoint) .collect(); let wallet = wallet_arc.lock().unwrap(); let mut builder = wallet.build_tx(); + if let Some(rgb_state) = &rgb_state { + builder.add_utxos(&rgb_state.rgb_inputs).expect("valid utxos"); + } builder - .add_utxos(&rgb_inputs) - .expect("valid utxos") .unspendable(unspendable_utxos) .fee_rate(FeeRate::from_sat_per_vb(FEE_RATE)) .ordering(bdk::wallet::tx_builder::TxOrdering::Untouched) @@ -246,97 +290,117 @@ async fn handle_ldk_events( ) .add_data(&[1]); - let funding_seal = BuilderSeal::Revealed(GraphSeal::with_vout( - CloseMethod::OpretFirst, - 0, - STATIC_BLINDING, - )); - beneficiaries.push(funding_seal); - asset_transition_builder = asset_transition_builder - .add_raw_state_static( - assignment_id, - funding_seal, - TypedState::Amount(channel_rgb_amount), - ) - .expect("ok"); - - let change_vout = 2; - if rgb_change_amount > 0 { - let change_seal = BuilderSeal::Revealed(GraphSeal::with_vout( - CloseMethod::OpretFirst, - change_vout, - STATIC_BLINDING, - )); - beneficiaries.push(change_seal); - asset_transition_builder = asset_transition_builder - .add_raw_state_static( - assignment_id, - change_seal, - TypedState::Amount(rgb_change_amount), - ) - .expect("ok"); - } - let psbt = builder.finish().expect("valid psbt finish").0; - let (mut psbt, consignment) = runtime.send_rgb( - rgb_info.contract_id, - psbt, - asset_transition_builder, - beneficiaries, - ); + let (mut psbt, state_and_consignment_and_change_vout) = match rgb_state { + Some(mut rgb_state) => { + let funding_seal = BuilderSeal::Revealed(GraphSeal::with_vout( + CloseMethod::OpretFirst, + 0, + STATIC_BLINDING, + )); + beneficiaries.push(funding_seal); + rgb_state.asset_transition_builder = rgb_state + .asset_transition_builder + .add_raw_state_static( + rgb_state.assignment_id, + funding_seal, + TypedState::Amount(rgb_state.channel_rgb_amount), + ) + .expect("ok"); + + let change_vout = 2; + if rgb_state.rgb_change_amount > 0 { + let change_seal = BuilderSeal::Revealed(GraphSeal::with_vout( + CloseMethod::OpretFirst, + change_vout, + STATIC_BLINDING, + )); + beneficiaries.push(change_seal); + rgb_state.asset_transition_builder = rgb_state + .asset_transition_builder + .add_raw_state_static( + rgb_state.assignment_id, + change_seal, + TypedState::Amount(rgb_state.rgb_change_amount), + ) + .expect("ok"); + } + let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + + let (psbt, consignment) = runtime.send_rgb( + rgb_state.rgb_info.contract_id, + psbt, + rgb_state.asset_transition_builder.clone(), + beneficiaries, + ); + drop(runtime); + drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + (psbt, Some((rgb_state, consignment, change_vout))) + } + None => (psbt, None), + }; // Sign the final funding transaction wallet.sign(&mut psbt, SignOptions::default()).expect("able to sign"); let funding_tx = psbt.extract_tx(); let funding_txid = funding_tx.txid(); - let consignment_path = format!("{}/consignment_{funding_txid}", ldk_data_dir); - consignment.save(&consignment_path).expect("successful save"); - - if rgb_change_amount > 0 { - let rgb_change_utxo = RgbUtxo { - outpoint: OutPoint { txid: funding_txid, vout: change_vout }, - colored: true, - }; - rgb_utxos.utxos.push(rgb_change_utxo); - let serialized_utxos = serde_json::to_string(&rgb_utxos).expect("valid rgb utxos"); - fs::write(rgb_utxos_path, serialized_utxos).expect("able to write rgb utxos file"); + let consignment_path = format!("{}/consignment_{funding_txid}", ldk_data_dir.display()); + if let Some((rgb_state, consignment, change_vout)) = + &state_and_consignment_and_change_vout + { + consignment.save(&consignment_path).expect("successful save"); + + if rgb_state.rgb_change_amount > 0 { + let rgb_change_utxo = RgbUtxo { + outpoint: OutPoint { txid: funding_txid, vout: *change_vout }, + colored: true, + }; + rgb_utxos.utxos.push(rgb_change_utxo); + let serialized_utxos = + serde_json::to_string(&rgb_utxos).expect("valid rgb utxos"); + fs::write(rgb_utxos_path, serialized_utxos) + .expect("able to write rgb utxos file"); + } + let funding_consignment_path = format!( + "{}/consignment_{}", + ldk_data_dir.display(), + hex::encode(temporary_channel_id) + ); + consignment.save(funding_consignment_path).expect("successful save"); } - let funding_consignment_path = - format!("{}/consignment_{}", ldk_data_dir, hex::encode(temporary_channel_id)); - consignment.save(funding_consignment_path).expect("successful save"); - drop(runtime); - drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); let proxy_ref = (*proxy_client).clone(); let proxy_url_copy = proxy_url; let channel_manager_copy = channel_manager.clone(); tokio::spawn(async move { - let res = post_consignment( - proxy_ref, - &proxy_url_copy, - funding_txid.to_string(), - consignment_path.into(), - ) - .await; - if res.is_err() || res.unwrap().result.is_none() { - return; - } + if let Some((_, consignment, _)) = state_and_consignment_and_change_vout { + let res = post_consignment( + proxy_ref, + &proxy_url_copy, + funding_txid.to_string(), + consignment_path.into(), + ) + .await; + if res.is_err() || res.unwrap().result.is_none() { + return; + } - let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); - let transfer: RgbTransfer = consignment.unbindle(); - let validated_transfer = match transfer.validate(runtime.resolver()) { - Ok(consignment) => consignment, - Err(consignment) => consignment, - }; - let validation_status = validated_transfer.into_validation_status().unwrap(); - let validity = validation_status.validity(); - if !vec![Validity::Valid, Validity::UnminedTerminals].contains(&validity) { - return; - } + let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + let transfer: RgbTransfer = consignment.unbindle(); + let validated_transfer = match transfer.validate(runtime.resolver()) { + Ok(consignment) => consignment, + Err(consignment) => consignment, + }; + let validation_status = validated_transfer.into_validation_status().unwrap(); + let validity = validation_status.validity(); + if !vec![Validity::Valid, Validity::UnminedTerminals].contains(&validity) { + return; + } - drop(runtime); - drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + drop(runtime); + drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + } // Give the funding transaction back to LDK for opening the channel. if channel_manager_copy @@ -412,6 +476,8 @@ async fn handle_ldk_events( } } println!("Event::PaymentClaimed end"); + + maker_trades.lock().unwrap().remove(&payment_hash); } Event::PaymentSent { payment_preimage, payment_hash, fee_paid_msat, .. } => { let mut payments = outbound_payments.lock().unwrap(); @@ -553,90 +619,109 @@ async fn handle_ldk_events( let txid = outpoint.txid; let witness_txid = RgbTxid::from_str(&txid.to_string()).unwrap(); + let rgb_inputs: Vec = + vec![OutPoint { txid, vout: outpoint.index as u32 }]; + let transfer_info_path = format!("{ldk_data_dir}/{txid}_transfer_info"); - let transfer_info = read_rgb_transfer_info(&transfer_info_path); - let contract_id = transfer_info.contract_id; - - runtime.consume_anchor(transfer_info.anchor).expect("should consume anchor"); - for (id, bundle) in transfer_info.bundles { - runtime - .consume_bundle(id, bundle, witness_txid) - .expect("should consume bundle"); - } - let seal_holder = BuilderSeal::Revealed(GraphSeal::with_vout( - CloseMethod::OpretFirst, - transfer_info.vout, - STATIC_BLINDING, - )); - let seal_counterparty = BuilderSeal::Revealed(GraphSeal::with_vout( - CloseMethod::OpretFirst, - transfer_info.vout ^ 1, - STATIC_BLINDING, - )); - let beneficiaries = vec![seal_holder, seal_counterparty]; - let beneficiaries: Vec> = beneficiaries - .into_iter() - .map(|b| match b { - BuilderSeal::Revealed(graph_seal) => { - BuilderSeal::Revealed(graph_seal.resolve(witness_txid)) + let (is_colored, asset_transition_builder, assignment_id, amt_rgb, contract_id) = + if is_transfer_colored(&transfer_info_path) { + let transfer_info = read_rgb_transfer_info(&transfer_info_path); + let contract_id = transfer_info.contract_id; + + runtime + .consume_anchor(transfer_info.anchor) + .expect("should consume anchor"); + for (id, bundle) in transfer_info.bundles { + runtime + .consume_bundle(id, bundle, witness_txid) + .expect("should consume bundle"); + } + let seal_holder = BuilderSeal::Revealed(GraphSeal::with_vout( + CloseMethod::OpretFirst, + transfer_info.vout, + STATIC_BLINDING, + )); + let seal_counterparty = BuilderSeal::Revealed(GraphSeal::with_vout( + CloseMethod::OpretFirst, + transfer_info.vout ^ 1, + STATIC_BLINDING, + )); + let beneficiaries = vec![seal_holder, seal_counterparty]; + let beneficiaries: Vec> = beneficiaries + .into_iter() + .map(|b| match b { + BuilderSeal::Revealed(graph_seal) => { + BuilderSeal::Revealed(graph_seal.resolve(witness_txid)) + } + BuilderSeal::Concealed(seal) => BuilderSeal::Concealed(seal), + }) + .collect(); + let consignment = + runtime.transfer(contract_id, beneficiaries).expect("valid transfer"); + let transfer: RgbTransfer = consignment.clone().unbindle(); + + let validated_transfer = transfer + .clone() + .validate(runtime.resolver()) + .expect("invalid contract"); + let status = runtime + .accept_transfer(validated_transfer.clone(), true) + .expect("valid transfer"); + let validity = status.validity(); + if !matches!(validity, Validity::Valid) { + println!("WARNING: error consuming transfer"); + continue; } - BuilderSeal::Concealed(seal) => BuilderSeal::Concealed(seal), - }) - .collect(); - let consignment = - runtime.transfer(contract_id, beneficiaries).expect("valid transfer"); - let transfer: RgbTransfer = consignment.clone().unbindle(); - - let validated_transfer = - transfer.clone().validate(runtime.resolver()).expect("invalid contract"); - let status = runtime - .accept_transfer(validated_transfer.clone(), true) - .expect("valid transfer"); - let validity = status.validity(); - if !matches!(validity, Validity::Valid) { - println!("WARNING: error consuming transfer"); - continue; - } - let wallet = wallet_arc.lock().unwrap(); - let address = wallet.get_address(AddressIndex::New).expect("valid address").address; - let rgb_inputs: Vec = - vec![OutPoint { txid: outpoint.txid, vout: outpoint.index as u32 }]; - - let bundle = &transfer - .anchored_bundles() - .find(|ab| ab.anchor.txid.to_string() == outpoint.txid.to_string()) - .expect("found bundle for closing tx") - .bundle; - - let mut amt_rgb = 0; - for bundle_item in bundle.values() { - if let Some(transition) = &bundle_item.transition { - for assignment in transition.assignments.values() { - for fungible_assignment in assignment.as_fungible() { - if let Assign::Revealed { seal, state } = fungible_assignment { - if seal.vout == (outpoint.index as u32).into() { - amt_rgb += state.value.as_u64(); + let bundle = &transfer + .anchored_bundles() + .find(|ab| ab.anchor.txid.to_string() == outpoint.txid.to_string()) + .expect("found bundle for closing tx") + .bundle; + + let mut amt_rgb = 0; + for bundle_item in bundle.values() { + if let Some(transition) = &bundle_item.transition { + for assignment in transition.assignments.values() { + for fungible_assignment in assignment.as_fungible() { + if let Assign::Revealed { seal, state } = + fungible_assignment + { + if seal.vout == (outpoint.index as u32).into() { + amt_rgb += state.value.as_u64(); + } + }; } - }; + } } } - } - } - let asset_transition_builder = runtime - .transition_builder( - contract_id, - TypeName::try_from("RGB20").unwrap(), - None::<&str>, - ) - .expect("ok"); - let assignment_id = asset_transition_builder - .assignments_type(&FieldName::from("beneficiary")) - .expect("valid assignment"); - let mut beneficiaries = vec![]; + let asset_transition_builder = runtime + .transition_builder( + contract_id, + TypeName::try_from("RGB20").unwrap(), + None::<&str>, + ) + .expect("ok"); + let assignment_id = asset_transition_builder + .assignments_type(&FieldName::from("beneficiary")) + .expect("valid assignment"); + + ( + true, + Some(asset_transition_builder), + Some(assignment_id), + Some(amt_rgb), + Some(contract_id), + ) + } else { + (false, None, None, None, None) + }; - let (tx, vout, consignment) = match outp { + let wallet = wallet_arc.lock().unwrap(); + let address = wallet.get_address(AddressIndex::New).expect("valid address").address; + + let (tx, rgb_vars) = match outp { SpendableOutputDescriptor::StaticPaymentOutput(descriptor) => { let signer = keys_manager.derive_channel_keys( descriptor.channel_value_satoshis, @@ -652,31 +737,41 @@ async fn handle_ldk_events( builder .add_utxos(&rgb_inputs) .expect("valid utxos") - .add_data(&[1]) .fee_rate(FeeRate::from_sat_per_vb(FEE_RATE)) .manually_selected_only() .drain_to(address.script_pubkey()); + + if is_colored { + builder.add_data(&[1]); + } + let psbt = builder.finish().expect("valid psbt finish").0; - let (vout, asset_transition_builder) = update_transition_beneficiary( - &psbt, - &mut beneficiaries, - asset_transition_builder, - assignment_id, - amt_rgb, - ); - let (mut psbt, consignment) = runtime.send_rgb( - contract_id, - psbt, - asset_transition_builder, - beneficiaries, - ); + let (mut psbt, rgb_vars) = if is_colored { + let mut beneficiaries = vec![]; + let (vout, asset_transition_builder) = update_transition_beneficiary( + &psbt, + &mut beneficiaries, + asset_transition_builder.unwrap(), + assignment_id.unwrap(), + amt_rgb.unwrap(), + ); + let (psbt, consignment) = runtime.send_rgb( + contract_id.unwrap(), + psbt, + asset_transition_builder, + beneficiaries, + ); + (psbt, Some((vout, consignment))) + } else { + (psbt, None) + }; intermediate_wallet .sign(&mut psbt, SignOptions::default()) .expect("able to sign"); - (psbt.extract_tx(), vout, consignment) + (psbt.extract_tx(), rgb_vars) } SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) => { let signer = keys_manager.derive_channel_keys( @@ -711,19 +806,25 @@ async fn handle_ldk_events( let psbt = PartiallySignedTransaction::from_unsigned_tx(spend_tx.clone()) .expect("valid transaction"); - let (vout, asset_transition_builder) = update_transition_beneficiary( - &psbt, - &mut beneficiaries, - asset_transition_builder, - assignment_id, - amt_rgb, - ); - let (psbt, consignment) = runtime.send_rgb( - contract_id, - psbt, - asset_transition_builder, - beneficiaries, - ); + let (psbt, rgb_vars) = if is_colored { + let mut beneficiaries = vec![]; + let (vout, asset_transition_builder) = update_transition_beneficiary( + &psbt, + &mut beneficiaries, + asset_transition_builder.unwrap(), + assignment_id.unwrap(), + amt_rgb.unwrap(), + ); + let (psbt, consignment) = runtime.send_rgb( + contract_id.unwrap(), + psbt, + asset_transition_builder, + beneficiaries, + ); + (psbt, Some((vout, consignment))) + } else { + (psbt, None) + }; let mut spend_tx = psbt.extract_tx(); let input_idx = 0; @@ -732,7 +833,7 @@ async fn handle_ldk_events( .expect("possible dynamic sign"); spend_tx.input[input_idx].witness = Witness::from_vec(witness_vec); - (spend_tx, vout, consignment) + (spend_tx, rgb_vars) } SpendableOutputDescriptor::StaticOutput { outpoint: _, ref output } => { let derivation_idx = @@ -764,47 +865,59 @@ async fn handle_ldk_events( .drain_to(address.script_pubkey()); let psbt = builder.finish().expect("valid psbt finish").0; - let (vout, asset_transition_builder) = update_transition_beneficiary( - &psbt, - &mut beneficiaries, - asset_transition_builder, - assignment_id, - amt_rgb, - ); - let (mut psbt, consignment) = runtime.send_rgb( - contract_id, - psbt, - asset_transition_builder, - beneficiaries, - ); + let (mut psbt, rgb_vars) = if is_colored { + let mut beneficiaries = vec![]; + let (vout, asset_transition_builder) = update_transition_beneficiary( + &psbt, + &mut beneficiaries, + asset_transition_builder.unwrap(), + assignment_id.unwrap(), + amt_rgb.unwrap(), + ); + let (psbt, consignment) = runtime.send_rgb( + contract_id.unwrap(), + psbt, + asset_transition_builder, + beneficiaries, + ); + (psbt, Some((vout, consignment))) + } else { + (psbt, None) + }; intermediate_wallet .sign(&mut psbt, SignOptions::default()) .expect("able to sign"); - (psbt.extract_tx(), vout, consignment) + (psbt.extract_tx(), rgb_vars) } }; broadcast_tx(&tx, electrum_url.clone()); sync_wallet(&wallet, electrum_url.clone()); - let rgb_utxos_path = format!("{}/rgb_utxos", ldk_data_dir); - let serialized_utxos = - fs::read_to_string(&rgb_utxos_path).expect("able to read rgb utxos file"); - let mut rgb_utxos: RgbUtxos = - serde_json::from_str(&serialized_utxos).expect("valid rgb utxos"); - rgb_utxos - .utxos - .push(RgbUtxo { outpoint: OutPoint { txid: tx.txid(), vout }, colored: true }); - let serialized_utxos = serde_json::to_string(&rgb_utxos).expect("valid rgb utxos"); - fs::write(rgb_utxos_path, serialized_utxos).expect("able to write rgb utxos file"); - - let transfer: RgbTransfer = consignment.unbindle(); - let validated_transfer = - transfer.clone().validate(runtime.resolver()).expect("invalid contract"); - let _status = - runtime.accept_transfer(validated_transfer, true).expect("valid consignment"); + if let Some((vout, consignment)) = rgb_vars { + let rgb_utxos_path = format!("{}/rgb_utxos", ldk_data_dir); + let serialized_utxos = + fs::read_to_string(&rgb_utxos_path).expect("able to read rgb utxos file"); + let mut rgb_utxos: RgbUtxos = + serde_json::from_str(&serialized_utxos).expect("valid rgb utxos"); + rgb_utxos.utxos.push(RgbUtxo { + outpoint: OutPoint { txid: tx.txid(), vout }, + colored: true, + }); + let serialized_utxos = + serde_json::to_string(&rgb_utxos).expect("valid rgb utxos"); + fs::write(rgb_utxos_path, serialized_utxos) + .expect("able to write rgb utxos file"); + + let transfer: RgbTransfer = consignment.unbindle(); + let validated_transfer = + transfer.clone().validate(runtime.resolver()).expect("invalid contract"); + let _status = runtime + .accept_transfer(validated_transfer, true) + .expect("valid consignment"); + } } drop(runtime); drop_rgb_runtime(&PathBuf::from(ldk_data_dir)); @@ -831,22 +944,26 @@ async fn handle_ldk_events( hex_utils::hex_str(channel_id), hex_utils::hex_str(&counterparty_node_id.serialize()), ); - let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); + let is_colored = is_channel_rgb(&channel_id, &ldk_data_dir.clone().into()); + if is_colored { + let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); - let funding_consignment_path = - format!("{}/consignment_{}", ldk_data_dir, hex::encode(channel_id)); + let funding_consignment_path = + format!("{}/consignment_{}", ldk_data_dir, hex::encode(channel_id)); - let funding_consignment_bindle = Bindle::::load(funding_consignment_path) - .expect("successful consignment load"); - let transfer: RgbTransfer = funding_consignment_bindle.unbindle(); + let funding_consignment_bindle = + Bindle::::load(funding_consignment_path) + .expect("successful consignment load"); + let transfer: RgbTransfer = funding_consignment_bindle.unbindle(); - let validated_transfer = - transfer.validate(runtime.resolver()).expect("invalid contract"); - let _status = - runtime.accept_transfer(validated_transfer, true).expect("valid consignment"); + let validated_transfer = + transfer.validate(runtime.resolver()).expect("invalid contract"); + let _status = + runtime.accept_transfer(validated_transfer, true).expect("valid consignment"); - drop(runtime); - drop_rgb_runtime(&PathBuf::from(ldk_data_dir)); + drop(runtime); + drop_rgb_runtime(&PathBuf::from(ldk_data_dir)); + } print!("> "); io::stdout().flush().unwrap(); @@ -865,7 +982,108 @@ async fn handle_ldk_events( // A "real" node should probably "lock" the UTXOs spent in funding transactions until // the funding transaction either confirms, or this event is generated. } - Event::HTLCIntercepted { .. } => {} + Event::HTLCIntercepted { + is_swap, + payment_hash, + intercept_id, + inbound_amount_msat, + expected_outbound_amount_msat, + inbound_rgb_amount, + expected_outbound_rgb_amount, + requested_next_hop_scid, + prev_short_channel_id, + } => { + if !is_swap { + channel_manager.fail_intercepted_htlc(intercept_id).unwrap(); + } + + let ldk_data_dir_path = PathBuf::from(ldk_data_dir.clone()); + let get_rgb_info = |channel_id| { + let info_file_path = ldk_data_dir_path.join(hex::encode(channel_id)); + if info_file_path.exists() { + let (rgb_info, _) = get_rgb_channel_info(channel_id, &ldk_data_dir_path); + Some(( + rgb_info.contract_id, + rgb_info.local_rgb_amount, + rgb_info.remote_rgb_amount, + )) + } else { + None + } + }; + + let inbound_channel = channel_manager + .list_channels() + .into_iter() + .find(|details| details.short_channel_id == Some(prev_short_channel_id)) + .expect("Should always be a valid channel"); + let outbound_channel = channel_manager + .list_channels() + .into_iter() + .find(|details| details.short_channel_id == Some(requested_next_hop_scid)) + .expect("Should always be a valid channel"); + + let inbound_rgb_info = get_rgb_info(&inbound_channel.channel_id); + let outbound_rgb_info = get_rgb_info(&outbound_channel.channel_id); + + println!("EVENT: Requested swap with params inbound_msat={} outbound_msat={} inbound_rgb={:?} outbound_rgb={:?} inbound_contract_id={:?}, outbound_contract_id={:?}", inbound_amount_msat, expected_outbound_amount_msat, inbound_rgb_amount, expected_outbound_rgb_amount, inbound_rgb_info.map(|i| i.0), outbound_rgb_info.map(|i| i.0)); + + let mut trades_lock = whitelisted_trades.lock().unwrap(); + let (whitelist_contract_id, whitelist_swap_type) = match trades_lock.get(&payment_hash) + { + None => { + println!("ERROR: rejecting non-whitelisted swap"); + channel_manager.fail_intercepted_htlc(intercept_id).unwrap(); + return; + } + Some(x) => x, + }; + + match whitelist_swap_type { + SwapType::BuyAsset { amount_rgb, amount_msats } => { + // We subtract HTLC_MIN_MSAT because a node receiving an RGB payment also receives that amount of sats with it as the payment amount, + // so we exclude it from the calculation of how many sats we are effectively giving out. + let net_msat_diff = (expected_outbound_amount_msat) + .saturating_sub(inbound_amount_msat.saturating_sub(HTLC_MIN_MSAT)); + + if inbound_rgb_amount != Some(*amount_rgb) + || inbound_rgb_info.map(|x| x.0) != Some(*whitelist_contract_id) + || net_msat_diff != *amount_msats + || outbound_rgb_info.is_some() + { + println!("ERROR: swap doesn't match the whitelisted info, rejecting it"); + channel_manager.fail_intercepted_htlc(intercept_id).unwrap(); + return; + } + } + SwapType::SellAsset { amount_rgb, amount_msats } => { + let net_msat_diff = + inbound_amount_msat.saturating_sub(expected_outbound_amount_msat); + + if expected_outbound_rgb_amount != Some(*amount_rgb) + || outbound_rgb_info.map(|x| x.0) != Some(*whitelist_contract_id) + || net_msat_diff != *amount_msats + || inbound_rgb_info.is_some() + { + println!("ERROR: swap doesn't match the whitelisted info, rejecting it"); + channel_manager.fail_intercepted_htlc(intercept_id).unwrap(); + return; + } + } + } + + println!("Swap is whitelisted, forwarding the htlc..."); + trades_lock.remove(&payment_hash); + + channel_manager + .forward_intercepted_htlc( + intercept_id, + channelmanager::NextHopForward::ShortChannelId(requested_next_hop_scid), + expected_outbound_amount_msat, + expected_outbound_rgb_amount, + ) + .expect("Forward should be valid"); + } } } @@ -1051,6 +1269,7 @@ async fn start_ldk() { // Step 11: Initialize the ChannelManager let mut user_config = UserConfig::default(); user_config.channel_handshake_limits.force_announced_channel_preference = false; + user_config.accept_intercept_htlcs = true; let mut restarting_node = true; let (channel_manager_blockhash, channel_manager) = { if let Ok(mut f) = fs::File::open(format!("{}/manager", ldk_data_dir.clone())) { @@ -1065,7 +1284,7 @@ async fn start_ldk() { fee_estimator.clone(), chain_monitor.clone(), broadcaster.clone(), - router, + router.clone(), logger.clone(), user_config, channel_monitor_mut_references, @@ -1084,7 +1303,7 @@ async fn start_ldk() { fee_estimator.clone(), chain_monitor.clone(), broadcaster.clone(), - router, + router.clone(), logger.clone(), keys_manager.clone(), keys_manager.clone(), @@ -1232,6 +1451,8 @@ async fn start_ldk() { // Step 18: Handle LDK Events let channel_manager_event_listener = Arc::clone(&channel_manager); + let whitelisted_trades = Arc::new(Mutex::new(HashMap::new())); + let maker_trades = Arc::new(Mutex::new(HashMap::new())); let network_graph_event_listener = Arc::clone(&network_graph); let keys_manager_event_listener = Arc::clone(&keys_manager); let inbound_payments_event_listener = Arc::clone(&inbound_payments); @@ -1240,6 +1461,8 @@ async fn start_ldk() { let ldk_data_dir_copy = ldk_data_dir.clone(); let proxy_client_copy = proxy_client.clone(); let wallet_copy = wallet.clone(); + let whitelisted_trades_copy = whitelisted_trades.clone(); + let maker_trades_copy = maker_trades.clone(); let event_handler = move |event: Event| { let channel_manager_event_listener = Arc::clone(&channel_manager_event_listener); let network_graph_event_listener = Arc::clone(&network_graph_event_listener); @@ -1249,6 +1472,8 @@ async fn start_ldk() { let ldk_data_dir_copy = ldk_data_dir_copy.clone(); let proxy_client_copy = proxy_client_copy.clone(); let wallet_copy = wallet_copy.clone(); + let whitelisted_trades_copy = whitelisted_trades_copy.clone(); + let maker_trades_copy = maker_trades_copy.clone(); async move { handle_ldk_events( &channel_manager_event_listener, @@ -1263,6 +1488,8 @@ async fn start_ldk() { proxy_url.to_string(), wallet_copy, electrum_url.to_string(), + &whitelisted_trades_copy, + &maker_trades_copy, ) .await; } @@ -1359,6 +1586,7 @@ async fn start_ldk() { Arc::clone(&keys_manager), Arc::clone(&network_graph), Arc::clone(&onion_messenger), + Arc::clone(&router), inbound_payments, outbound_payments, ldk_data_dir.clone(), @@ -1370,6 +1598,8 @@ async fn start_ldk() { proxy_endpoint, wallet.clone(), electrum_url.to_string(), + whitelisted_trades, + maker_trades, ) .await; diff --git a/src/swap.rs b/src/swap.rs new file mode 100644 index 0000000..b0df174 --- /dev/null +++ b/src/swap.rs @@ -0,0 +1,120 @@ +use std::convert::TryInto; + +use lightning::ln::PaymentHash; +use rgb::contract::ContractId; + +use crate::hex_utils; + +#[derive(Debug, Clone, Copy)] +pub enum SwapType { + BuyAsset { amount_rgb: u64, amount_msats: u64 }, + SellAsset { amount_rgb: u64, amount_msats: u64 }, +} + +impl SwapType { + pub fn opposite(self) -> Self { + match self { + SwapType::BuyAsset { amount_rgb, amount_msats } => { + SwapType::SellAsset { amount_rgb, amount_msats } + } + SwapType::SellAsset { amount_rgb, amount_msats } => { + SwapType::BuyAsset { amount_rgb, amount_msats } + } + } + } + + pub fn is_buy(&self) -> bool { + matches!(self, SwapType::BuyAsset { .. }) + } + + pub fn amount_msats(&self) -> u64 { + match self { + SwapType::BuyAsset { amount_msats, .. } | SwapType::SellAsset { amount_msats, .. } => { + *amount_msats + } + } + } + pub fn amount_rgb(&self) -> u64 { + match self { + SwapType::BuyAsset { amount_rgb, .. } | SwapType::SellAsset { amount_rgb, .. } => { + *amount_rgb + } + } + } + + pub fn side(&self) -> &'static str { + match self { + SwapType::BuyAsset { .. } => "buy", + SwapType::SellAsset { .. } => "sell", + } + } +} + +#[derive(Debug)] +pub struct SwapString { + pub asset_id: ContractId, + pub swap_type: SwapType, + pub expiry: u64, + pub payment_hash: PaymentHash, +} + +impl std::str::FromStr for SwapString { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let mut iter = s.split(":"); + let amount = iter.next(); + let asset_id = iter.next(); + let side = iter.next(); + let price = iter.next(); + let expiry = iter.next(); + let payment_hash = iter.next(); + + if payment_hash.is_none() || iter.next().is_some() { + return Err("Parsing swap string: wrong number of parts"); + } + + let amount = amount.unwrap().parse::(); + let asset_id = ContractId::from_str(asset_id.unwrap()); + let price = price.unwrap().parse::(); + let expiry = expiry.unwrap().parse::(); + let payment_hash = hex_utils::to_vec(payment_hash.unwrap()) + .and_then(|vec| vec.try_into().ok()) + .map(|slice| PaymentHash(slice)); + + if amount.is_err() + || asset_id.is_err() + || price.is_err() + || expiry.is_err() + || payment_hash.is_none() + { + return Err("Parsing swap string: unable to parse parts"); + } + + let amount = amount.unwrap(); + let asset_id = asset_id.unwrap(); + let price = price.unwrap(); + let expiry = expiry.unwrap(); + let payment_hash = payment_hash.unwrap(); + + if amount == 0 || price == 0 || expiry == 0 { + return Err("Parsing swap string: amount, price and expiry should be non-zero"); + } + + let swap_type = match side { + Some("buy") => SwapType::BuyAsset { amount_rgb: amount, amount_msats: amount * price }, + Some("sell") => { + SwapType::SellAsset { amount_rgb: amount, amount_msats: amount * price } + } + _ => { + return Err("Invalid swap type"); + } + }; + + Ok(SwapString { asset_id, swap_type, expiry, payment_hash }) + } +} + +pub fn get_current_timestamp() -> u64 { + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() +} diff --git a/tests/common.sh b/tests/common.sh index d623a00..0f8d719 100644 --- a/tests/common.sh +++ b/tests/common.sh @@ -230,22 +230,31 @@ refresh() { timestamp } -open_channel() { - local src_num dst_num dst_port dst_id rgb_amt current_chan_num +open_colored_channel() { + open_colored_channel_custom_sats_amt $1 $2 $3 $4 $5 30010 $6 +} + +open_big_colored_channel() { + open_colored_channel_custom_sats_amt $1 $2 $3 $4 $5 16777215 $6 +} + +open_colored_channel_custom_sats_amt() { + local src_num dst_num dst_port dst_id rgb_amt sats_amt current_chan_num src_num="$1" dst_num="$2" dst_port="$3" dst_id="$4" rgb_amt="$5" - current_chan_num="${6:-0}" + sats_amt="$6" + current_chan_num="${7:-0}" _tit "open channel from node $src_num to node $dst_num with $rgb_amt assets" - $TMUX_CMD send-keys -t "node$src_num" "openchannel $dst_id@127.0.0.1:$dst_port 30010 1394000 $ASSET_ID $rgb_amt --public" C-m + $TMUX_CMD send-keys -t "node$src_num" "opencoloredchannel $dst_id@127.0.0.1:$dst_port $sats_amt 1394000 $ASSET_ID $rgb_amt --public" C-m check "$src_num" - _wait_for_text_multi $T_1 "node$src_num" "openchannel" "HANDLED ACCEPT CHANNEL" + _wait_for_text_multi $T_1 "node$src_num" "opencoloredchannel" "HANDLED ACCEPT CHANNEL" timestamp - _wait_for_text_multi $T_1 "node$src_num" "openchannel" "FUNDING COMPLETED" + _wait_for_text_multi $T_1 "node$src_num" "opencoloredchannel" "FUNDING COMPLETED" timestamp - _wait_for_text_multi $T_1 "node$src_num" "openchannel" "HANDLED FUNDING SIGNED" + _wait_for_text_multi $T_1 "node$src_num" "opencoloredchannel" "HANDLED FUNDING SIGNED" timestamp check "$dst_num" _wait_for_text $T_1 "node$dst_num" "HANDLED OPEN CHANNEL" @@ -272,6 +281,39 @@ open_channel() { _out "channel ID: $CHANNEL_ID" } +open_vanilla_channel() { + local src_num dst_num dst_port dst_id msat_amount + src_num="$1" + dst_num="$2" + dst_port="$3" + dst_id="$4" + msat_amount="$5" + _tit "open channel from node $src_num to node $dst_num of $msat_amount mSAT" + $TMUX_CMD send-keys -t "node$src_num" "openchannel $dst_id@127.0.0.1:$dst_port $msat_amount 546000 --public" C-m + check $src_num + _wait_for_text_multi $T_1 "node$src_num" "openchannel" "HANDLED ACCEPT CHANNEL" + timestamp + _wait_for_text_multi $T_1 "node$src_num" "openchannel" "FUNDING COMPLETED" + timestamp + _wait_for_text_multi $T_1 "node$src_num" "openchannel" "HANDLED FUNDING SIGNED" + timestamp + check $dst_num + _wait_for_text $T_1 "node$dst_num" "HANDLED OPEN CHANNEL" + timestamp + _wait_for_text_multi $T_1 "node$dst_num" "HANDLED OPEN CHANNEL" "HANDLED FUNDING CREATED" + timestamp + + mine 6 + sleep 3 + + $TMUX_CMD send-keys -t "node$src_num" "listchannels" C-m + sleep 1 + CHANNEL_ID=$(_wait_for_text 5 "node$src_num" "[^_]channel_id:" \ + | head -1 | grep -Eo '[0-9a-f]{64}') + _out "channel ID: $CHANNEL_ID" +} + + list_channels() { local node_num chan_num lines text matches node_num="$1" @@ -373,7 +415,7 @@ forceclose_channel() { mine 1 } -keysend_init() { +colored_keysend_init() { local src_num dst_num dst_id rgb_amt src_num="$1" dst_num="$2" @@ -381,21 +423,21 @@ keysend_init() { rgb_amt="$4" _tit "send $rgb_amt assets off-chain from node $src_num to node $dst_num" - $TMUX_CMD send-keys -t "node$src_num" "keysend $dst_id 3000000 $ASSET_ID $rgb_amt" C-m + $TMUX_CMD send-keys -t "node$src_num" "coloredkeysend $dst_id 3000000 $ASSET_ID $rgb_amt" C-m timestamp check "$src_num" _wait_for_text_multi $T_1 "node$src_num" "keysend" "EVENT: initiated sending" timestamp } -keysend() { +colored_keysend() { local src_num dst_num dst_id rgb_amt src_num="$1" dst_num="$2" dst_id="$3" rgb_amt="$4" - keysend_init "$src_num" "$dst_num" "$dst_id" "$rgb_amt" + colored_keysend_init "$src_num" "$dst_num" "$dst_id" "$rgb_amt" _wait_for_text_multi $T_1 "node$src_num" "keysend" "EVENT: successfully sent payment" timestamp @@ -411,13 +453,68 @@ keysend() { timestamp } -get_invoice() { +keysend_init() { + local src_num dst_num dst_id sats_amt + src_num="$1" + dst_num="$2" + dst_id="$3" + sats_amt="$4" + + _tit "send $sats_amt sats off-chain from node $src_num to node $dst_num" + $TMUX_CMD send-keys -t "node$src_num" "keysend $dst_id $sats_amt" C-m + timestamp + check "$src_num" + _wait_for_text_multi $T_1 "node$src_num" "keysend" "EVENT: initiated sending" + timestamp +} + +keysend() { + local src_num dst_num dst_id sats_amt + src_num="$1" + dst_num="$2" + dst_id="$3" + sats_amt="$4" + + keysend_init "$src_num" "$dst_num" "$dst_id" "$sats_amt" + + _wait_for_text_multi $T_1 "node$src_num" "keysend" "EVENT: successfully sent payment" + timestamp + _wait_for_text_multi $T_1 "node$src_num" "EVENT: successfully sent payment" "HANDLED REVOKE AND ACK" + timestamp + + check "$dst_num" + _wait_for_text $T_1 "node$dst_num" "EVENT: received payment" + timestamp + _wait_for_text_multi $T_1 "node$dst_num" "EVENT: received payment" "Event::PaymentClaimed end" + timestamp + _wait_for_text_multi $T_1 "node$dst_num" "Event::PaymentClaimed end" "HANDLED COMMITMENT SIGNED" + timestamp +} + +get_colored_invoice() { local num rgb_amt text pattern num="$1" rgb_amt="$2" _tit "get invoice for $rgb_amt assets from node $num" - $TMUX_CMD send-keys -t "node$num" "getinvoice 3000000 900 $ASSET_ID $rgb_amt" C-m + $TMUX_CMD send-keys -t "node$num" "getcoloredinvoice 3000000 900 $ASSET_ID $rgb_amt" C-m + timestamp + check "$num" + pattern="SUCCESS: generated invoice: " + INVOICE="$(_wait_for_text_multi $T_1 "node$num" \ + 'getcoloredinvoice' "$pattern" 3 | sed "s/$pattern//" \ + |grep -Eo '^[0-9a-z]+$' | sed -E ':a; N; $!ba; s/[\n ]//g')" + timestamp + _out "invoice: $INVOICE" +} + +get_vanilla_invoice() { + local num msat_amount text pattern + num="$1" + msat_amount="$2" + + _tit "get invoice for $msat_amount mSATs from node $num" + $TMUX_CMD send-keys -t node$num "getinvoice $msat_amount 900" C-m timestamp check "$num" pattern="SUCCESS: generated invoice: " @@ -501,3 +598,156 @@ check_channel_reestablish() { _wait_for_text_multi $T_1 "node$num" "$prevtext" "HANDLED CHANNEL READY" >/dev/null timestamp } + +send_swap() { + local node exchange swaptype amt_msat amt_asset + node="$1" + exchange="$2" + swaptype="$3" + amt_msat="$4" + amt_asset="$5" + + _tit "node $node swapping ($swaptype) $amt_msat msats for $amt_asset $ASSET_ID through node $exchange" + $TMUX_CMD send-keys -t node$node "sendswap $exchange $swaptype $amt_msat $ASSET_ID $amt_asset" C-m + timestamp + check $node + _wait_for_text_multi $T_1 node$node "sendswap" "EVENT: initiated swap" + timestamp + _wait_for_text_multi $T_1 node$node "sendswap" "EVENT: successfully sent payment" + timestamp + _wait_for_text $T_1 node$node "EVENT: received payment" + timestamp + _wait_for_text $T_1 node$node "Event::PaymentClaimed end" + timestamp + _wait_for_text_multi $T_1 node$node "Event::PaymentClaimed end" "HANDLED COMMITMENT SIGNED" + timestamp +} + +maker_init() { + local node amount asset side price timeout + node="$1" + amount="$2" + side="$3" + timeout="$4" + price="$5" + + _tit "node $node initializating trade (mm-side): swapping ($side) $amount of $ASSET_ID at $price msats/asset" + timestamp + $TMUX_CMD send-keys -t node$node "makerinit $amount $ASSET_ID $side $timeout $price" C-m + swap_string=$(_wait_for_text 5 node$node "SUCCESS! swap_string =" |awk '{print $NF}') + payment_secret=$(_wait_for_text 5 node$node "payment_secret: " |awk '{print $NF}') + _out "swap_string: $swap_string" + _out "payment_secret: $payment_secret" + sleep 1 +} +maker_init_amount_failure() { + local node amount asset side price timeout + node="$1" + amount="$2" + side="$3" + timeout="$4" + price="$5" + + _tit "node $node initializating trade (mm-side): swapping ($side) $amount of $ASSET_ID at $price msats/asset" + timestamp + $TMUX_CMD send-keys -t node$node "makerinit $amount $ASSET_ID $side $timeout $price" C-m + _wait_for_text 5 node$node "ERROR: do not have enough RGB assets" +} + +taker() { + local node + node="$1" + + _tit "node $node taking the trade $swap_string" + $TMUX_CMD send-keys -t node$node "taker $swap_string" C-m + taker_pk=$(_wait_for_text 5 node$node "our_pk: " |awk '{print $NF}') + _out "taker_pk: $taker_pk" + sleep 1 +} + +taker_expect_timeout() { + local node + node="$1" + + _tit "node $node taking the trade $swap_string" + $TMUX_CMD send-keys -t node$node "taker $swap_string" C-m + _wait_for_text_multi $T_1 node$node "taker" "ERROR: the swap offer has already expired" + timestamp +} + +taker_amount_failure() { + local node + node="$1" + + _tit "node $node taking the trade $swap_string" + $TMUX_CMD send-keys -t node$node "taker $swap_string" C-m + _wait_for_text 5 node$node "ERROR: do not have enough RGB assets" +} + +taker_list() { + local node text trades_num text + node="$1" + trades_num="$2" + + lines=$((trades_num*9)) + + _tit "listing whitelisted taker trades on node $node" + $TMUX_CMD send-keys -t node$node "tradeslist taker" C-m + text="$(_wait_for_text 5 "node$node" "tradeslist taker" $lines)" + echo "$text" + matches=$(echo "$text" | grep -c "side: .*") + [ "$matches" = "$trades_num" ] || _exit "not enough trades" +} + +maker_list() { + local node text trades_num text + node="$1" + trades_num="$2" + + lines=$((trades_num*10)) + + _tit "listing whitelisted maker trades on node $node" + $TMUX_CMD send-keys -t node$node "tradeslist maker" C-m + text="$(_wait_for_text 5 "node$node" "tradeslist maker" $lines)" + echo "$text" + matches=$(echo "$text" | grep -c "side: .*") + [ "$matches" = "$trades_num" ] || _exit "not enough trades" +} + +maker_execute() { + local node + node="$1" + + _tit "node $node completing the trade..." + $TMUX_CMD send-keys -t node$node "makerexecute $swap_string $payment_secret $taker_pk" C-m + timestamp + _wait_for_text_multi $T_1 node$node "makerexecute" "EVENT: initiated swap" + timestamp + _wait_for_text_multi $T_1 node$node "makerexecute" "EVENT: successfully sent payment" + timestamp + _wait_for_text $T_1 node$node "EVENT: received payment" + timestamp + _wait_for_text $T_1 node$node "Event::PaymentClaimed end" + timestamp + _wait_for_text_multi $T_1 node$node "Event::PaymentClaimed end" "HANDLED COMMITMENT SIGNED" + timestamp + +} + +maker_execute_expect_failure() { + local node taker_pk + node="$1" + taker_pk="$2" + failure_node="$3" + + _tit "node $node completing the trade..." + $TMUX_CMD send-keys -t node$node "makerexecute $swap_string $payment_secret $taker_pk" C-m + timestamp + _wait_for_text_multi $T_1 node$node "makerexecute" "EVENT: initiated swap" + timestamp + _wait_for_text $T_1 node$failure_node "ERROR: rejecting non-whitelisted swap" + timestamp + _wait_for_text_multi $T_1 node$node "makerexecute" "EVENT: Failed to send payment to payment hash .* RetriesExhausted>" + timestamp + +} diff --git a/tests/scripts/close_coop.sh b/tests/scripts/close_coop.sh index 20dd02e..053ec41 100644 --- a/tests/scripts/close_coop.sh +++ b/tests/scripts/close_coop.sh @@ -14,13 +14,13 @@ create_utxos 3 issue_asset # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 list_channels 1 list_channels 2 asset_balance 1 400 # send assets -keysend 1 2 "$NODE2_ID" 100 +colored_keysend 1 2 "$NODE2_ID" 100 list_channels 1 list_channels 2 list_payments 1 diff --git a/tests/scripts/close_coop_nobtc_acceptor.sh b/tests/scripts/close_coop_nobtc_acceptor.sh index 171d7aa..dcc1043 100644 --- a/tests/scripts/close_coop_nobtc_acceptor.sh +++ b/tests/scripts/close_coop_nobtc_acceptor.sh @@ -13,13 +13,13 @@ create_utxos 3 issue_asset # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 list_channels 1 list_channels 2 asset_balance 1 400 # send assets -keysend 1 2 "$NODE2_ID" 100 +colored_keysend 1 2 "$NODE2_ID" 100 list_channels 1 list_channels 2 list_payments 1 diff --git a/tests/scripts/close_coop_other_side.sh b/tests/scripts/close_coop_other_side.sh index 2a3021a..90d4476 100644 --- a/tests/scripts/close_coop_other_side.sh +++ b/tests/scripts/close_coop_other_side.sh @@ -14,13 +14,13 @@ create_utxos 3 issue_asset # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 list_channels 1 list_channels 2 asset_balance 1 400 # send assets -keysend 1 2 "$NODE2_ID" 100 +colored_keysend 1 2 "$NODE2_ID" 100 list_channels 1 list_channels 2 list_payments 1 diff --git a/tests/scripts/close_coop_vanilla.sh b/tests/scripts/close_coop_vanilla.sh new file mode 100644 index 0000000..aa7a734 --- /dev/null +++ b/tests/scripts/close_coop_vanilla.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +source tests/common.sh + + +get_node_ids + +get_address 1 +fund_address $address +mine 1 + +# wait for bdk and ldk to sync up with electrs +sleep 5 + +# open channel +open_vanilla_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 16777215 +list_channels 1 +list_channels 2 + +# get invoice +get_vanilla_invoice 2 3000000 + +# send payment +send_payment 1 2 "$INVOICE" +list_channels 1 +list_channels 2 +list_payments 1 +list_payments 2 + +close_channel 1 2 "$NODE2_ID" \ No newline at end of file diff --git a/tests/scripts/close_coop_zero_balance.sh b/tests/scripts/close_coop_zero_balance.sh index 0dd621f..6d506e7 100644 --- a/tests/scripts/close_coop_zero_balance.sh +++ b/tests/scripts/close_coop_zero_balance.sh @@ -13,7 +13,7 @@ create_utxos 2 issue_asset # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 1000 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 1000 list_channels 1 list_channels 2 asset_balance 1 0 diff --git a/tests/scripts/close_force.sh b/tests/scripts/close_force.sh index 0aadd11..f28cac1 100644 --- a/tests/scripts/close_force.sh +++ b/tests/scripts/close_force.sh @@ -14,13 +14,13 @@ create_utxos 3 issue_asset # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 list_channels 1 list_channels 2 asset_balance 1 400 # send assets -keysend 1 2 "$NODE2_ID" 100 +colored_keysend 1 2 "$NODE2_ID" 100 list_channels 1 list_channels 2 list_payments 1 diff --git a/tests/scripts/close_force_nobtc_acceptor.sh b/tests/scripts/close_force_nobtc_acceptor.sh index ad6ff04..ba21e56 100644 --- a/tests/scripts/close_force_nobtc_acceptor.sh +++ b/tests/scripts/close_force_nobtc_acceptor.sh @@ -14,13 +14,13 @@ create_utxos 3 issue_asset # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 list_channels 1 list_channels 2 asset_balance 1 400 # send assets -keysend 1 2 "$NODE2_ID" 100 +colored_keysend 1 2 "$NODE2_ID" 100 list_channels 1 list_channels 2 list_payments 1 diff --git a/tests/scripts/close_force_other_side.sh b/tests/scripts/close_force_other_side.sh index be2b95e..a316adf 100644 --- a/tests/scripts/close_force_other_side.sh +++ b/tests/scripts/close_force_other_side.sh @@ -14,13 +14,13 @@ create_utxos 3 issue_asset # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 list_channels 1 list_channels 2 asset_balance 1 400 # send assets -keysend 1 2 "$NODE2_ID" 100 +colored_keysend 1 2 "$NODE2_ID" 100 list_channels 1 list_channels 2 list_payments 1 diff --git a/tests/scripts/close_force_pending_htlc.sh b/tests/scripts/close_force_pending_htlc.sh index 6cdda11..3fb257c 100644 --- a/tests/scripts/close_force_pending_htlc.sh +++ b/tests/scripts/close_force_pending_htlc.sh @@ -19,7 +19,7 @@ send_assets 1 400 asset_balance 1 600 # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 channel12_id="$CHANNEL_ID" list_channels 1 list_channels 2 @@ -30,14 +30,14 @@ refresh 2 asset_balance 2 400 # open channel -open_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 400 1 +open_colored_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 400 1 channel23_id="$CHANNEL_ID" list_channels 2 2 list_channels 3 asset_balance 2 0 # send assets and exit intermediate node while payment is goind through -keysend_init 1 3 "$NODE3_ID" 50 +colored_keysend_init 1 3 "$NODE3_ID" 50 _wait_for_text "$T_1" node2 "HANDLED UPDATE ADD HTLC" timestamp _wait_for_text "$T_1" node2 "HANDLED REVOKE AND ACK" diff --git a/tests/scripts/multi_open_close.sh b/tests/scripts/multi_open_close.sh index d58f43e..f74bdb7 100644 --- a/tests/scripts/multi_open_close.sh +++ b/tests/scripts/multi_open_close.sh @@ -14,13 +14,13 @@ create_utxos 3 issue_asset # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 list_channels 1 list_channels 2 asset_balance 1 400 # send assets -keysend 1 2 "$NODE2_ID" 100 +colored_keysend 1 2 "$NODE2_ID" 100 list_channels 1 list_channels 2 list_payments 1 @@ -32,13 +32,13 @@ asset_balance 1 900 asset_balance 2 100 # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 list_channels 1 list_channels 2 asset_balance 1 400 # send assets -keysend 1 2 "$NODE2_ID" 100 +colored_keysend 1 2 "$NODE2_ID" 100 list_channels 1 list_channels 2 list_payments 1 diff --git a/tests/scripts/multihop.sh b/tests/scripts/multihop.sh index a2ab78a..d3f3340 100644 --- a/tests/scripts/multihop.sh +++ b/tests/scripts/multihop.sh @@ -19,7 +19,7 @@ send_assets 1 400 asset_balance 1 600 # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 channel12_id="$CHANNEL_ID" list_channels 1 list_channels 2 @@ -30,14 +30,17 @@ refresh 2 asset_balance 2 400 # open channel -open_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 300 1 +open_colored_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 300 1 channel23_id="$CHANNEL_ID" list_channels 2 2 list_channels 3 asset_balance 2 100 -# send assets -keysend 1 3 "$NODE3_ID" 50 + +# send payment +get_colored_invoice 3 50 +send_payment 1 3 "$INVOICE" + list_channels 1 list_channels 2 2 list_channels 3 diff --git a/tests/scripts/multiple_payments.sh b/tests/scripts/multiple_payments.sh index 141b925..d3cbe76 100644 --- a/tests/scripts/multiple_payments.sh +++ b/tests/scripts/multiple_payments.sh @@ -14,25 +14,25 @@ create_utxos 3 issue_asset # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 list_channels 1 list_channels 2 asset_balance 1 400 # send assets (3 times) -get_invoice 2 1 +get_colored_invoice 2 1 send_payment 1 2 "$INVOICE" list_channels 1 list_channels 2 list_payments 1 list_payments 2 -get_invoice 2 2 +get_colored_invoice 2 2 send_payment 1 2 "$INVOICE" list_channels 1 list_channels 2 list_payments 1 2 list_payments 2 2 -get_invoice 2 3 +get_colored_invoice 2 3 send_payment 1 2 "$INVOICE" list_channels 1 list_channels 2 diff --git a/tests/scripts/open_after_double_send.sh b/tests/scripts/open_after_double_send.sh index 21257dd..fe2ae92 100644 --- a/tests/scripts/open_after_double_send.sh +++ b/tests/scripts/open_after_double_send.sh @@ -27,14 +27,14 @@ refresh 2 asset_balance 2 300 # open channel -open_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 250 +open_colored_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 250 list_channels 1 list_channels 2 asset_balance 1 700 asset_balance 2 50 ## send assets -keysend 2 1 "$NODE1_ID" 50 +colored_keysend 2 1 "$NODE1_ID" 50 list_channels 1 list_channels 2 list_payments 1 diff --git a/tests/scripts/restart.sh b/tests/scripts/restart.sh index c91edcf..802e8b2 100644 --- a/tests/scripts/restart.sh +++ b/tests/scripts/restart.sh @@ -30,7 +30,7 @@ start_node 3 asset_balance 1 1000 # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 _tit "restart nodes" exit_node 1 start_node 1 @@ -46,7 +46,7 @@ list_channels 2 asset_balance 1 400 # send assets -keysend 1 2 "$NODE2_ID" 100 +colored_keysend 1 2 "$NODE2_ID" 100 _tit "restart nodes" exit_node 1 start_node 1 diff --git a/tests/scripts/send_payment.sh b/tests/scripts/send_payment.sh index 3f4ddb3..67f8304 100644 --- a/tests/scripts/send_payment.sh +++ b/tests/scripts/send_payment.sh @@ -14,13 +14,13 @@ create_utxos 3 issue_asset # open channel -open_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 list_channels 1 list_channels 2 asset_balance 1 400 # get invoice -get_invoice 2 100 +get_colored_invoice 2 100 # send payment send_payment 1 2 "$INVOICE" diff --git a/tests/scripts/send_vanilla_payment.sh b/tests/scripts/send_vanilla_payment.sh new file mode 100644 index 0000000..9c3922e --- /dev/null +++ b/tests/scripts/send_vanilla_payment.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +source tests/common.sh + + +get_node_ids + +get_address 1 +fund_address $address +mine 1 + +# wait for bdk and ldk to sync up with electrs +sleep 5 + +# open channel +open_vanilla_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 16777215 +list_channels 1 +list_channels 2 + +# get invoice +get_vanilla_invoice 2 3000000 + +# send payment +send_payment 1 2 "$INVOICE" +list_channels 1 +list_channels 2 +list_payments 1 +list_payments 2 diff --git a/tests/scripts/swap_roundtrip.sh b/tests/scripts/swap_roundtrip.sh new file mode 100644 index 0000000..665d712 --- /dev/null +++ b/tests/scripts/swap_roundtrip.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +source tests/common.sh + + +get_node_ids + +# create RGB UTXOs +create_utxos 1 +create_utxos 2 + +# issue asset +issue_asset + +# open channel +open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +list_channels 1 1 +list_channels 2 1 + +open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 +list_channels 2 2 +list_channels 1 2 + + +maker_init 2 10 "sell" 900 +taker 1 +taker_list 1 1 +maker_list 2 1 +maker_execute 2 + +sleep 5 + +list_channels 2 2 +list_channels 1 2 diff --git a/tests/scripts/swap_roundtrip_buy.sh b/tests/scripts/swap_roundtrip_buy.sh new file mode 100644 index 0000000..9957bfa --- /dev/null +++ b/tests/scripts/swap_roundtrip_buy.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +source tests/common.sh + + +get_node_ids + +# create RGB UTXOs +create_utxos 1 +create_utxos 2 +create_utxos 3 + +# issue asset +issue_asset + +# open channel +open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +list_channels 1 1 +list_channels 2 1 + +open_vanilla_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 16777215 +list_channels 2 2 +list_channels 3 1 + +open_vanilla_channel 3 1 "$NODE1_PORT" "$NODE1_ID" 16777215 +list_channels 3 2 +list_channels 1 2 + + +maker_init 1 10 "buy" 900 +taker 2 +taker_list 2 1 +maker_list 1 1 +maker_execute 1 + +sleep 5 + +list_channels 2 2 +list_channels 1 2 +list_channels 3 2 diff --git a/tests/scripts/swap_roundtrip_fail.sh b/tests/scripts/swap_roundtrip_fail.sh new file mode 100644 index 0000000..e29638b --- /dev/null +++ b/tests/scripts/swap_roundtrip_fail.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +source tests/common.sh + + +get_node_ids + +# create RGB UTXOs +create_utxos 1 +create_utxos 2 + +# issue asset +issue_asset + +# open channel +open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +list_channels 1 1 +list_channels 2 1 + +open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 +list_channels 2 2 +list_channels 1 2 + + +maker_init 2 10 "sell" 900 +# taker 1 like `swap_roundtrip` but we don't whitelist the swap so it will fail +maker_execute_expect_failure 2 "$NODE1_ID" 1 diff --git a/tests/scripts/swap_roundtrip_fail_amount_maker.sh b/tests/scripts/swap_roundtrip_fail_amount_maker.sh new file mode 100644 index 0000000..7c02eb0 --- /dev/null +++ b/tests/scripts/swap_roundtrip_fail_amount_maker.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +source tests/common.sh + + +get_node_ids + +# create RGB UTXOs +create_utxos 1 +create_utxos 2 + +# issue asset +issue_asset + +# open channel +open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +list_channels 1 1 +list_channels 2 1 + +open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 +list_channels 2 2 +list_channels 1 2 + +# The amount is too large, we don't have enough +maker_init_amount_failure 2 1000 "buy" 100 diff --git a/tests/scripts/swap_roundtrip_fail_amount_taker.sh b/tests/scripts/swap_roundtrip_fail_amount_taker.sh new file mode 100644 index 0000000..953c37a --- /dev/null +++ b/tests/scripts/swap_roundtrip_fail_amount_taker.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +source tests/common.sh + + +get_node_ids + +# create RGB UTXOs +create_utxos 1 +create_utxos 2 + +# issue asset +issue_asset + +# open channel +open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +list_channels 1 1 +list_channels 2 1 + +open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 +list_channels 2 2 +list_channels 1 2 + +maker_init 2 1000 "sell" 100 +# The amount is too large, we don't have enough +taker_amount_failure 1 diff --git a/tests/scripts/swap_roundtrip_multihop_buy.sh b/tests/scripts/swap_roundtrip_multihop_buy.sh new file mode 100644 index 0000000..a1ac7f9 --- /dev/null +++ b/tests/scripts/swap_roundtrip_multihop_buy.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +source tests/common.sh + + +get_node_ids + +# create RGB UTXOs +create_utxos 1 +create_utxos 2 +create_utxos 3 + +# issue asset +issue_asset + +# send assets +blind 2 +send_assets 1 400 +asset_balance 1 600 + +# open channel +open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 +list_channels 1 +list_channels 2 +asset_balance 1 100 + +refresh 2 +asset_balance 2 400 + +# open channel +open_big_colored_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 300 1 +list_channels 2 2 +list_channels 3 +asset_balance 2 100 + +# needs more funding +create_utxos 1 +create_utxos 2 +create_utxos 3 + +open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 +list_channels 2 3 +list_channels 1 2 + +open_vanilla_channel 3 2 "$NODE2_PORT" "$NODE2_ID" 16777215 +list_channels 3 2 +list_channels 2 4 + +sleep 5 + +maker_init 1 2 "buy" 90 +taker 3 +taker_list 3 1 +maker_list 1 1 +maker_execute 1 + +sleep 5 + +list_payments 1 2 +list_payments 3 0 + +exit 0 diff --git a/tests/scripts/swap_roundtrip_multihop_sell.sh b/tests/scripts/swap_roundtrip_multihop_sell.sh new file mode 100644 index 0000000..7587027 --- /dev/null +++ b/tests/scripts/swap_roundtrip_multihop_sell.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +source tests/common.sh + + +get_node_ids + +# create RGB UTXOs +create_utxos 1 +create_utxos 2 +create_utxos 3 + +# issue asset +issue_asset + +# send assets +blind 2 +send_assets 1 400 +asset_balance 1 600 + +# open channel +open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 +channel12_id="$CHANNEL_ID" +list_channels 1 +list_channels 2 +asset_balance 1 100 + +refresh 2 +asset_balance 2 400 + +# open channel +open_big_colored_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 300 1 +channel23_id="$CHANNEL_ID" +list_channels 2 2 +list_channels 3 +asset_balance 2 100 + +# needs more funding +create_utxos 1 +create_utxos 2 +create_utxos 3 + +open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 +list_channels 2 3 +list_channels 1 2 + +open_vanilla_channel 3 2 "$NODE2_PORT" "$NODE2_ID" 16777215 +list_channels 3 2 +list_channels 2 4 + +sleep 10 + +maker_init 3 2 "sell" 90 +taker 1 +taker_list 1 1 +maker_list 3 1 +maker_execute 3 + +sleep 5 + +list_payments 1 0 +list_payments 3 2 + +exit 0 diff --git a/tests/scripts/swap_roundtrip_timeout.sh b/tests/scripts/swap_roundtrip_timeout.sh new file mode 100644 index 0000000..a5c572e --- /dev/null +++ b/tests/scripts/swap_roundtrip_timeout.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +source tests/common.sh + + +get_node_ids + +# create RGB UTXOs +create_utxos 1 +create_utxos 2 + +# issue asset +issue_asset + +# open channel +open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 +list_channels 1 1 +list_channels 2 1 + +open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 +list_channels 2 2 +list_channels 1 2 + +# the timeout is too short, it will already be expired when the taker accepts +maker_init 2 10 "sell" 1 +sleep 3 +taker_expect_timeout 1 diff --git a/tests/scripts/vanilla_keysend.sh b/tests/scripts/vanilla_keysend.sh new file mode 100644 index 0000000..d44964f --- /dev/null +++ b/tests/scripts/vanilla_keysend.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +source tests/common.sh + + +get_node_ids + +get_address 1 +fund_address $address +mine 1 + +# wait for bdk and ldk to sync up with electrs +sleep 5 + +# open channel +open_vanilla_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 16777215 +list_channels 1 +list_channels 2 + +# send payment +keysend 1 2 "$NODE2_ID" 3000000 +list_channels 1 +list_channels 2 +list_payments 1 +list_payments 2 +