@@ -26,6 +26,7 @@ let mockSupportsOnchainSends = true;
2626let mockGetLnurlParams = { } ;
2727let mockBlobUtilFetch = jest . fn ( ) ;
2828
29+ const ZEUS_ECASH_GIFT_URL = 'https://zeusln.com/e/' ;
2930jest . mock ( './AddressUtils' , ( ) => ( {
3031 processBIP21Uri : ( ...args : string [ ] ) => mockProcessBIP21Uri ( ...args ) ,
3132 isValidBitcoinAddress : ( ) => mockIsValidBitcoinAddress ,
@@ -37,7 +38,8 @@ jest.mock('./AddressUtils', () => ({
3738 processLNDHubAddress : ( ...args : any [ ] ) => mockProcessLNDHubAddress ( ...args ) ,
3839 isValidNpub : ( ) => false ,
3940 isPsbt : ( ) => false ,
40- isValidTxHex : ( ) => false
41+ isValidTxHex : ( ) => false ,
42+ ZEUS_ECASH_GIFT_URL
4143} ) ) ;
4244jest . mock ( './TorUtils' , ( ) => ( { } ) ) ;
4345jest . mock ( './BackendUtils' , ( ) => ( {
@@ -48,6 +50,13 @@ jest.mock('./BackendUtils', () => ({
4850 supportsLnurlAuth : ( ) => false
4951} ) ) ;
5052
53+ let mockIsValidCashuToken = false ;
54+ let mockDecodedCashuToken = { } ;
55+ jest . mock ( './CashuUtils' , ( ) => ( {
56+ isValidCashuToken : ( ) => mockIsValidCashuToken ,
57+ decodeCashuToken : ( ) => mockDecodedCashuToken
58+ } ) ) ;
59+
5160const mockProcessLndConnectUrl = jest . fn ( ) ;
5261const mockProcessCLNRestConnectUrl = jest . fn ( ) ;
5362const mockProcessLncUrl = jest . fn ( ) ;
@@ -99,6 +108,8 @@ describe('handleAnything', () => {
99108 mockIsValidLightningOffer = false ;
100109 mockIsValidLNDHubAddress = false ;
101110 mockIsValidNodeUri = false ;
111+ mockIsValidCashuToken = false ;
112+ mockDecodedCashuToken = { } ;
102113 } ) ;
103114
104115 describe ( 'input sanitization' , ( ) => {
@@ -1534,4 +1545,171 @@ describe('handleAnything', () => {
15341545 ) . toBe ( false ) ;
15351546 } ) ;
15361547 } ) ;
1548+
1549+ // IMPORTANT: zeusln.com/e/ URLs must be checked BEFORE the lnurl block in handleAnything,
1550+ // because findlnurl() from js-lnurl can match patterns in Cashu tokens that look like bech32 lnurls.
1551+ // These tests verify the correct route is returned when findlnurl might otherwise match.
1552+ describe ( 'zeusln.com ecash gift URLs' , ( ) => {
1553+ const validCashuToken =
1554+ 'cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSComFpSAAQeTfbDMhlYXCCpGFhCGFzeEAxOWQ3YmRiNTIwMzRmOWQ5OGQwZTU5OTEwM2FkMTlmZTAyODc1ODQ2MThlYmViYTFkNzExM2FiMjI4MDM0YjNlYWNYIQI5u9Nss3cyYXJoLTwRMY4qgCNL7-J7EtBFnGOIeq6tcWFko2FlWCBYlzHfzFSuLuI31zvZGHme0QGeeEjNoGhIs6l2fbbHH2FzWCA2ecAUKZWjtQagWP7wOUVRgsHYlyOG7CUhUFdQjUed2GFyWCC67afI6qoB6bISgCWK24JOdD_-gxUyaSrgfrZJmHsBh6RhYQRhc3hANWYzMGRlMDA1NjVkYTRhZDg2Yzk1MzljYzYzMGE3NTBlYWU2OTJiY2Q5ODgwMWI0OTI0NDA1ZDAxN2UzNGE5MWFjWCECN8hTvJgIFBwN0QY3prfyf4z7BxPVLBPptzKcnb_OupNhZKNhZVggcx4XGwTyM4riizWgckD44KUJQtKHUGGjHIJHFKdPE31hc1ggqASxqD7ZTbA59c7SAHgQvLdLq_xYLL2rAVr2ziIGfkRhclgg-bTi6nbDj8Mdq9rnKhAGgrQ4w6ihk9pvu9xUommg6V-iYWlIAFAFUPBJQUZhcIGkYWEEYXN4QDBlMzhhZWIxYzIxMTk1N2U5Njg4YTdlY2YzNzQ4Y2E2MTA0MDMzNmZjZWJmMzE4M2EwMDAwOWFiYzJiMjcxNWFhY1ghAu0yuRBFejZTitq8LJufeiB2CUAEk-pHTIWqYtFcqtCSYWSjYWVYIDhNUWOMw2xaCZT4Ob8bdV5CxkErkHh-m2XUUqCKZS3WYXNYIBUltNlKIUTrHi4M1Z8SL3l3_EPp-5eOPltdS648bpVGYXJYIFZdUQc6c-waYIhOMSS_-cS19HiHZn4xZe4s65dGN8u5' ;
1555+
1556+ it ( 'should route to CashuToken view for valid zeusln.com/e/ URL' , async ( ) => {
1557+ const url = `${ ZEUS_ECASH_GIFT_URL } ${ validCashuToken } ` ;
1558+ mockProcessBIP21Uri . mockReturnValue ( { value : url } ) ;
1559+ mockIsValidCashuToken = true ;
1560+ mockDecodedCashuToken = {
1561+ token : [
1562+ {
1563+ mint : 'https://mint.minibits.cash/Bitcoin' ,
1564+ proofs : [ { amount : 8 } , { amount : 4 } , { amount : 4 } ]
1565+ }
1566+ ] ,
1567+ unit : 'sat'
1568+ } ;
1569+
1570+ const result = await handleAnything ( url ) ;
1571+
1572+ expect ( result ) . toEqual ( [
1573+ 'CashuToken' ,
1574+ {
1575+ token : validCashuToken ,
1576+ decoded : expect . any ( Object )
1577+ }
1578+ ] ) ;
1579+ } ) ;
1580+
1581+ it ( 'should route to CashuToken even when findlnurl would match token content' , async ( ) => {
1582+ // This test simulates the bug where findlnurl matched patterns in Cashu tokens
1583+ const url = `${ ZEUS_ECASH_GIFT_URL } ${ validCashuToken } ` ;
1584+ mockProcessBIP21Uri . mockReturnValue ( { value : url } ) ;
1585+ mockIsValidCashuToken = true ;
1586+ mockDecodedCashuToken = {
1587+ token : [
1588+ {
1589+ mint : 'https://mint.minibits.cash/Bitcoin' ,
1590+ proofs : [ { amount : 16 } ]
1591+ }
1592+ ] ,
1593+ unit : 'sat'
1594+ } ;
1595+ // Simulate findlnurl returning a match (which would happen with some Cashu tokens)
1596+ mockFindLnurl . mockReturnValue ( 'lnurl1somefalsepositive' ) ;
1597+
1598+ const result = await handleAnything ( url ) ;
1599+
1600+ // Should still route to CashuToken, NOT to lnurl handling
1601+ expect ( result ) . toEqual ( [
1602+ 'CashuToken' ,
1603+ {
1604+ token : validCashuToken ,
1605+ decoded : expect . any ( Object )
1606+ }
1607+ ] ) ;
1608+ } ) ;
1609+
1610+ it ( 'should return true for valid zeusln.com/e/ URL from clipboard' , async ( ) => {
1611+ const url = `${ ZEUS_ECASH_GIFT_URL } ${ validCashuToken } ` ;
1612+ mockProcessBIP21Uri . mockReturnValue ( { value : url } ) ;
1613+ mockIsValidCashuToken = true ;
1614+ mockDecodedCashuToken = {
1615+ token : [
1616+ {
1617+ mint : 'https://mint.minibits.cash/Bitcoin' ,
1618+ proofs : [ { amount : 16 } ]
1619+ }
1620+ ] ,
1621+ unit : 'sat'
1622+ } ;
1623+
1624+ const result = await handleAnything ( url , undefined , true ) ;
1625+
1626+ expect ( result ) . toBe ( true ) ;
1627+ } ) ;
1628+
1629+ it ( 'should route to CashuToken for V3 token (cashuA prefix)' , async ( ) => {
1630+ const simpleToken =
1631+ 'cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFtdLCAibWludCI6ICJodHRwczovL21pbnQuZXhhbXBsZS5jb20ifV19' ;
1632+ const url = `${ ZEUS_ECASH_GIFT_URL } ${ simpleToken } ` ;
1633+ mockProcessBIP21Uri . mockReturnValue ( { value : url } ) ;
1634+ mockIsValidCashuToken = true ;
1635+ mockDecodedCashuToken = {
1636+ token : [
1637+ {
1638+ mint : 'https://mint.example.com' ,
1639+ proofs : [ ]
1640+ }
1641+ ]
1642+ } ;
1643+
1644+ const result = await handleAnything ( url ) ;
1645+
1646+ expect ( result ) . toEqual ( [
1647+ 'CashuToken' ,
1648+ {
1649+ token : simpleToken ,
1650+ decoded : expect . any ( Object )
1651+ }
1652+ ] ) ;
1653+ } ) ;
1654+
1655+ it ( 'should throw error for zeusln.com/e/ URL with invalid token' , async ( ) => {
1656+ const invalidToken = 'notavalidcashutoken' ;
1657+ const url = `${ ZEUS_ECASH_GIFT_URL } ${ invalidToken } ` ;
1658+ mockProcessBIP21Uri . mockReturnValue ( { value : url } ) ;
1659+ mockIsValidCashuToken = false ;
1660+
1661+ await expect ( handleAnything ( url ) ) . rejects . toThrow ( ) ;
1662+ } ) ;
1663+
1664+ it ( 'should return false for zeusln.com/e/ URL with invalid token from clipboard' , async ( ) => {
1665+ const invalidToken = 'notavalidcashutoken' ;
1666+ const url = `${ ZEUS_ECASH_GIFT_URL } ${ invalidToken } ` ;
1667+ mockProcessBIP21Uri . mockReturnValue ( { value : url } ) ;
1668+ mockIsValidCashuToken = false ;
1669+
1670+ const result = await handleAnything ( url , undefined , true ) ;
1671+
1672+ expect ( result ) . toBe ( false ) ;
1673+ } ) ;
1674+
1675+ it ( 'should handle zeusln.com/e/ URL with whitespace trimmed' , async ( ) => {
1676+ const url = ` ${ ZEUS_ECASH_GIFT_URL } ${ validCashuToken } ` ;
1677+ mockProcessBIP21Uri . mockReturnValue ( {
1678+ value : `${ ZEUS_ECASH_GIFT_URL } ${ validCashuToken } `
1679+ } ) ;
1680+ mockIsValidCashuToken = true ;
1681+ mockDecodedCashuToken = {
1682+ token : [
1683+ {
1684+ mint : 'https://mint.minibits.cash/Bitcoin' ,
1685+ proofs : [ { amount : 16 } ]
1686+ }
1687+ ] ,
1688+ unit : 'sat'
1689+ } ;
1690+
1691+ const result = await handleAnything ( url ) ;
1692+
1693+ expect ( result [ 0 ] ) . toBe ( 'CashuToken' ) ;
1694+ } ) ;
1695+
1696+ it ( 'should not match similar but different URLs' , async ( ) => {
1697+ // URL that starts with zeusln.com but not /e/ path
1698+ const url = 'https://zeusln.com/download' ;
1699+ mockProcessBIP21Uri . mockReturnValue ( { value : url } ) ;
1700+ mockIsValidCashuToken = false ;
1701+
1702+ // Should fall through to error since it's not a valid payment method
1703+ await expect ( handleAnything ( url ) ) . rejects . toThrow ( ) ;
1704+ } ) ;
1705+
1706+ it ( 'should not match http:// URLs (only https://)' , async ( ) => {
1707+ const url = `http://zeusln.com/e/${ validCashuToken } ` ;
1708+ mockProcessBIP21Uri . mockReturnValue ( { value : url } ) ;
1709+ mockIsValidCashuToken = false ;
1710+
1711+ // http:// should not match the https:// pattern
1712+ await expect ( handleAnything ( url ) ) . rejects . toThrow ( ) ;
1713+ } ) ;
1714+ } ) ;
15371715} ) ;
0 commit comments