@@ -54,6 +54,36 @@ function waitForEvent(lnurlServer: any, name: string): Promise<void> {
5454 } ) ;
5555}
5656
57+ function spendingBalanceLabelSats ( satsInteger : number ) : string {
58+ return satsInteger . toString ( ) . replace ( / \B (? = ( \d { 3 } ) + (? ! \d ) ) / g, ' ' ) ;
59+ }
60+
61+ /** Balance in msats after pay (subtract) or withdraw (add) from a prior msat total. */
62+ function applyLnurlMsatDelta ( balanceMsats : bigint , deltaMsats : number , direction : 'pay' | 'withdraw' ) : bigint {
63+ const d = BigInt ( deltaMsats ) ;
64+ return direction === 'pay' ? balanceMsats - d : balanceMsats + d ;
65+ }
66+
67+ async function expectMoneyTextRoundedSats (
68+ parentTestId : 'ReviewAmount-primary' | 'WithdrawAmount-primary' ,
69+ msats : number ,
70+ ) {
71+ const money = await elementByIdWithin ( parentTestId , 'MoneyText' ) ;
72+ const raw = await money . getText ( ) ;
73+ const digits = raw . replace ( / [ ^ \d ] / g, '' ) ;
74+ const displayed = Number ( digits ) ;
75+ if ( Number . isNaN ( displayed ) ) {
76+ throw new Error ( `MoneyText is not numeric: raw=${ JSON . stringify ( raw ) } msats=${ msats } ` ) ;
77+ }
78+ const floorSats = Math . floor ( msats / 1000 ) ;
79+ const ceilSats = Math . ceil ( msats / 1000 ) ;
80+ if ( displayed !== floorSats && displayed !== ceilSats ) {
81+ throw new Error (
82+ `Unexpected MoneyText: raw=${ JSON . stringify ( raw ) } displayed=${ displayed } expected=${ floorSats } |${ ceilSats } msats=${ msats } ` ,
83+ ) ;
84+ }
85+ }
86+
5787describe ( '@lnurl - LNURL' , ( ) => {
5888 let electrum : Awaited < ReturnType < typeof initElectrum > > | undefined ;
5989 let lnurlServer : any ;
@@ -98,7 +128,7 @@ describe('@lnurl - LNURL', () => {
98128 } ) ;
99129
100130 ciIt (
101- '@lnurl_1 - Can process lnurl-channel, lnurl-pay, lnurl-withdraw, and lnurl-auth' ,
131+ '@lnurl_1 - Can process lnurl-channel, lnurl-pay, lnurl-withdraw, lnurl-auth, and msat-precision pay/withdraw ' ,
102132 async ( ) => {
103133 await receiveOnchainFunds ( { sats : 1000 } ) ;
104134
@@ -309,6 +339,75 @@ describe('@lnurl - LNURL', () => {
309339 await swipeFullScreen ( 'down' ) ;
310340 await swipeFullScreen ( 'down' ) ;
311341
342+ // Fixed min==max LNURL amounts in msats (LND invoice uses value_msat). Each pair pays then withdraws the same amount so balance returns to 19 713 sats.
343+ // 222538 — remainder 538 msats (regression: payment must not truncate msats).
344+ // 222222 — remainder 222 msats (< 500).
345+ // 500500 — remainder 500 msats exactly.
346+ let balanceMsats = 19713000n ;
347+
348+ async function msatPayWithdraw ( label : string , msats : number ) {
349+ const afterPay = applyLnurlMsatDelta ( balanceMsats , msats , 'pay' ) ;
350+ const afterWithdraw = applyLnurlMsatDelta ( afterPay , msats , 'withdraw' ) ;
351+
352+ const payReq = await lnurlServer . generateNewUrl ( 'payRequest' , {
353+ minSendable : msats ,
354+ maxSendable : msats ,
355+ metadata : `[["text/plain","lnurl-msat-${ label } "]]` ,
356+ commentAllowed : 0 ,
357+ } ) ;
358+ console . log ( `payRequest msat ${ label } ` , payReq ) ;
359+
360+ await enterAddressViaScanPrompt ( payReq . encoded , { acceptCameraPermission : false } ) ;
361+ await sleep ( 2000 ) ;
362+ await elementById ( 'ReviewAmount-primary' ) . waitForDisplayed ( { timeout : 5000 } ) ;
363+ await elementById ( 'CommentInput' ) . waitForDisplayed ( { reverse : true } ) ;
364+ await expectMoneyTextRoundedSats ( 'ReviewAmount-primary' , msats ) ;
365+ await dragOnElement ( 'GRAB' , 'right' , 0.95 ) ;
366+ await elementById ( 'SendSuccess' ) . waitForDisplayed ( ) ;
367+ await tap ( 'Close' ) ;
368+ balanceMsats = afterPay ;
369+ await expectTextWithin (
370+ 'ActivitySpending' ,
371+ spendingBalanceLabelSats ( Number ( balanceMsats / 1000n ) ) ,
372+ ) ;
373+ await elementById ( 'ActivityShort-0' ) . waitForDisplayed ( ) ;
374+ await expectTextWithin ( 'ActivityShort-0' , '-' ) ;
375+ await expectTextWithin ( 'ActivityShort-0' , 'Sent' ) ;
376+ await sleep ( 1000 ) ;
377+ await swipeFullScreen ( 'down' ) ;
378+ await swipeFullScreen ( 'down' ) ;
379+
380+ const wReq = await lnurlServer . generateNewUrl ( 'withdrawRequest' , {
381+ minWithdrawable : msats ,
382+ maxWithdrawable : msats ,
383+ defaultDescription : `lnurl-withdraw-msat-${ label } ` ,
384+ } ) ;
385+ console . log ( `withdrawRequest msat ${ label } ` , wReq ) ;
386+
387+ await enterAddressViaScanPrompt ( wReq . encoded , { acceptCameraPermission : false } ) ;
388+ await sleep ( 2000 ) ;
389+ await elementById ( 'WithdrawAmount-primary' ) . waitForDisplayed ( { timeout : 5000 } ) ;
390+ await expectMoneyTextRoundedSats ( 'WithdrawAmount-primary' , msats ) ;
391+ await tap ( 'WithdrawConfirmButton' ) ;
392+ await acknowledgeReceivedPayment ( ) ;
393+ balanceMsats = afterWithdraw ;
394+ await expectTextWithin (
395+ 'ActivitySpending' ,
396+ spendingBalanceLabelSats ( Number ( balanceMsats / 1000n ) ) ,
397+ ) ;
398+ await elementById ( 'ActivityShort-0' ) . waitForDisplayed ( ) ;
399+ await expectTextWithin ( 'ActivityShort-0' , '+' ) ;
400+ await expectTextWithin ( 'ActivityShort-0' , `lnurl-withdraw-msat-${ label } ` ) ;
401+ await expectTextWithin ( 'ActivityShort-0' , 'Received' ) ;
402+ await sleep ( 1000 ) ;
403+ await swipeFullScreen ( 'down' ) ;
404+ await swipeFullScreen ( 'down' ) ;
405+ }
406+
407+ await msatPayWithdraw ( '222538' , 222_538 ) ;
408+ await msatPayWithdraw ( '222222' , 222_222 ) ;
409+ await msatPayWithdraw ( '500500' , 500_500 ) ;
410+
312411 // lnurl-auth
313412 const loginRequest1 = await lnurlServer . generateNewUrl ( 'login' ) ;
314413 console . log ( 'loginRequest1' , loginRequest1 ) ;
0 commit comments