@@ -1226,3 +1226,180 @@ async fn test_decodeoffer_invalid() {
12261226 let body: serde_json:: Value = resp. json ( ) . await . unwrap ( ) ;
12271227 assert_eq ! ( body[ "code" ] . as_str( ) . unwrap( ) , "bad_request" ) ;
12281228}
1229+
1230+ #[ tokio:: test( flavor = "multi_thread" ) ]
1231+ async fn test_payinvoice_invalid_bolt11 ( ) {
1232+ let bitcoind = TestBitcoind :: new ( ) ;
1233+ let server = MdkdHandle :: start ( & bitcoind, None , None , & random_mnemonic ( ) ) . await ;
1234+
1235+ let resp = server
1236+ . post_form ( "/payinvoice" , & [ ( "invoice" , "not-a-real-bolt11" ) ] )
1237+ . await ;
1238+ assert_eq ! ( resp. status( ) , 400 ) ;
1239+
1240+ let body: serde_json:: Value = resp. json ( ) . await . unwrap ( ) ;
1241+ assert_eq ! ( body[ "code" ] . as_str( ) . unwrap( ) , "bad_request" ) ;
1242+ assert ! ( body[ "error" ]
1243+ . as_str( )
1244+ . unwrap( )
1245+ . to_lowercase( )
1246+ . contains( "bolt11" ) ) ;
1247+ }
1248+
1249+ #[ tokio:: test( flavor = "multi_thread" ) ]
1250+ async fn test_payinvoice_outbound_payment ( ) {
1251+ let bitcoind = TestBitcoind :: new ( ) ;
1252+ let lsp = LspNode :: new ( & bitcoind) ;
1253+ fund_lsp ( & bitcoind, & lsp) . await ;
1254+
1255+ let server = MdkdHandle :: start ( & bitcoind, None , Some ( & lsp) , & random_mnemonic ( ) ) . await ;
1256+ let payer = PayerNode :: new ( & bitcoind) ;
1257+ setup_payer_lsp_channel ( & bitcoind, & payer, & lsp, 500_000 ) . await ;
1258+
1259+ // Step 1: fund mdkd's outbound side by receiving a payment first.
1260+ // This opens the JIT mdkd<->LSP channel and leaves mdkd with the inbound funds.
1261+ let invoice: serde_json:: Value = server
1262+ . post_form (
1263+ "/createinvoice" ,
1264+ & [
1265+ ( "amountSat" , "200000" ) ,
1266+ ( "description" , "fund-mdkd-for-pay-test" ) ,
1267+ ( "expirySeconds" , "3600" ) ,
1268+ ] ,
1269+ )
1270+ . await
1271+ . json ( )
1272+ . await
1273+ . unwrap ( ) ;
1274+ let inbound_invoice = invoice[ "serialized" ] . as_str ( ) . unwrap ( ) ;
1275+ let inbound_hash = invoice[ "paymentHash" ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
1276+
1277+ payer. pay_invoice ( inbound_invoice) ;
1278+
1279+ let start = std:: time:: Instant :: now ( ) ;
1280+ loop {
1281+ let resp: serde_json:: Value = server
1282+ . get ( & format ! ( "/payments/incoming/{inbound_hash}" ) )
1283+ . await
1284+ . json ( )
1285+ . await
1286+ . unwrap ( ) ;
1287+ if resp[ "isPaid" ] . as_bool ( ) . unwrap_or ( false ) {
1288+ break ;
1289+ }
1290+ if start. elapsed ( ) > Duration :: from_secs ( 60 ) {
1291+ panic ! ( "Timed out funding mdkd via LSP JIT channel" ) ;
1292+ }
1293+ bitcoind. mine_blocks ( 1 ) ;
1294+ tokio:: time:: sleep ( Duration :: from_secs ( 2 ) ) . await ;
1295+ }
1296+
1297+ // Step 2: PayerNode issues a fresh invoice we will pay FROM mdkd.
1298+ let payer_balance_before = payer. outbound_capacity_msat ( ) ;
1299+ let outbound_invoice = payer. create_invoice ( 50_000 , "pay test" , 3600 ) ;
1300+
1301+ // Step 3: hit /payinvoice on mdkd and wait for it to settle.
1302+ let resp = server
1303+ . post_form ( "/payinvoice" , & [ ( "invoice" , & outbound_invoice) ] )
1304+ . await ;
1305+ assert_eq ! ( resp. status( ) , 200 , "/payinvoice returned non-200" ) ;
1306+ let body: serde_json:: Value = resp. json ( ) . await . unwrap ( ) ;
1307+ let payment_id = body[ "paymentId" ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
1308+ assert_eq ! ( payment_id. len( ) , 64 ) ;
1309+ assert_eq ! ( body[ "paymentHash" ] . as_str( ) . unwrap( ) . len( ) , 64 ) ;
1310+
1311+ let start = std:: time:: Instant :: now ( ) ;
1312+ let settled: serde_json:: Value = loop {
1313+ let resp: serde_json:: Value = server
1314+ . get ( & format ! ( "/payments/outgoing/{payment_id}" ) )
1315+ . await
1316+ . json ( )
1317+ . await
1318+ . unwrap ( ) ;
1319+ if resp[ "isPaid" ] . as_bool ( ) . unwrap_or ( false ) {
1320+ break resp;
1321+ }
1322+ if start. elapsed ( ) > Duration :: from_secs ( 60 ) {
1323+ panic ! (
1324+ "Timed out waiting for outgoing payment to settle: {:?}" ,
1325+ resp
1326+ ) ;
1327+ }
1328+ bitcoind. mine_blocks ( 1 ) ;
1329+ tokio:: time:: sleep ( Duration :: from_secs ( 1 ) ) . await ;
1330+ } ;
1331+
1332+ assert ! ( settled[ "isPaid" ] . as_bool( ) . unwrap( ) ) ;
1333+ assert ! (
1334+ settled[ "preimage" ] . as_str( ) . is_some( ) ,
1335+ "settled payment should expose a preimage"
1336+ ) ;
1337+ let sent = settled[ "sent" ] . as_u64 ( ) . unwrap ( ) ;
1338+ assert_eq ! ( sent, 50_000 , "sent should equal the invoice amount in sats" ) ;
1339+ let fees = settled[ "fees" ] . as_u64 ( ) . unwrap ( ) ;
1340+ assert ! ( fees < sent, "fees should be a fraction of sent amount" ) ;
1341+
1342+ // Verify the payer node actually received the value (defense against silent
1343+ // routing bugs where mdkd thinks the payment succeeded but the counterparty
1344+ // never saw it).
1345+ let start = std:: time:: Instant :: now ( ) ;
1346+ loop {
1347+ payer. sync_wallets ( ) ;
1348+ let payer_balance_after = payer. outbound_capacity_msat ( ) ;
1349+ // Payer's *outbound* capacity decreases by (received - fee they took, if any) when they
1350+ // route - but since mdkd is paying THEM directly, payer's *inbound* capacity decreases.
1351+ // We assert via list_balances spendable Lightning balance increase instead.
1352+ let spendable = payer. node . list_balances ( ) . total_lightning_balance_sats ;
1353+ if spendable >= 50_000 {
1354+ break ;
1355+ }
1356+ if start. elapsed ( ) > Duration :: from_secs ( 30 ) {
1357+ panic ! (
1358+ "Payer node never observed the inbound 50k sat (before={} after={} spendable={})" ,
1359+ payer_balance_before, payer_balance_after, spendable
1360+ ) ;
1361+ }
1362+ tokio:: time:: sleep ( Duration :: from_millis ( 500 ) ) . await ;
1363+ }
1364+ }
1365+
1366+ #[ tokio:: test( flavor = "multi_thread" ) ]
1367+ async fn test_payinvoice_amount_mismatch_400 ( ) {
1368+ let bitcoind = TestBitcoind :: new ( ) ;
1369+ let server = MdkdHandle :: start ( & bitcoind, None , None , & random_mnemonic ( ) ) . await ;
1370+
1371+ // A throwaway PayerNode just to mint a real bolt11 with an amount.
1372+ let payer = PayerNode :: new ( & bitcoind) ;
1373+ let invoice = payer. create_invoice ( 10_000 , "mismatch test" , 600 ) ;
1374+
1375+ // amountSat that disagrees with the invoice amount must be rejected up front.
1376+ let resp = server
1377+ . post_form (
1378+ "/payinvoice" ,
1379+ & [ ( "invoice" , & invoice) , ( "amountSat" , "5000" ) ] ,
1380+ )
1381+ . await ;
1382+ assert_eq ! ( resp. status( ) , 400 ) ;
1383+ let body: serde_json:: Value = resp. json ( ) . await . unwrap ( ) ;
1384+ assert_eq ! ( body[ "code" ] . as_str( ) . unwrap( ) , "bad_request" ) ;
1385+ let err = body[ "error" ] . as_str ( ) . unwrap ( ) . to_lowercase ( ) ;
1386+ assert ! ( err. contains( "does not match" ) , "unexpected error: {err}" ) ;
1387+ assert ! ( err. contains( "amountsat" ) , "unexpected error: {err}" ) ;
1388+
1389+ // Matching amountSat must pass validation. Payment itself will fail with an
1390+ // internal error (no channels in this minimal setup), but crucially it must
1391+ // not be rejected with a 400 from the validation path.
1392+ let resp = server
1393+ . post_form (
1394+ "/payinvoice" ,
1395+ & [ ( "invoice" , & invoice) , ( "amountSat" , "10000" ) ] ,
1396+ )
1397+ . await ;
1398+ assert_ne ! (
1399+ resp. status( ) ,
1400+ 400 ,
1401+ "matching amountSat should pass validation, got status {} body {}" ,
1402+ resp. status( ) ,
1403+ resp. text( ) . await . unwrap_or_default( )
1404+ ) ;
1405+ }
0 commit comments