@@ -12,6 +12,7 @@ import {
1212 waitForSettlementReceipt ,
1313 parseSettlementRouterParams ,
1414 settleWithSettlementRouter ,
15+ InsufficientBalanceError ,
1516} from "../src/index.js" ;
1617import { SettlementRouterError } from "../src/types.js" ;
1718import {
@@ -317,6 +318,137 @@ describe("SettlementRouter integration", () => {
317318 ) ;
318319 } ) ;
319320
321+ it ( "should validate user balance before sending transaction when publicClient is provided" , async ( ) => {
322+ const params = {
323+ token : MOCK_ADDRESSES . token ,
324+ from : MOCK_ADDRESSES . payer ,
325+ value : MOCK_VALUES . paymentAmount ,
326+ validAfter : MOCK_VALUES . validAfter ,
327+ validBefore : MOCK_VALUES . validBefore ,
328+ nonce : MOCK_VALUES . nonce ,
329+ signature : MOCK_VALUES . signature ,
330+ salt : MOCK_VALUES . salt ,
331+ payTo : MOCK_ADDRESSES . merchant ,
332+ facilitatorFee : MOCK_VALUES . facilitatorFee ,
333+ hook : MOCK_ADDRESSES . hook ,
334+ hookData : MOCK_VALUES . hookData ,
335+ settlementRouter : MOCK_ADDRESSES . settlementRouter ,
336+ } ;
337+
338+ // Mock balance check - user has sufficient balance (2,000,000 is greater than payment 1,000,000 + fee 100,000)
339+ const sufficientBalance = BigInt ( 2000000 ) ;
340+ mockPublicClient . readContract . mockResolvedValue ( sufficientBalance ) ;
341+
342+ const txHash = await executeSettlementWithRouter ( mockWalletClient , params , {
343+ publicClient : mockPublicClient ,
344+ } ) ;
345+
346+ expect ( txHash ) . toBe ( mockSettleResponse . transaction ) ;
347+ expect ( mockPublicClient . readContract ) . toHaveBeenCalledWith ( {
348+ address : MOCK_ADDRESSES . token ,
349+ abi : expect . arrayContaining ( [
350+ expect . objectContaining ( {
351+ name : "balanceOf" ,
352+ type : "function" ,
353+ } ) ,
354+ ] ) ,
355+ functionName : "balanceOf" ,
356+ args : [ MOCK_ADDRESSES . payer ] ,
357+ } ) ;
358+ expect ( mockWalletClient . writeContract ) . toHaveBeenCalled ( ) ;
359+ } ) ;
360+
361+ it ( "should throw error when user has insufficient balance" , async ( ) => {
362+ const params = {
363+ token : MOCK_ADDRESSES . token ,
364+ from : MOCK_ADDRESSES . payer ,
365+ value : MOCK_VALUES . paymentAmount ,
366+ validAfter : MOCK_VALUES . validAfter ,
367+ validBefore : MOCK_VALUES . validBefore ,
368+ nonce : MOCK_VALUES . nonce ,
369+ signature : MOCK_VALUES . signature ,
370+ salt : MOCK_VALUES . salt ,
371+ payTo : MOCK_ADDRESSES . merchant ,
372+ facilitatorFee : MOCK_VALUES . facilitatorFee ,
373+ hook : MOCK_ADDRESSES . hook ,
374+ hookData : MOCK_VALUES . hookData ,
375+ settlementRouter : MOCK_ADDRESSES . settlementRouter ,
376+ } ;
377+
378+ // Mock insufficient balance (1000 is less than payment amount + facilitator fee)
379+ const paymentAmount = BigInt ( MOCK_VALUES . paymentAmount ) ;
380+ const facilitatorFee = BigInt ( MOCK_VALUES . facilitatorFee ) ;
381+ const requiredAmount = paymentAmount + facilitatorFee ;
382+ const insufficientBalance = requiredAmount - 1n ;
383+
384+ mockPublicClient . readContract . mockResolvedValue ( insufficientBalance ) ;
385+
386+ await expect (
387+ executeSettlementWithRouter ( mockWalletClient , params , {
388+ publicClient : mockPublicClient ,
389+ } ) ,
390+ ) . rejects . toThrow ( InsufficientBalanceError ) ;
391+
392+ // Verify transaction was NOT sent
393+ expect ( mockWalletClient . writeContract ) . not . toHaveBeenCalled ( ) ;
394+ } ) ;
395+
396+ it ( "should skip balance check when publicClient is not provided" , async ( ) => {
397+ const params = {
398+ token : MOCK_ADDRESSES . token ,
399+ from : MOCK_ADDRESSES . payer ,
400+ value : MOCK_VALUES . paymentAmount ,
401+ validAfter : MOCK_VALUES . validAfter ,
402+ validBefore : MOCK_VALUES . validBefore ,
403+ nonce : MOCK_VALUES . nonce ,
404+ signature : MOCK_VALUES . signature ,
405+ salt : MOCK_VALUES . salt ,
406+ payTo : MOCK_ADDRESSES . merchant ,
407+ facilitatorFee : MOCK_VALUES . facilitatorFee ,
408+ hook : MOCK_ADDRESSES . hook ,
409+ hookData : MOCK_VALUES . hookData ,
410+ settlementRouter : MOCK_ADDRESSES . settlementRouter ,
411+ } ;
412+
413+ // Call without publicClient
414+ const txHash = await executeSettlementWithRouter ( mockWalletClient , params ) ;
415+
416+ expect ( txHash ) . toBe ( mockSettleResponse . transaction ) ;
417+ // Balance check should not be performed
418+ expect ( mockPublicClient . readContract ) . not . toHaveBeenCalled ( ) ;
419+ // But transaction should still be sent
420+ expect ( mockWalletClient . writeContract ) . toHaveBeenCalled ( ) ;
421+ } ) ;
422+
423+ it ( "should handle balance check errors gracefully and proceed with transaction" , async ( ) => {
424+ const params = {
425+ token : MOCK_ADDRESSES . token ,
426+ from : MOCK_ADDRESSES . payer ,
427+ value : MOCK_VALUES . paymentAmount ,
428+ validAfter : MOCK_VALUES . validAfter ,
429+ validBefore : MOCK_VALUES . validBefore ,
430+ nonce : MOCK_VALUES . nonce ,
431+ signature : MOCK_VALUES . signature ,
432+ salt : MOCK_VALUES . salt ,
433+ payTo : MOCK_ADDRESSES . merchant ,
434+ facilitatorFee : MOCK_VALUES . facilitatorFee ,
435+ hook : MOCK_ADDRESSES . hook ,
436+ hookData : MOCK_VALUES . hookData ,
437+ settlementRouter : MOCK_ADDRESSES . settlementRouter ,
438+ } ;
439+
440+ // Mock balance check failure (e.g., RPC error)
441+ mockPublicClient . readContract . mockRejectedValue ( new Error ( "RPC timeout" ) ) ;
442+
443+ const txHash = await executeSettlementWithRouter ( mockWalletClient , params , {
444+ publicClient : mockPublicClient ,
445+ } ) ;
446+
447+ // Should still proceed with transaction despite balance check failure
448+ expect ( txHash ) . toBe ( mockSettleResponse . transaction ) ;
449+ expect ( mockWalletClient . writeContract ) . toHaveBeenCalled ( ) ;
450+ } ) ;
451+
320452 it ( "should use custom gas limit" , async ( ) => {
321453 const params = {
322454 token : MOCK_ADDRESSES . token ,
0 commit comments