diff --git a/openapi.yaml b/openapi.yaml index 9cc50dd..e08bcbb 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1894,6 +1894,9 @@ components: push_msat: type: integer example: 1394000 + push_asset_amount: + type: integer + example: 100 asset_amount: type: integer example: 333 diff --git a/src/routes.rs b/src/routes.rs index 95ca53f..e522d87 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -834,6 +834,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, @@ -2916,6 +2917,24 @@ pub(crate) async fn open_channel( ))); } + // Validate push_asset_amount + if let Some(push_asset_amount) = payload.push_asset_amount { + // push_asset_amount can only be used with RGB channels + 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); } @@ -3073,11 +3092,18 @@ pub(crate) async fn open_channel( tracing::info!("EVENT: initiated channel with peer {}", peer_pubkey); if let Some((contract_id, asset_amount)) = &colored_info { + // Calculate local and remote amounts based on push_asset_amount + let (local_amount, remote_amount) = if let Some(push_amount) = payload.push_asset_amount { + (*asset_amount - push_amount, push_amount) + } else { + (*asset_amount, 0) + }; + let rgb_info = RgbInfo { contract_id: *contract_id, schema: schema.unwrap(), - local_rgb_amount: *asset_amount, - remote_rgb_amount: 0, + local_rgb_amount: local_amount, + remote_rgb_amount: remote_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 055b260..ec7eb08 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/getchannelid.rs b/src/test/getchannelid.rs index 18ed347..0e19237 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 8d9a17e..23b5628 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1039,6 +1039,7 @@ async fn open_channel( None, None, None, + None, true, ) .await @@ -1053,6 +1054,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>, @@ -1074,6 +1076,7 @@ async fn open_channel_with_custom_data( 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, @@ -1685,6 +1688,7 @@ mod open_after_double_send; mod openchannel_fail; mod openchannel_optional_addr; mod payment; +mod push_rgb_assets; mod refuse_high_fees; mod restart; mod send_receive; diff --git a/src/test/openchannel_fail.rs b/src/test/openchannel_fail.rs index 32dee21..5047106 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, @@ -154,6 +158,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 +191,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 +226,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 +259,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 +292,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 +327,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 +360,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 +393,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 +426,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 +447,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, diff --git a/src/test/openchannel_optional_addr.rs b/src/test/openchannel_optional_addr.rs index a86555d..5d300d0 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/push_rgb_assets.rs b/src/test/push_rgb_assets.rs new file mode 100644 index 0000000..4673d0d --- /dev/null +++ b/src/test/push_rgb_assets.rs @@ -0,0 +1,140 @@ +use super::*; +const TEST_DIR_BASE: &str = "tmp/push_rgb_assets/"; + +#[serial_test::serial] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[traced_test] +async fn test_push_rgb_assets() { + initialize(); + + let test_dir_base = format!("{TEST_DIR_BASE}test_push_rgb_assets/"); + let test_dir_node1 = format!("{test_dir_base}node1"); + let test_dir_node2 = format!("{test_dir_base}node2"); + 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 node2_pubkey = node_info(node2_addr).await.pubkey; + + fund_and_create_utxos(node1_addr, None).await; + fund_and_create_utxos(node2_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 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); + + // Check balances after channel opening with push + assert_eq!(asset_balance_spendable(node1_addr, &asset_id).await, 400); + + let node1_channels = list_channels(node1_addr).await; + let node1_channel = node1_channels.iter().find(|c| c.channel_id == 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 == channel.channel_id).unwrap(); + assert_eq!(node2_channel.asset_local_amount, Some(250)); + assert_eq!(node2_channel.asset_remote_amount, Some(350)); + +} + +#[serial_test::serial] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[traced_test] +async fn test_push_rgb_assets_validation() { + initialize(); + + let test_dir_base = format!("{TEST_DIR_BASE}test_push_rgb_assets_validation/"); + let test_dir_node1 = format!("{test_dir_base}node1"); + let test_dir_node2 = format!("{test_dir_base}node2"); + 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 node2_pubkey = node_info(node2_addr).await.pubkey; + + // Fund node1 with on-chain funds + fund_and_create_utxos(node1_addr, None).await; + + // Issue an RGB asset on node1 + let asset = issue_asset_nia(node1_addr).await; + let asset_id = asset.asset_id; + + // Connect peers + connect_peer( + node1_addr, + &node2_pubkey, + &format!("127.0.0.1:{NODE2_PEER_PORT}"), + ) + .await; + + // Test 1: Try to use push_asset_amount without RGB channel (should fail) + let payload = OpenChannelRequest { + peer_pubkey_and_opt_addr: format!("{}@127.0.0.1:{}", node2_pubkey, NODE2_PEER_PORT), + capacity_sat: 100_000, + push_msat: 0, + asset_amount: None, + asset_id: None, + push_asset_amount: Some(100), // This should fail + 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://{}/openchannel", node1_addr)) + .json(&payload) + .send() + .await + .unwrap(); + + assert_eq!(res.status(), 400); // Should fail with bad request + + + // Test 2: Try to push more than asset_amount (should fail) + let payload = OpenChannelRequest { + peer_pubkey_and_opt_addr: format!("{}@127.0.0.1:{}", node2_pubkey, 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), // More than asset_amount, should fail + 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://{}/openchannel", node1_addr)) + .json(&payload) + .send() + .await + .unwrap(); + + assert_eq!(res.status(), 400); // Should fail with bad request + + println!("✓ RGB asset push validation working correctly!"); +} \ No newline at end of file diff --git a/src/test/refuse_high_fees.rs b/src/test/refuse_high_fees.rs index daa8445..6ae9903 100644 --- a/src/test/refuse_high_fees.rs +++ b/src/test/refuse_high_fees.rs @@ -66,6 +66,7 @@ async fn refuse_high_fees() { None, Some(300), Some(&asset_id), + None, Some(2_000_000), None, None,