diff --git a/class/deeplink-schema-match.js b/class/deeplink-schema-match.js index e39a3e44d..1b2ca41dd 100644 --- a/class/deeplink-schema-match.js +++ b/class/deeplink-schema-match.js @@ -153,6 +153,18 @@ class DeeplinkSchemaMatch { }, }, ]); + } else if (Lnurl.isLightningAddress(event.url)) { + // this might be not just an email but a lightning addres + // @see https://lightningaddress.com + completionHandler([ + 'ScanLndInvoiceRoot', + { + screen: 'ScanLndInvoice', + params: { + uri: event.url, + }, + }, + ]); } else if (DeeplinkSchemaMatch.isSafelloRedirect(event)) { const urlObject = url.parse(event.url, true); // eslint-disable-line node/no-deprecated-api diff --git a/class/lnurl.js b/class/lnurl.js index 5a3e85585..c82dd7a51 100644 --- a/class/lnurl.js +++ b/class/lnurl.js @@ -28,7 +28,15 @@ export default class Lnurl { static getUrlFromLnurl(lnurlExample) { const found = Lnurl.findlnurl(lnurlExample); - if (!found) return false; + if (!found) { + if (Lnurl.isLightningAddress(lnurlExample)) { + const username = lnurlExample.split('@')[0].trim(); + const host = lnurlExample.split('@')[1].trim(); + return `https://${host}/.well-known/lnurlp/${username}`; + } else { + return false; + } + } const decoded = bech32.decode(found, 10000); return Buffer.from(bech32.fromWords(decoded.words)).toString(); @@ -256,4 +264,11 @@ export default class Lnurl { getCommentAllowed() { return this?._lnurlPayServicePayload?.commentAllowed ? parseInt(this._lnurlPayServicePayload.commentAllowed) : false; } + + static isLightningAddress(address) { + // ensure only 1 `@` present: + if (address.split('@').length !== 2) return false; + const splitted = address.split('@'); + return !!splitted[0].trim() && !!splitted[1].trim(); + } } diff --git a/screen/lnd/lnurlPay.js b/screen/lnd/lnurlPay.js index 4f192e2d4..85a69859d 100644 --- a/screen/lnd/lnurlPay.js +++ b/screen/lnd/lnurlPay.js @@ -56,11 +56,16 @@ const LnurlPay = () => { useEffect(() => { if (lnurl) { const ln = new Lnurl(lnurl, AsyncStorage); - ln.callLnurlPayService().then(setPayload); + ln.callLnurlPayService() + .then(setPayload) + .catch(error => { + alert(error.message); + pop(); + }); setLN(ln); setIsLoading(false); } - }, [lnurl]); + }, [lnurl, pop]); useEffect(() => { setPayButtonDisabled(isLoading); diff --git a/screen/lnd/scanLndInvoice.js b/screen/lnd/scanLndInvoice.js index b07dd7c19..1f333450b 100644 --- a/screen/lnd/scanLndInvoice.js +++ b/screen/lnd/scanLndInvoice.js @@ -92,6 +92,9 @@ const ScanLndInvoice = () => { useEffect(() => { if (wallet && uri) { + if (Lnurl.isLnurl(uri)) return processLnurlPay(uri); + if (Lnurl.isLightningAddress(uri)) return processLnurlPay(uri); + let data = uri; // handling BIP21 w/BOLT11 support const ind = data.indexOf('lightning='); @@ -102,9 +105,6 @@ const ScanLndInvoice = () => { data = data.replace('LIGHTNING:', '').replace('lightning:', ''); console.log(data); - /** - * @type {LightningCustodianWallet} - */ let decoded; try { decoded = wallet.decodeInvoice(data); @@ -144,6 +144,7 @@ const ScanLndInvoice = () => { const processInvoice = data => { if (Lnurl.isLnurl(data)) return processLnurlPay(data); + if (Lnurl.isLightningAddress(data)) return processLnurlPay(data); setParams({ uri: data }); }; @@ -216,7 +217,12 @@ const ScanLndInvoice = () => { }; const processTextForInvoice = text => { - if (text.toLowerCase().startsWith('lnb') || text.toLowerCase().startsWith('lightning:lnb') || Lnurl.isLnurl(text)) { + if ( + text.toLowerCase().startsWith('lnb') || + text.toLowerCase().startsWith('lightning:lnb') || + Lnurl.isLnurl(text) || + Lnurl.isLightningAddress(text) + ) { processInvoice(text); } else { setDecoded(undefined); diff --git a/tests/unit/deeplink-schema-match.test.js b/tests/unit/deeplink-schema-match.test.js index 8f62475d1..99e2c77c4 100644 --- a/tests/unit/deeplink-schema-match.test.js +++ b/tests/unit/deeplink-schema-match.test.js @@ -202,6 +202,20 @@ describe('unit - DeepLinkSchemaMatch', function () { }, ], }, + { + argument: { + url: 'lnaddress@zbd.gg', + }, + expected: [ + 'ScanLndInvoiceRoot', + { + screen: 'ScanLndInvoice', + params: { + uri: 'lnaddress@zbd.gg', + }, + }, + ], + }, { argument: { url: require('fs').readFileSync('./tests/unit/fixtures/skeleton-cobo.txt', 'ascii'), diff --git a/tests/unit/lnurl.test.js b/tests/unit/lnurl.test.js index 3be0c28ea..a4bb6a419 100644 --- a/tests/unit/lnurl.test.js +++ b/tests/unit/lnurl.test.js @@ -164,3 +164,55 @@ describe('LNURL', function () { assert.strictEqual(Lnurl.decipherAES(ciphertext, preimage, iv), '1234'); }); }); + +describe('lightning address', function () { + it('can getUrlFromLnurl()', () => { + assert.strictEqual(Lnurl.getUrlFromLnurl('lnaddress@zbd.gg'), 'https://zbd.gg/.well-known/lnurlp/lnaddress'); + }); + + it('can detect', async () => { + assert.ok(Lnurl.isLightningAddress('lnaddress@zbd.gg')); + assert.ok(Lnurl.isLightningAddress(' lnaddress@zbd.gg ')); + assert.ok(Lnurl.isLightningAddress(' lnaddress@zbd.gg ')); + assert.ok(Lnurl.isLightningAddress(' lnaddress@8.8.8.8 ')); + assert.ok(Lnurl.isLightningAddress(' lnaddress@hidden.onion ')); + assert.ok(!Lnurl.isLightningAddress(' bla bla ')); + assert.ok(!Lnurl.isLightningAddress('')); + assert.ok(!Lnurl.isLightningAddress('@')); + assert.ok(!Lnurl.isLightningAddress('@a')); + assert.ok(!Lnurl.isLightningAddress('a@')); + + const LN = new Lnurl('lnaddress@zbd.gg'); + + // poor-man's mock: + LN._fetchGet = LN.fetchGet; + let requestedUri = -1; + LN.fetchGet = actuallyRequestedUri => { + requestedUri = actuallyRequestedUri; + return { + minSendable: 1000, + maxSendable: 45000000, + commentAllowed: 150, + tag: 'payRequest', + metadata: '[["text/plain","lnaddress - lightningaddress.com"],["text/identifier","lnaddress@zbd.gg"],["image/png;base64","img"]]', + callback: 'https://api.zebedee.io/v0/process-static-charges/9a44621d-0665-44eb-96af-e06534311be5', + }; + }; + + const lnurlpayPayload = await LN.callLnurlPayService(); + assert.deepStrictEqual(lnurlpayPayload, { + amount: 1, + callback: 'https://api.zebedee.io/v0/process-static-charges/9a44621d-0665-44eb-96af-e06534311be5', + commentAllowed: 150, + description: 'lnaddress - lightningaddress.com', + domain: 'api.zebedee.io', + fixed: false, + image: 'data:image/png;base64,img', + max: 45000, + metadata: '[["text/plain","lnaddress - lightningaddress.com"],["text/identifier","lnaddress@zbd.gg"],["image/png;base64","img"]]', + min: 1, + }); + + assert.strictEqual(requestedUri, 'https://zbd.gg/.well-known/lnurlp/lnaddress'); + }); +});