diff --git a/openapi.yaml b/openapi.yaml index f500ba0e..81a6cf52 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2272,6 +2272,9 @@ components: - string - 'null' example: rgb:CJkb4YZw-jRiz2sk-~PARPio-wtVYI1c-XAEYCqO-wTfvRZ8 + push_asset_amount: + type: integer + example: 100 public: type: boolean example: true diff --git a/rust-lightning b/rust-lightning index 961313c7..ea151249 160000 --- a/rust-lightning +++ b/rust-lightning @@ -1 +1 @@ -Subproject commit 961313c75d410a5c92118b5eda8255809a8eac20 +Subproject commit ea151249bc794bc38b1bc054c09c6f6463c05a10 diff --git a/src/ldk.rs b/src/ldk.rs index 75bcd23c..34639e2d 100644 --- a/src/ldk.rs +++ b/src/ldk.rs @@ -590,7 +590,7 @@ async fn handle_ldk_events( &PathBuf::from(&static_state.ldk_data_dir), ); - let channel_rgb_amount: u64 = rgb_info.local_rgb_amount; + let channel_rgb_amount = rgb_info.local_rgb_amount + rgb_info.remote_rgb_amount; let asset_id = rgb_info.contract_id.to_string(); let assignment = match rgb_info.schema { AssetSchema::Nia | AssetSchema::Cfa => Assignment::Fungible(channel_rgb_amount), diff --git a/src/routes.rs b/src/routes.rs index 6a4cdcf4..0e6d6a3d 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -839,6 +839,7 @@ pub(crate) struct OpenChannelRequest { pub(crate) push_msat: u64, pub(crate) asset_amount: Option, pub(crate) asset_id: Option, + pub(crate) push_asset_amount: Option, pub(crate) public: bool, pub(crate) with_anchors: bool, pub(crate) fee_base_msat: Option, @@ -3044,6 +3045,21 @@ pub(crate) async fn open_channel( ))); } + if let Some(push_asset_amount) = payload.push_asset_amount { + if colored_info.is_none() { + return Err(APIError::InvalidAmount(s!( + "push_asset_amount can only be used with RGB channels (asset_id must be specified)" + ))); + } + if let Some((_, asset_amount)) = &colored_info { + if push_asset_amount > *asset_amount { + return Err(APIError::InvalidAmount(s!( + "push_asset_amount cannot be higher than asset_amount" + ))); + } + } + } + if colored_info.is_some() && !payload.with_anchors { return Err(APIError::AnchorsRequired); } @@ -3175,6 +3191,7 @@ pub(crate) async fn open_channel( temporary_channel_id, Some(config), consignment_endpoint, + payload.push_asset_amount, ) .map_err(|e| { *unlocked_state.rgb_send_lock.lock().unwrap() = false; @@ -3201,11 +3218,12 @@ pub(crate) async fn open_channel( tracing::info!("EVENT: initiated channel with peer {}", peer_pubkey); if let Some((contract_id, asset_amount)) = &colored_info { + let push_amount = payload.push_asset_amount.unwrap_or(0); let rgb_info = RgbInfo { contract_id: *contract_id, schema: schema.unwrap(), - local_rgb_amount: *asset_amount, - remote_rgb_amount: 0, + local_rgb_amount: *asset_amount - push_amount, + remote_rgb_amount: push_amount, }; write_rgb_channel_info( &get_rgb_channel_info_path( diff --git a/src/test/close_coop_vanilla.rs b/src/test/close_coop_vanilla.rs index 055b2601..ec7eb08e 100644 --- a/src/test/close_coop_vanilla.rs +++ b/src/test/close_coop_vanilla.rs @@ -138,6 +138,7 @@ async fn without_anchors() { None, None, None, + None, false, ) .await; diff --git a/src/test/concurrent_openchannel.rs b/src/test/concurrent_openchannel.rs index 3db73a8a..c7e682b0 100644 --- a/src/test/concurrent_openchannel.rs +++ b/src/test/concurrent_openchannel.rs @@ -59,6 +59,7 @@ async fn concurrent_openchannel() { Some(PUSH_MSAT), Some(ASSET_AMOUNT), Some(&asset_id), + None, 20, ), open_channel_with_retry( @@ -69,6 +70,7 @@ async fn concurrent_openchannel() { Some(PUSH_MSAT), Some(ASSET_AMOUNT), Some(&asset_id), + None, 20, ), open_channel_with_retry( @@ -79,6 +81,7 @@ async fn concurrent_openchannel() { Some(PUSH_MSAT), Some(ASSET_AMOUNT), Some(&asset_id), + None, 20, ), open_channel_with_retry( @@ -89,6 +92,7 @@ async fn concurrent_openchannel() { Some(PUSH_MSAT), Some(ASSET_AMOUNT), Some(&asset_id), + None, 20, ), open_channel_with_retry( @@ -99,6 +103,7 @@ async fn concurrent_openchannel() { Some(PUSH_MSAT), Some(ASSET_AMOUNT), Some(&asset_id), + None, 20, ) ); diff --git a/src/test/getchannelid.rs b/src/test/getchannelid.rs index 18ed3476..0e192371 100644 --- a/src/test/getchannelid.rs +++ b/src/test/getchannelid.rs @@ -37,6 +37,7 @@ async fn getchannelid_success() { Some(&asset_id), None, None, + None, Some(&temporary_channel_id), true, ) diff --git a/src/test/mod.rs b/src/test/mod.rs index 4cefa7e4..268b0294 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1084,6 +1084,7 @@ async fn open_channel( None, None, None, + None, true, ) .await @@ -1098,6 +1099,7 @@ async fn open_channel_with_retry( push_msat: Option, asset_amount: Option, asset_id: Option<&str>, + push_asset_amount: Option, max_retries: u32, ) -> Channel { let mut attempt = 0; @@ -1111,6 +1113,7 @@ async fn open_channel_with_retry( push_msat, asset_amount, asset_id, + push_asset_amount, None, None, None, @@ -1146,6 +1149,7 @@ async fn open_channel_raw( push_msat: Option, asset_amount: Option, asset_id: Option<&str>, + push_asset_amount: Option, fee_base_msat: Option, fee_proportional_millionths: Option, temporary_channel_id: Option<&str>, @@ -1180,6 +1184,7 @@ async fn open_channel_raw( push_msat: push_msat.unwrap_or(0), asset_amount, asset_id: asset_id.map(|a| a.to_string()), + push_asset_amount, public: true, with_anchors, fee_base_msat, @@ -1207,10 +1212,18 @@ async fn open_channel_raw( tokio::time::sleep(std::time::Duration::from_secs(1)).await; let channels = list_channels(node_address).await; if let Some(channel) = channels.iter().find(|c| { + let asset_amounts_match = if asset_id.is_some() { + let local_amount = asset_amount.unwrap_or(0) - push_asset_amount.unwrap_or(0); + let remote_amount = push_asset_amount.unwrap_or(0); + c.asset_local_amount == Some(local_amount) + && c.asset_remote_amount == Some(remote_amount) + } else { + c.asset_local_amount.is_none() && c.asset_remote_amount.is_none() + }; !c.ready && c.peer_pubkey == dest_peer_pubkey && c.asset_id == asset_id.map(|id| id.to_string()) - && c.asset_local_amount == asset_amount + && asset_amounts_match }) { if let Some(txid) = &channel.funding_txid { let txout = _get_txout(txid); @@ -1254,6 +1267,7 @@ async fn open_channel_with_custom_data( push_msat: Option, asset_amount: Option, asset_id: Option<&str>, + push_asset_amount: Option, fee_base_msat: Option, fee_proportional_millionths: Option, temporary_channel_id: Option<&str>, @@ -1267,6 +1281,7 @@ async fn open_channel_with_custom_data( push_msat, asset_amount, asset_id, + push_asset_amount, fee_base_msat, fee_proportional_millionths, temporary_channel_id, @@ -1870,6 +1885,7 @@ mod multi_open_close; mod open_after_double_send; mod openchannel_fail; mod openchannel_optional_addr; +mod openchannel_push_asset_amount; mod payment; mod refuse_high_fees; mod restart; diff --git a/src/test/openchannel_fail.rs b/src/test/openchannel_fail.rs index 32dee214..e8c78f0f 100644 --- a/src/test/openchannel_fail.rs +++ b/src/test/openchannel_fail.rs @@ -29,6 +29,7 @@ async fn open_fail() { push_msat: 3_500_000, asset_amount: Some(100), asset_id: Some(asset_id.clone()), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -63,6 +64,7 @@ async fn open_fail() { push_msat: 3_500_000, asset_amount: Some(100), asset_id: Some(s!("rgb:EIkAVQvq-WbAb5JG-CYxbUER-oqDNwne-ZNxBDID-p0cpf9U")), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -90,6 +92,7 @@ async fn open_fail() { push_msat: 3_500_000, asset_amount: Some(0), asset_id: Some(asset_id.clone()), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -122,6 +125,7 @@ async fn open_fail() { push_msat: 3_500_000, asset_amount: Some(100), asset_id: Some(s!("bad asset ID")), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -147,6 +151,67 @@ async fn open_fail() { assert_eq!(channels_1.len(), 0); assert_eq!(channels_2.len(), 0); + // open with push_asset_amount but without RGB info + let payload = OpenChannelRequest { + peer_pubkey_and_opt_addr: format!("{node2_pubkey}@127.0.0.1:{NODE2_PEER_PORT}"), + capacity_sat: 100_000, + push_msat: 0, + asset_amount: None, + asset_id: None, + push_asset_amount: Some(100), + public: true, + with_anchors: true, + fee_base_msat: None, + fee_proportional_millionths: None, + temporary_channel_id: None, + }; + let res = reqwest::Client::new() + .post(format!("http://{node1_addr}/openchannel")) + .json(&payload) + .send() + .await + .unwrap(); + check_response_is_nok( + res, + reqwest::StatusCode::BAD_REQUEST, + "Invalid amount: push_asset_amount can only be used with RGB channels (asset_id must be specified)", + "InvalidAmount", + ) + .await; + + // open with push_asset_amount higher than asset_amount + let payload = OpenChannelRequest { + peer_pubkey_and_opt_addr: format!("{node2_pubkey}@127.0.0.1:{NODE2_PEER_PORT}"), + capacity_sat: 100_000, + push_msat: 0, + asset_amount: Some(500), + asset_id: Some(asset_id.clone()), + push_asset_amount: Some(600), + public: true, + with_anchors: true, + fee_base_msat: None, + fee_proportional_millionths: None, + temporary_channel_id: None, + }; + let res = reqwest::Client::new() + .post(format!("http://{node1_addr}/openchannel")) + .json(&payload) + .send() + .await + .unwrap(); + check_response_is_nok( + res, + reqwest::StatusCode::BAD_REQUEST, + "Invalid amount: push_asset_amount cannot be higher than asset_amount", + "InvalidAmount", + ) + .await; + + let channels_1 = list_channels(node1_addr).await; + let channels_2 = list_channels(node2_addr).await; + assert_eq!(channels_1.len(), 0); + assert_eq!(channels_2.len(), 0); + // open with invalid BTC amount (too low) let payload = OpenChannelRequest { peer_pubkey_and_opt_addr: format!("{node2_pubkey}@127.0.0.1:{NODE2_PEER_PORT}"), @@ -154,6 +219,7 @@ async fn open_fail() { push_msat: 3_500_000, asset_amount: None, asset_id: None, + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -186,6 +252,7 @@ async fn open_fail() { push_msat: 3_500_000, asset_amount: Some(100), asset_id: Some(asset_id.clone()), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -220,6 +287,7 @@ async fn open_fail() { push_msat: 100_000_000, asset_amount: Some(100), asset_id: Some(asset_id.clone()), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -252,6 +320,7 @@ async fn open_fail() { push_msat: 100_000_001, asset_amount: Some(100), asset_id: Some(asset_id.clone()), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -284,6 +353,7 @@ async fn open_fail() { push_msat: 3_500_000, asset_amount: Some(100), asset_id: Some(asset_id.clone()), + push_asset_amount: None, public: true, with_anchors: false, fee_base_msat: None, @@ -318,6 +388,7 @@ async fn open_fail() { push_msat: 0, asset_amount: None, asset_id: None, + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -350,6 +421,7 @@ async fn open_fail() { push_msat: 3_500_000, asset_amount: Some(2000), asset_id: Some(asset_id.clone()), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -382,6 +454,7 @@ async fn open_fail() { push_msat: 3_500_000, asset_amount: Some(100), asset_id: Some(asset_id.clone()), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -414,6 +487,7 @@ async fn open_fail() { push_msat: 3_500_000, asset_amount: Some(100), asset_id: Some(asset_id.clone()), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -434,6 +508,7 @@ async fn open_fail() { push_msat: 3_500_000, asset_amount: Some(100), asset_id: Some(asset_id), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -454,8 +529,20 @@ async fn open_fail() { ) .await; - let channels_1 = list_channels(node1_addr).await; - let channels_2 = list_channels(node2_addr).await; - assert_eq!(channels_1.len(), 1); - assert_eq!(channels_2.len(), 1); + let t_0 = OffsetDateTime::now_utc(); + loop { + let channels_1 = list_channels(node1_addr).await; + let channels_2 = list_channels(node2_addr).await; + if channels_1.len() == 1 && channels_2.len() == 1 { + break; + } + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + if (OffsetDateTime::now_utc() - t_0).as_seconds_f32() > 10.0 { + panic!( + "expected one pending channel on both nodes, got {} and {}", + channels_1.len(), + channels_2.len() + ); + } + } } diff --git a/src/test/openchannel_optional_addr.rs b/src/test/openchannel_optional_addr.rs index a86555df..5d300d0c 100644 --- a/src/test/openchannel_optional_addr.rs +++ b/src/test/openchannel_optional_addr.rs @@ -30,6 +30,7 @@ async fn openchannel_optional_addr_forward() { push_msat: 3_500_000, asset_amount: Some(600), asset_id: Some(asset_id.clone()), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, @@ -113,6 +114,7 @@ async fn openchannel_optional_addr_reverse() { push_msat: 3_500_000, asset_amount: Some(600), asset_id: Some(asset_id.clone()), + push_asset_amount: None, public: true, with_anchors: true, fee_base_msat: None, diff --git a/src/test/openchannel_push_asset_amount.rs b/src/test/openchannel_push_asset_amount.rs new file mode 100644 index 00000000..6fe175a7 --- /dev/null +++ b/src/test/openchannel_push_asset_amount.rs @@ -0,0 +1,217 @@ +use super::*; +const TEST_DIR_BASE: &str = "tmp/openchannel_push_asset_amount/"; + +#[serial_test::serial] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[traced_test] +async fn openchannel_push_asset_amount() { + initialize(); + + let test_dir_node1 = format!("{TEST_DIR_BASE}openchannel_push_asset_amount/node1"); + let test_dir_node2 = format!("{TEST_DIR_BASE}openchannel_push_asset_amount/node2"); + let test_dir_node3 = format!("{TEST_DIR_BASE}openchannel_push_asset_amount/node3"); + let (node1_addr, _) = start_node(&test_dir_node1, NODE1_PEER_PORT, false).await; + let (node2_addr, _) = start_node(&test_dir_node2, NODE2_PEER_PORT, false).await; + let (node3_addr, _) = start_node(&test_dir_node3, NODE3_PEER_PORT, false).await; + + let node1_pubkey = node_info(node1_addr).await.pubkey; + let node2_pubkey = node_info(node2_addr).await.pubkey; + + fund_and_create_utxos(node1_addr, None).await; + fund_and_create_utxos(node2_addr, None).await; + fund_and_create_utxos(node3_addr, None).await; + + let asset_id = issue_asset_nia(node1_addr).await.asset_id; + connect_peer( + node1_addr, + &node2_pubkey, + &format!("127.0.0.1:{NODE2_PEER_PORT}"), + ) + .await; + + // Open channel with asset push: 600 total, push 250 to counterparty + let partial_push_channel = open_channel_with_custom_data( + node1_addr, + &node2_pubkey, + Some(NODE2_PEER_PORT), + None, + None, + Some(600), + Some(&asset_id), + Some(250), + None, + None, + None, + true, + ) + .await; + + let channels_1 = list_channels(node1_addr).await; + let channels_2 = list_channels(node2_addr).await; + assert_eq!(channels_1.len(), 1); + assert_eq!(channels_2.len(), 1); + + let node1_channels = list_channels(node1_addr).await; + let node1_channel = node1_channels + .iter() + .find(|c| c.channel_id == partial_push_channel.channel_id) + .unwrap(); + assert_eq!(node1_channel.asset_local_amount, Some(350)); + assert_eq!(node1_channel.asset_remote_amount, Some(250)); + + let node2_channels = list_channels(node2_addr).await; + let node2_channel = node2_channels + .iter() + .find(|c| c.channel_id == partial_push_channel.channel_id) + .unwrap(); + assert_eq!(node2_channel.asset_local_amount, Some(250)); + assert_eq!(node2_channel.asset_remote_amount, Some(350)); + + keysend_with_ln_balance( + node1_addr, + node2_addr, + &node2_pubkey, + None, + Some(&asset_id), + Some(100), + Some(350), + Some(250), + ) + .await; + keysend(node1_addr, &node2_pubkey, Some(10_000_000), None, None).await; + keysend_with_ln_balance( + node2_addr, + node1_addr, + &node1_pubkey, + None, + Some(&asset_id), + Some(50), + Some(350), + Some(250), + ) + .await; + + let node1_channel = list_channels(node1_addr) + .await + .into_iter() + .find(|c| c.channel_id == partial_push_channel.channel_id) + .unwrap(); + assert_eq!(node1_channel.asset_local_amount, Some(300)); + assert_eq!(node1_channel.asset_remote_amount, Some(300)); + + let node2_channel = list_channels(node2_addr) + .await + .into_iter() + .find(|c| c.channel_id == partial_push_channel.channel_id) + .unwrap(); + assert_eq!(node2_channel.asset_local_amount, Some(300)); + assert_eq!(node2_channel.asset_remote_amount, Some(300)); + + close_channel( + node1_addr, + &partial_push_channel.channel_id, + &node2_pubkey, + false, + ) + .await; + wait_for_balance(node1_addr, &asset_id, 700).await; + wait_for_balance(node2_addr, &asset_id, 300).await; + + let full_push_channel = open_channel_with_custom_data( + node1_addr, + &node2_pubkey, + Some(NODE2_PEER_PORT), + None, + None, + Some(600), + Some(&asset_id), + Some(600), + None, + None, + None, + true, + ) + .await; + + shutdown(&[node1_addr, node2_addr]).await; + let (node1_addr, _) = start_node(&test_dir_node1, NODE1_PEER_PORT, true).await; + let (node2_addr, _) = start_node(&test_dir_node2, NODE2_PEER_PORT, true).await; + wait_for_usable_channels(node1_addr, 1).await; + wait_for_usable_channels(node2_addr, 1).await; + + assert_eq!(asset_balance_spendable(node1_addr, &asset_id).await, 100); + assert_eq!(asset_balance_spendable(node2_addr, &asset_id).await, 300); + + let node1_channel = list_channels(node1_addr) + .await + .into_iter() + .find(|c| c.channel_id == full_push_channel.channel_id) + .unwrap(); + assert_eq!(node1_channel.asset_local_amount, Some(0)); + assert_eq!(node1_channel.asset_remote_amount, Some(600)); + + let node2_channel = list_channels(node2_addr) + .await + .into_iter() + .find(|c| c.channel_id == full_push_channel.channel_id) + .unwrap(); + assert_eq!(node2_channel.asset_local_amount, Some(600)); + assert_eq!(node2_channel.asset_remote_amount, Some(0)); + + keysend(node1_addr, &node2_pubkey, Some(10_000_000), None, None).await; + keysend_with_ln_balance( + node2_addr, + node1_addr, + &node1_pubkey, + None, + Some(&asset_id), + Some(100), + Some(600), + Some(0), + ) + .await; + + let node1_channel = list_channels(node1_addr) + .await + .into_iter() + .find(|c| c.channel_id == full_push_channel.channel_id) + .unwrap(); + assert_eq!(node1_channel.asset_local_amount, Some(100)); + assert_eq!(node1_channel.asset_remote_amount, Some(500)); + + let node2_channel = list_channels(node2_addr) + .await + .into_iter() + .find(|c| c.channel_id == full_push_channel.channel_id) + .unwrap(); + assert_eq!(node2_channel.asset_local_amount, Some(500)); + assert_eq!(node2_channel.asset_remote_amount, Some(100)); + + close_channel( + node1_addr, + &full_push_channel.channel_id, + &node2_pubkey, + false, + ) + .await; + wait_for_balance(node1_addr, &asset_id, 200).await; + wait_for_balance(node2_addr, &asset_id, 800).await; + + let recipient_id = rgb_invoice(node3_addr, None, false).await.recipient_id; + send_asset( + node2_addr, + &asset_id, + Assignment::Fungible(100), + recipient_id, + None, + ) + .await; + mine(false); + refresh_transfers(node3_addr).await; + refresh_transfers(node3_addr).await; + refresh_transfers(node2_addr).await; + + assert_eq!(asset_balance_spendable(node1_addr, &asset_id).await, 200); + assert_eq!(asset_balance_spendable(node2_addr, &asset_id).await, 700); + assert_eq!(asset_balance_spendable(node3_addr, &asset_id).await, 100); +} diff --git a/src/test/refuse_high_fees.rs b/src/test/refuse_high_fees.rs index 8d84614d..40c7259d 100644 --- a/src/test/refuse_high_fees.rs +++ b/src/test/refuse_high_fees.rs @@ -67,6 +67,7 @@ async fn refuse_high_fees() { None, Some(300), Some(&asset_id), + None, Some(2_000_000), None, None,