mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 18:00:17 +01:00
ADD: lnurl-pay support
This commit is contained in:
parent
3a96d1eb71
commit
f386bb345b
@ -1,6 +1,3 @@
|
|||||||
/**
|
|
||||||
* @exports {AppStorage}
|
|
||||||
*/
|
|
||||||
import { AppStorage } from './class';
|
import { AppStorage } from './class';
|
||||||
import DeviceQuickActions from './class/quick-actions';
|
import DeviceQuickActions from './class/quick-actions';
|
||||||
import Biometric from './class/biometrics';
|
import Biometric from './class/biometrics';
|
||||||
@ -10,8 +7,7 @@ const prompt = require('./blue_modules/prompt');
|
|||||||
const EV = require('./blue_modules/events');
|
const EV = require('./blue_modules/events');
|
||||||
const currency = require('./blue_modules/currency');
|
const currency = require('./blue_modules/currency');
|
||||||
const BlueElectrum = require('./blue_modules/BlueElectrum'); // eslint-disable-line no-unused-vars
|
const BlueElectrum = require('./blue_modules/BlueElectrum'); // eslint-disable-line no-unused-vars
|
||||||
/** @type {AppStorage} */
|
const BlueApp: AppStorage = new AppStorage();
|
||||||
const BlueApp = new AppStorage();
|
|
||||||
// If attempt reaches 10, a wipe keychain option will be provided to the user.
|
// If attempt reaches 10, a wipe keychain option will be provided to the user.
|
||||||
let unlockAttempt = 0;
|
let unlockAttempt = 0;
|
||||||
|
|
||||||
|
@ -39,6 +39,8 @@ import QRCode from 'react-native-qrcode-svg';
|
|||||||
import { useTheme } from '@react-navigation/native';
|
import { useTheme } from '@react-navigation/native';
|
||||||
import { BlueCurrentTheme } from './components/themes';
|
import { BlueCurrentTheme } from './components/themes';
|
||||||
import loc, { formatBalance, formatBalanceWithoutSuffix, formatBalancePlain, removeTrailingZeros, transactionTimeToReadable } from './loc';
|
import loc, { formatBalance, formatBalanceWithoutSuffix, formatBalancePlain, removeTrailingZeros, transactionTimeToReadable } from './loc';
|
||||||
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
|
import Lnurl from './class/lnurl';
|
||||||
/** @type {AppStorage} */
|
/** @type {AppStorage} */
|
||||||
const BlueApp = require('./BlueApp');
|
const BlueApp = require('./BlueApp');
|
||||||
const { height, width } = Dimensions.get('window');
|
const { height, width } = Dimensions.get('window');
|
||||||
@ -1698,7 +1700,7 @@ export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = Bitco
|
|||||||
return (item.confirmations < 7 ? loc.transactions.list_conf + ': ' + item.confirmations + ' ' : '') + txMemo() + (item.memo || '');
|
return (item.confirmations < 7 ? loc.transactions.list_conf + ': ' + item.confirmations + ' ' : '') + txMemo() + (item.memo || '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPress = () => {
|
const onPress = async () => {
|
||||||
if (item.hash) {
|
if (item.hash) {
|
||||||
NavigationService.navigate('TransactionStatus', { hash: item.hash });
|
NavigationService.navigate('TransactionStatus', { hash: item.hash });
|
||||||
} else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') {
|
} else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') {
|
||||||
@ -1710,6 +1712,25 @@ export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = Bitco
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (lightningWallet.length === 1) {
|
if (lightningWallet.length === 1) {
|
||||||
|
// is it a successful lnurl-pay?
|
||||||
|
const LN = new Lnurl(false, AsyncStorage);
|
||||||
|
let paymentHash = item.payment_hash;
|
||||||
|
if (typeof paymentHash === 'object') {
|
||||||
|
paymentHash = Buffer.from(paymentHash.data).toString('hex');
|
||||||
|
}
|
||||||
|
const loaded = await LN.loadSuccessfulPayment(paymentHash);
|
||||||
|
if (loaded) {
|
||||||
|
NavigationService.navigate('ScanLndInvoiceRoot', {
|
||||||
|
screen: 'LnurlPaySuccess',
|
||||||
|
params: {
|
||||||
|
paymentHash: paymentHash,
|
||||||
|
justPaid: false,
|
||||||
|
fromWalletID: lightningWallet[0].getID(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
NavigationService.navigate('LNDViewInvoice', {
|
NavigationService.navigate('LNDViewInvoice', {
|
||||||
invoice: item,
|
invoice: item,
|
||||||
fromWallet: lightningWallet[0],
|
fromWallet: lightningWallet[0],
|
||||||
|
@ -60,6 +60,8 @@ import LappBrowser from './screen/lnd/browser';
|
|||||||
import LNDCreateInvoice from './screen/lnd/lndCreateInvoice';
|
import LNDCreateInvoice from './screen/lnd/lndCreateInvoice';
|
||||||
import LNDViewInvoice from './screen/lnd/lndViewInvoice';
|
import LNDViewInvoice from './screen/lnd/lndViewInvoice';
|
||||||
import LNDViewAdditionalInvoiceInformation from './screen/lnd/lndViewAdditionalInvoiceInformation';
|
import LNDViewAdditionalInvoiceInformation from './screen/lnd/lndViewAdditionalInvoiceInformation';
|
||||||
|
import LnurlPay from './screen/lnd/lnurlPay';
|
||||||
|
import LnurlPaySuccess from './screen/lnd/lnurlPaySuccess';
|
||||||
import LoadingScreen from './LoadingScreen';
|
import LoadingScreen from './LoadingScreen';
|
||||||
import UnlockWith from './UnlockWith';
|
import UnlockWith from './UnlockWith';
|
||||||
import { BlueNavigationStyle } from './BlueComponents';
|
import { BlueNavigationStyle } from './BlueComponents';
|
||||||
@ -135,6 +137,8 @@ const WalletsRoot = () => (
|
|||||||
/>
|
/>
|
||||||
<WalletsStack.Screen name="HodlHodlViewOffer" component={HodlHodlViewOffer} options={HodlHodlViewOffer.navigationOptions} />
|
<WalletsStack.Screen name="HodlHodlViewOffer" component={HodlHodlViewOffer} options={HodlHodlViewOffer.navigationOptions} />
|
||||||
<WalletsStack.Screen name="Broadcast" component={Broadcast} options={Broadcast.navigationOptions} />
|
<WalletsStack.Screen name="Broadcast" component={Broadcast} options={Broadcast.navigationOptions} />
|
||||||
|
<WalletsStack.Screen name="LnurlPay" component={LnurlPay} options={LnurlPay.navigationOptions} />
|
||||||
|
<WalletsStack.Screen name="LnurlPaySuccess" component={LnurlPaySuccess} options={LnurlPaySuccess.navigationOptions} />
|
||||||
</WalletsStack.Navigator>
|
</WalletsStack.Navigator>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -203,6 +207,8 @@ const ScanLndInvoiceRoot = () => (
|
|||||||
<ScanLndInvoiceStack.Screen name="ScanLndInvoice" component={ScanLndInvoice} options={ScanLndInvoice.navigationOptions} />
|
<ScanLndInvoiceStack.Screen name="ScanLndInvoice" component={ScanLndInvoice} options={ScanLndInvoice.navigationOptions} />
|
||||||
<ScanLndInvoiceStack.Screen name="SelectWallet" component={SelectWallet} options={SelectWallet.navigationOptions} />
|
<ScanLndInvoiceStack.Screen name="SelectWallet" component={SelectWallet} options={SelectWallet.navigationOptions} />
|
||||||
<ScanLndInvoiceStack.Screen name="Success" component={Success} options={Success.navigationOptions} />
|
<ScanLndInvoiceStack.Screen name="Success" component={Success} options={Success.navigationOptions} />
|
||||||
|
<ScanLndInvoiceStack.Screen name="LnurlPay" component={LnurlPay} options={LnurlPay.navigationOptions} />
|
||||||
|
<ScanLndInvoiceStack.Screen name="LnurlPaySuccess" component={LnurlPaySuccess} options={LnurlPaySuccess.navigationOptions} />
|
||||||
</ScanLndInvoiceStack.Navigator>
|
</ScanLndInvoiceStack.Navigator>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -96,6 +96,9 @@ class DeeplinkSchemaMatch {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else if (DeeplinkSchemaMatch.isLnUrl(event.url)) {
|
} else if (DeeplinkSchemaMatch.isLnUrl(event.url)) {
|
||||||
|
// at this point we can not tell if it is lnurl-pay or lnurl-withdraw since it needs additional async call
|
||||||
|
// to the server, which is undesirable here, so LNDCreateInvoice screen will handle it for us and will
|
||||||
|
// redirect user to LnurlPay screen if necessary
|
||||||
completionHandler([
|
completionHandler([
|
||||||
'LNDCreateInvoiceRoot',
|
'LNDCreateInvoiceRoot',
|
||||||
{
|
{
|
||||||
|
243
class/lnurl.js
Normal file
243
class/lnurl.js
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
import bech32 from 'bech32';
|
||||||
|
import bolt11 from 'bolt11';
|
||||||
|
const CryptoJS = require('crypto-js');
|
||||||
|
|
||||||
|
const createHash = require('create-hash');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://github.com/btcontract/lnurl-rfc/blob/master/lnurl-pay.md
|
||||||
|
*/
|
||||||
|
export default class Lnurl {
|
||||||
|
static TAG_PAY_REQUEST = 'payRequest'; // type of LNURL
|
||||||
|
static TAG_WITHDRAW_REQUEST = "withdrawRequest"; // type of LNURL
|
||||||
|
|
||||||
|
constructor(url, AsyncStorage) {
|
||||||
|
this._lnurl = url;
|
||||||
|
this._lnurlPayServiceBolt11Payload = false;
|
||||||
|
this._lnurlPayServicePayload = false;
|
||||||
|
this._AsyncStorage = AsyncStorage;
|
||||||
|
this._preimage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static findlnurl(bodyOfText) {
|
||||||
|
var res = /,*?((lnurl)([0-9]{1,}[a-z0-9]+){1})/.exec(bodyOfText.toLowerCase());
|
||||||
|
if (res) {
|
||||||
|
return res[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getUrlFromLnurl(lnurlExample) {
|
||||||
|
const found = Lnurl.findlnurl(lnurlExample);
|
||||||
|
if (!found) return false;
|
||||||
|
|
||||||
|
const decoded = bech32.decode(lnurlExample, 10000);
|
||||||
|
return Buffer.from(bech32.fromWords(decoded.words)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static isLnurl(url) {
|
||||||
|
return url.toLowerCase().startsWith('lnurl1');
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchGet(url) {
|
||||||
|
const resp = await fetch(url, { method: 'GET' });
|
||||||
|
if (resp.status >= 300) {
|
||||||
|
throw new Error('Bad response from server');
|
||||||
|
}
|
||||||
|
const reply = await resp.json();
|
||||||
|
if (reply.status === 'ERROR') {
|
||||||
|
throw new Error('Reply from server: ' + reply.reason);
|
||||||
|
}
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeInvoice(invoice) {
|
||||||
|
const { payeeNodeKey, tags, satoshis, millisatoshis, timestamp } = bolt11.decode(invoice);
|
||||||
|
|
||||||
|
const decoded = {
|
||||||
|
destination: payeeNodeKey,
|
||||||
|
num_satoshis: satoshis ? satoshis.toString() : '0',
|
||||||
|
num_millisatoshis: millisatoshis ? millisatoshis.toString() : '0',
|
||||||
|
timestamp: timestamp.toString(),
|
||||||
|
fallback_addr: '',
|
||||||
|
route_hints: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < tags.length; i++) {
|
||||||
|
const { tagName, data } = tags[i];
|
||||||
|
switch (tagName) {
|
||||||
|
case 'payment_hash':
|
||||||
|
decoded.payment_hash = data;
|
||||||
|
break;
|
||||||
|
case 'purpose_commit_hash':
|
||||||
|
decoded.description_hash = data;
|
||||||
|
break;
|
||||||
|
case 'min_final_cltv_expiry':
|
||||||
|
decoded.cltv_expiry = data.toString();
|
||||||
|
break;
|
||||||
|
case 'expire_time':
|
||||||
|
decoded.expiry = data.toString();
|
||||||
|
break;
|
||||||
|
case 'description':
|
||||||
|
decoded.description = data;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decoded.expiry) decoded.expiry = '3600'; // default
|
||||||
|
|
||||||
|
if (parseInt(decoded.num_satoshis) === 0 && decoded.num_millisatoshis > 0) {
|
||||||
|
decoded.num_satoshis = (decoded.num_millisatoshis / 1000).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestBolt11FromLnurlPayService(amountSat) {
|
||||||
|
if (!this._lnurlPayServicePayload) throw new Error('this._lnurlPayServicePayload is not set');
|
||||||
|
if (!this._lnurlPayServicePayload.callback) throw new Error('this._lnurlPayServicePayload.callback is not set');
|
||||||
|
if (amountSat < this._lnurlPayServicePayload.min || amountSat > this._lnurlPayServicePayload.max)
|
||||||
|
throw new Error('amount is not right, ' + amountSat + ' should be between ' + this._lnurlPayServicePayload.min + ' and ' + this._lnurlPayServicePayload.max);
|
||||||
|
const nonce = Math.floor(Math.random() * 2e16).toString(16);
|
||||||
|
const separator = this._lnurlPayServicePayload.callback.indexOf('?') === -1 ? '?' : '&';
|
||||||
|
const urlToFetch = this._lnurlPayServicePayload.callback + separator + 'amount=' + Math.floor(amountSat * 1000) + '&nonce=' + nonce;
|
||||||
|
this._lnurlPayServiceBolt11Payload = await this.fetchGet(urlToFetch);
|
||||||
|
if (this._lnurlPayServiceBolt11Payload.status === 'ERROR')
|
||||||
|
throw new Error(this._lnurlPayServiceBolt11Payload.reason || 'requestBolt11FromLnurlPayService() error');
|
||||||
|
|
||||||
|
// check pr description_hash, amount etc:
|
||||||
|
const decoded = this.decodeInvoice(this._lnurlPayServiceBolt11Payload.pr);
|
||||||
|
const metadataHash = createHash('sha256').update(this._lnurlPayServicePayload.metadata).digest('hex');
|
||||||
|
if (metadataHash !== decoded.description_hash) {
|
||||||
|
throw new Error(`Invoice description_hash doesn't match metadata.`);
|
||||||
|
}
|
||||||
|
if (parseInt(decoded.num_satoshis) !== Math.round(amountSat)) {
|
||||||
|
throw new Error(`Invoice doesn't match specified amount, got ${decoded.num_satoshis}, expected ${Math.round(amountSat)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._lnurlPayServiceBolt11Payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async callLnurlPayService() {
|
||||||
|
if (!this._lnurl) throw new Error('this._lnurl is not set');
|
||||||
|
const url = Lnurl.getUrlFromLnurl(this._lnurl);
|
||||||
|
// calling the url
|
||||||
|
const reply = await this.fetchGet(url);
|
||||||
|
|
||||||
|
if (reply.tag !== Lnurl.TAG_PAY_REQUEST) {
|
||||||
|
throw new Error('lnurl-pay expected, found tag ' + reply.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = reply;
|
||||||
|
|
||||||
|
// parse metadata and extract things from it
|
||||||
|
var image;
|
||||||
|
var description;
|
||||||
|
const kvs = JSON.parse(data.metadata);
|
||||||
|
for (let i = 0; i < kvs.length; i++) {
|
||||||
|
const [k, v] = kvs[i];
|
||||||
|
switch (k) {
|
||||||
|
case 'text/plain':
|
||||||
|
description = v;
|
||||||
|
break;
|
||||||
|
case 'image/png;base64':
|
||||||
|
case 'image/jpeg;base64':
|
||||||
|
image = 'data:' + k + ',' + v;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setting the payment screen with the parameters
|
||||||
|
const min = Math.ceil((data.minSendable || 0) / 1000);
|
||||||
|
const max = Math.floor(data.maxSendable / 1000);
|
||||||
|
|
||||||
|
this._lnurlPayServicePayload = {
|
||||||
|
callback: data.callback,
|
||||||
|
fixed: min === max,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
domain: data.callback.match(new RegExp('https://([^/]+)/'))[1],
|
||||||
|
metadata: data.metadata,
|
||||||
|
description,
|
||||||
|
image,
|
||||||
|
amount: min,
|
||||||
|
// lnurl: uri,
|
||||||
|
};
|
||||||
|
return this._lnurlPayServicePayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSuccessfulPayment(paymentHash) {
|
||||||
|
if (!paymentHash) throw new Error('No paymentHash provided');
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await this._AsyncStorage.getItem('lnurlpay_success_data_' + paymentHash);
|
||||||
|
data = JSON.parse(data);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) return false;
|
||||||
|
|
||||||
|
this._lnurlPayServicePayload = data.lnurlPayServicePayload;
|
||||||
|
this._lnurlPayServiceBolt11Payload = data.lnurlPayServiceBolt11Payload;
|
||||||
|
this._lnurl = data.lnurl;
|
||||||
|
this._preimage = data.preimage;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeSuccess(paymentHash, preimage) {
|
||||||
|
if (typeof preimage === 'object') {
|
||||||
|
preimage = Buffer.from(preimage.data).toString('hex');
|
||||||
|
}
|
||||||
|
this._preimage = preimage;
|
||||||
|
|
||||||
|
await this._AsyncStorage.setItem(
|
||||||
|
'lnurlpay_success_data_' + paymentHash,
|
||||||
|
JSON.stringify({
|
||||||
|
lnurlPayServicePayload: this._lnurlPayServicePayload,
|
||||||
|
lnurlPayServiceBolt11Payload: this._lnurlPayServiceBolt11Payload,
|
||||||
|
lnurl: this._lnurl,
|
||||||
|
preimage,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSuccessAction() {
|
||||||
|
return this._lnurlPayServiceBolt11Payload.successAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDomain() {
|
||||||
|
return this._lnurlPayServicePayload.domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription() {
|
||||||
|
return this._lnurlPayServicePayload.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
getImage() {
|
||||||
|
return this._lnurlPayServicePayload.image;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLnurl() {
|
||||||
|
return this._lnurl;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisposable() {
|
||||||
|
return this._lnurlPayServiceBolt11Payload.disposable;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreimage() {
|
||||||
|
return this._preimage;
|
||||||
|
}
|
||||||
|
|
||||||
|
static decipherAES(ciphertextBase64, preimageHex, ivBase64) {
|
||||||
|
const iv = CryptoJS.enc.Base64.parse(ivBase64);
|
||||||
|
const key = CryptoJS.enc.Hex.parse(preimageHex);
|
||||||
|
return CryptoJS.AES.decrypt(Buffer.from(ciphertextBase64, 'base64').toString('hex'), key, {
|
||||||
|
iv,
|
||||||
|
mode: CryptoJS.mode.CBC,
|
||||||
|
format: CryptoJS.format.Hex,
|
||||||
|
}).toString(CryptoJS.enc.Utf8);
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,7 @@ import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
|||||||
import { Icon } from 'react-native-elements';
|
import { Icon } from 'react-native-elements';
|
||||||
import loc, { formatBalanceWithoutSuffix, formatBalancePlain } from '../../loc';
|
import loc, { formatBalanceWithoutSuffix, formatBalancePlain } from '../../loc';
|
||||||
import { BlueCurrentTheme } from '../../components/themes';
|
import { BlueCurrentTheme } from '../../components/themes';
|
||||||
|
import Lnurl from '../../class/lnurl';
|
||||||
const currency = require('../../blue_modules/currency');
|
const currency = require('../../blue_modules/currency');
|
||||||
const BlueApp = require('../../BlueApp');
|
const BlueApp = require('../../BlueApp');
|
||||||
const EV = require('../../blue_modules/events');
|
const EV = require('../../blue_modules/events');
|
||||||
@ -290,7 +291,20 @@ export default class LNDCreateInvoice extends Component {
|
|||||||
throw new Error('Reply from server: ' + reply.reason);
|
throw new Error('Reply from server: ' + reply.reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reply.tag !== 'withdrawRequest') {
|
if (reply.tag === Lnurl.TAG_PAY_REQUEST) {
|
||||||
|
// we are here by mistake. user wants to SEND to lnurl-pay, but he is on a screen that creates
|
||||||
|
// invoices (including through lnurl-withdraw)
|
||||||
|
this.props.navigation.navigate('ScanLndInvoiceRoot', {
|
||||||
|
screen: 'LnurlPay',
|
||||||
|
params: {
|
||||||
|
lnurl: data,
|
||||||
|
fromWalletID: this.state.fromWallet.getID(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reply.tag !== Lnurl.TAG_WITHDRAW_REQUEST) {
|
||||||
throw new Error('Unsupported lnurl');
|
throw new Error('Unsupported lnurl');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
270
screen/lnd/lnurlPay.js
Normal file
270
screen/lnd/lnurlPay.js
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
/* global alert */
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
|
import {
|
||||||
|
BlueBitcoinAmount,
|
||||||
|
BlueButton,
|
||||||
|
BlueCard,
|
||||||
|
BlueDismissKeyboardInputAccessory,
|
||||||
|
BlueLoading,
|
||||||
|
BlueNavigationStyle,
|
||||||
|
BlueSpacing20,
|
||||||
|
BlueText,
|
||||||
|
SafeBlueArea,
|
||||||
|
} from '../../BlueComponents';
|
||||||
|
import { BlueCurrentTheme } from '../../components/themes';
|
||||||
|
import Lnurl from '../../class/lnurl';
|
||||||
|
import { Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||||
|
import loc, { formatBalanceWithoutSuffix } from '../../loc';
|
||||||
|
import { Icon } from 'react-native-elements';
|
||||||
|
import Biometric from '../../class/biometrics';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const BlueApp = require('../../BlueApp');
|
||||||
|
const EV = require('../../blue_modules/events');
|
||||||
|
const currency = require('../../blue_modules/currency');
|
||||||
|
|
||||||
|
export default class LnurlPay extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const fromWalletID = props.route.params.fromWalletID;
|
||||||
|
const lnurl = props.route.params.lnurl;
|
||||||
|
|
||||||
|
const fromWallet = BlueApp.getWallets().find(w => w.getID() === fromWalletID);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isLoading: true,
|
||||||
|
fromWalletID,
|
||||||
|
fromWallet,
|
||||||
|
lnurl,
|
||||||
|
payButtonDisabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const LN = new Lnurl(this.state.lnurl, AsyncStorage);
|
||||||
|
const payload = await LN.callLnurlPayService();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
payload,
|
||||||
|
amount: payload.min,
|
||||||
|
isLoading: false,
|
||||||
|
unit: BitcoinUnit.SATS,
|
||||||
|
LN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onWalletSelect = wallet => {
|
||||||
|
this.setState({ fromWallet: wallet, fromWalletID: wallet.getID() }, () => {
|
||||||
|
this.props.navigation.pop();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
pay = async () => {
|
||||||
|
this.setState({
|
||||||
|
payButtonDisabled: true,
|
||||||
|
});
|
||||||
|
/** @type {Lnurl} */
|
||||||
|
const LN = this.state.LN;
|
||||||
|
|
||||||
|
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
|
||||||
|
if (isBiometricsEnabled) {
|
||||||
|
if (!(await Biometric.unlockWithBiometrics())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let amountSats = this.state.amount;
|
||||||
|
switch (this.state.unit) {
|
||||||
|
case BitcoinUnit.SATS:
|
||||||
|
amountSats = parseInt(amountSats); // nop
|
||||||
|
break;
|
||||||
|
case BitcoinUnit.BTC:
|
||||||
|
amountSats = currency.btcToSatoshi(amountSats);
|
||||||
|
break;
|
||||||
|
case BitcoinUnit.LOCAL_CURRENCY:
|
||||||
|
amountSats = currency.btcToSatoshi(currency.fiatToBTC(amountSats));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {LightningCustodianWallet} */
|
||||||
|
const fromWallet = this.state.fromWallet;
|
||||||
|
|
||||||
|
let bolt11payload;
|
||||||
|
try {
|
||||||
|
bolt11payload = await LN.requestBolt11FromLnurlPayService(amountSats);
|
||||||
|
await fromWallet.payInvoice(bolt11payload.pr);
|
||||||
|
const decoded = fromWallet.decodeInvoice(bolt11payload.pr);
|
||||||
|
this.setState({ payButtonDisabled: false });
|
||||||
|
|
||||||
|
// success, probably
|
||||||
|
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
|
||||||
|
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
|
||||||
|
if (fromWallet.last_paid_invoice_result && fromWallet.last_paid_invoice_result.payment_preimage) {
|
||||||
|
await LN.storeSuccess(decoded.payment_hash, fromWallet.last_paid_invoice_result.payment_preimage);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.navigation.navigate('ScanLndInvoiceRoot', {
|
||||||
|
screen: 'LnurlPaySuccess',
|
||||||
|
params: {
|
||||||
|
paymentHash: decoded.payment_hash,
|
||||||
|
justPaid: true,
|
||||||
|
fromWalletID: this.state.fromWalletID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (Err) {
|
||||||
|
console.log(Err.message);
|
||||||
|
this.setState({ isLoading: false, payButtonDisabled: false });
|
||||||
|
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
|
||||||
|
return alert(Err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWalletSelectionButton = () => {
|
||||||
|
if (this.state.renderWalletSelectionButtonHidden) return;
|
||||||
|
return (
|
||||||
|
<View style={styles.walletSelectRoot}>
|
||||||
|
{!this.state.isLoading && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.walletSelectTouch}
|
||||||
|
onPress={() =>
|
||||||
|
this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text style={styles.walletSelectText}>{loc.wallets.select_wallet.toLowerCase()}</Text>
|
||||||
|
<Icon name="angle-right" size={18} type="font-awesome" color="#9aa0aa" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
<View style={styles.walletWrap}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.walletWrapTouch}
|
||||||
|
onPress={() =>
|
||||||
|
this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text style={styles.walletWrapLabel}>{this.state.fromWallet.getLabel()}</Text>
|
||||||
|
<Text style={styles.walletWrapBalance}>
|
||||||
|
{formatBalanceWithoutSuffix(this.state.fromWallet.getBalance(), BitcoinUnit.SATS, false)}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.walletWrapSats}>{BitcoinUnit.SATS}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderGotPayload() {
|
||||||
|
return (
|
||||||
|
<SafeBlueArea>
|
||||||
|
<ScrollView>
|
||||||
|
<BlueCard>
|
||||||
|
<BlueBitcoinAmount
|
||||||
|
isLoading={this.state.isLoading}
|
||||||
|
amount={this.state.amount.toString()}
|
||||||
|
onAmountUnitChange={unit => {
|
||||||
|
this.setState({ unit });
|
||||||
|
}}
|
||||||
|
onChangeText={text => {
|
||||||
|
this.setState({ amount: text });
|
||||||
|
}}
|
||||||
|
disabled={this.state.payload && this.state.payload.fixed}
|
||||||
|
unit={this.state.unit}
|
||||||
|
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||||
|
/>
|
||||||
|
<BlueText style={styles.alignSelfCenter}>
|
||||||
|
please pay between {this.state.payload.min} and {this.state.payload.max} sat
|
||||||
|
</BlueText>
|
||||||
|
<BlueSpacing20 />
|
||||||
|
{this.state.payload.image && <Image style={styles.img} source={{ uri: this.state.payload.image }} />}
|
||||||
|
<BlueText style={styles.alignSelfCenter}>{this.state.payload.description}</BlueText>
|
||||||
|
<BlueText style={styles.alignSelfCenter}>{this.state.payload.domain}</BlueText>
|
||||||
|
<BlueSpacing20 />
|
||||||
|
<BlueButton title={loc.lnd.payButton} onPress={this.pay} disabled={this.state.payButtonDisabled} />
|
||||||
|
<BlueSpacing20 />
|
||||||
|
{this.renderWalletSelectionButton()}
|
||||||
|
</BlueCard>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeBlueArea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.isLoading) {
|
||||||
|
return <BlueLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.renderGotPayload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LnurlPay.propTypes = {
|
||||||
|
route: PropTypes.shape({
|
||||||
|
params: PropTypes.shape({
|
||||||
|
fromWalletID: PropTypes.string.isRequired,
|
||||||
|
lnurl: PropTypes.string.isRequired,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
navigation: PropTypes.shape({
|
||||||
|
navigate: PropTypes.func,
|
||||||
|
pop: PropTypes.func,
|
||||||
|
dangerouslyGetParent: PropTypes.func,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
img: { width: 200, height: 200, alignSelf: 'center' },
|
||||||
|
alignSelfCenter: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
walletSelectRoot: {
|
||||||
|
marginBottom: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
walletSelectTouch: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
walletSelectText: {
|
||||||
|
color: '#9aa0aa',
|
||||||
|
fontSize: 14,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
walletWrap: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginVertical: 4,
|
||||||
|
},
|
||||||
|
walletWrapTouch: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
walletWrapLabel: {
|
||||||
|
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
walletWrapBalance: {
|
||||||
|
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginLeft: 4,
|
||||||
|
marginRight: 4,
|
||||||
|
},
|
||||||
|
walletWrapSats: {
|
||||||
|
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '600',
|
||||||
|
textAlignVertical: 'bottom',
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
LnurlPay.navigationOptions = ({ navigation, route }) => {
|
||||||
|
return {
|
||||||
|
...BlueNavigationStyle(navigation, true, () => navigation.dangerouslyGetParent().popToTop()),
|
||||||
|
title: '',
|
||||||
|
headerLeft: null,
|
||||||
|
};
|
||||||
|
};
|
210
screen/lnd/lnurlPaySuccess.js
Normal file
210
screen/lnd/lnurlPaySuccess.js
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import LottieView from 'lottie-react-native';
|
||||||
|
import { View, Text, Linking, StyleSheet, Image, ScrollView } from 'react-native';
|
||||||
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
|
import { Icon } from 'react-native-elements';
|
||||||
|
import {
|
||||||
|
BlueButton,
|
||||||
|
BlueButtonLink,
|
||||||
|
BlueNavigationStyle,
|
||||||
|
SafeBlueArea,
|
||||||
|
BlueCard,
|
||||||
|
BlueLoading,
|
||||||
|
BlueText,
|
||||||
|
BlueSpacing20,
|
||||||
|
} from '../../BlueComponents';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Lnurl from '../../class/lnurl';
|
||||||
|
import loc from '../../loc';
|
||||||
|
|
||||||
|
export default class LnurlPaySuccess extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const paymentHash = props.route.params.paymentHash;
|
||||||
|
const fromWalletID = props.route.params.fromWalletID;
|
||||||
|
const justPaid = !!props.route.params.justPaid;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
paymentHash,
|
||||||
|
isLoading: true,
|
||||||
|
fromWalletID,
|
||||||
|
justPaid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const LN = new Lnurl(false, AsyncStorage);
|
||||||
|
await LN.loadSuccessfulPayment(this.state.paymentHash);
|
||||||
|
|
||||||
|
const successAction = LN.getSuccessAction();
|
||||||
|
if (!successAction) {
|
||||||
|
this.setState({ isLoading: false, LN });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = { LN, isLoading: false };
|
||||||
|
|
||||||
|
switch (successAction.tag) {
|
||||||
|
case 'aes': {
|
||||||
|
const preimage = LN.getPreimage();
|
||||||
|
newState.message = Lnurl.decipherAES(successAction.ciphertext, preimage, successAction.iv);
|
||||||
|
newState.preamble = successAction.description;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'url':
|
||||||
|
newState.url = successAction.url;
|
||||||
|
newState.preamble = successAction.description;
|
||||||
|
break;
|
||||||
|
case 'message':
|
||||||
|
this.setState({ message: successAction.message });
|
||||||
|
newState.message = successAction.message;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.isLoading || !this.state.LN) {
|
||||||
|
return <BlueLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Lnurl} */
|
||||||
|
const LN = this.state.LN;
|
||||||
|
const domain = LN.getDomain();
|
||||||
|
const repeatable = !LN.getDisposable();
|
||||||
|
const lnurl = LN.getLnurl();
|
||||||
|
const description = LN.getDescription();
|
||||||
|
const image = LN.getImage();
|
||||||
|
const { preamble, message, url, justPaid } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeBlueArea style={styles.root}>
|
||||||
|
<ScrollView>
|
||||||
|
{justPaid ? (
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<LottieView style={styles.icon} source={require('../../img/bluenice.json')} autoPlay loop={false} />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<Icon name="check" size={50} type="font-awesome" color="#0f5cc0" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<BlueSpacing20 />
|
||||||
|
<BlueText style={styles.alignSelfCenter}>{domain}</BlueText>
|
||||||
|
<BlueText style={styles.alignSelfCenter}>{description}</BlueText>
|
||||||
|
{image && <Image style={styles.img} source={{ uri: image }} />}
|
||||||
|
<BlueSpacing20 />
|
||||||
|
|
||||||
|
{(preamble || url || message) && (
|
||||||
|
<BlueCard>
|
||||||
|
<View style={styles.successContainer}>
|
||||||
|
<Text style={styles.successText}>{preamble}</Text>
|
||||||
|
{url ? (
|
||||||
|
<BlueButtonLink
|
||||||
|
title={url}
|
||||||
|
onPress={() => {
|
||||||
|
Linking.openURL(url);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text selectable style={{ ...styles.successText, ...styles.successValue }}>
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</BlueCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<BlueCard>
|
||||||
|
{repeatable ? (
|
||||||
|
<BlueButton
|
||||||
|
onPress={() => {
|
||||||
|
this.props.navigation.navigate('ScanLndInvoiceRoot', {
|
||||||
|
screen: 'LnurlPay',
|
||||||
|
params: {
|
||||||
|
lnurl: lnurl,
|
||||||
|
fromWalletID: this.state.fromWalletID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
title="repeat"
|
||||||
|
icon={{ name: 'refresh', type: 'font-awesome', color: '#9aa0aa' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<BlueButton
|
||||||
|
onPress={() => {
|
||||||
|
this.props.navigation.dangerouslyGetParent().popToTop();
|
||||||
|
}}
|
||||||
|
title={loc.send.success_done}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</BlueCard>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeBlueArea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LnurlPaySuccess.propTypes = {
|
||||||
|
navigation: PropTypes.shape({
|
||||||
|
navigate: PropTypes.func,
|
||||||
|
pop: PropTypes.func,
|
||||||
|
dangerouslyGetParent: PropTypes.func,
|
||||||
|
}),
|
||||||
|
route: PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
params: PropTypes.shape({
|
||||||
|
paymentHash: PropTypes.string.isRequired,
|
||||||
|
fromWalletID: PropTypes.string.isRequired,
|
||||||
|
justPaid: PropTypes.bool.isRequired,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
img: { width: 200, height: 200, alignSelf: 'center' },
|
||||||
|
alignSelfCenter: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: 0,
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
backgroundColor: '#ccddf9',
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
maxWidth: 120,
|
||||||
|
maxHeight: 120,
|
||||||
|
padding: 0,
|
||||||
|
borderRadius: 60,
|
||||||
|
alignSelf: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 400,
|
||||||
|
height: 400,
|
||||||
|
},
|
||||||
|
successContainer: {
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
successText: {
|
||||||
|
textAlign: 'center',
|
||||||
|
margin: 4,
|
||||||
|
},
|
||||||
|
successValue: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
LnurlPaySuccess.navigationOptions = ({ navigation, route }) => {
|
||||||
|
return {
|
||||||
|
...BlueNavigationStyle(navigation, true, () => navigation.dangerouslyGetParent().popToTop()),
|
||||||
|
title: '',
|
||||||
|
headerLeft: null,
|
||||||
|
};
|
||||||
|
};
|
@ -23,6 +23,7 @@ import {
|
|||||||
BlueLoading,
|
BlueLoading,
|
||||||
} from '../../BlueComponents';
|
} from '../../BlueComponents';
|
||||||
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
|
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
|
||||||
|
import Lnurl from '../../class/lnurl';
|
||||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||||
import { Icon } from 'react-native-elements';
|
import { Icon } from 'react-native-elements';
|
||||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
@ -213,9 +214,20 @@ export default class ScanLndInvoice extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
processInvoice = data => {
|
processInvoice = data => {
|
||||||
|
if (Lnurl.isLnurl(data)) return this.processLnurlPay(data);
|
||||||
this.props.navigation.setParams({ uri: data });
|
this.props.navigation.setParams({ uri: data });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
processLnurlPay = data => {
|
||||||
|
this.props.navigation.navigate('ScanLndInvoiceRoot', {
|
||||||
|
screen: 'LnurlPay',
|
||||||
|
params: {
|
||||||
|
lnurl: data,
|
||||||
|
fromWalletID: this.state.fromWallet.getID(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
async pay() {
|
async pay() {
|
||||||
if (!('decoded' in this.state)) {
|
if (!('decoded' in this.state)) {
|
||||||
return null;
|
return null;
|
||||||
@ -286,7 +298,7 @@ export default class ScanLndInvoice extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processTextForInvoice = text => {
|
processTextForInvoice = text => {
|
||||||
if (text.toLowerCase().startsWith('lnb') || text.toLowerCase().startsWith('lightning:lnb')) {
|
if (text.toLowerCase().startsWith('lnb') || text.toLowerCase().startsWith('lightning:lnb') || Lnurl.isLnurl(text)) {
|
||||||
this.processInvoice(text);
|
this.processInvoice(text);
|
||||||
} else {
|
} else {
|
||||||
this.setState({ decoded: undefined, expiresIn: undefined, destination: text });
|
this.setState({ decoded: undefined, expiresIn: undefined, destination: text });
|
||||||
|
89
tests/unit/lnurl.test.js
Normal file
89
tests/unit/lnurl.test.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/* global describe, it */
|
||||||
|
import Lnurl from '../../class/lnurl';
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
describe('LNURL', function () {
|
||||||
|
it('can findlnurl', () => {
|
||||||
|
const lnurlExample = 'LNURL1DP68GURN8GHJ7MRWW3UXYMM59E3XJEMNW4HZU7RE0GHKCMN4WFKZ7URP0YLH2UM9WF5KG0FHXYCNV9G9W58';
|
||||||
|
const found = Lnurl.findlnurl(lnurlExample);
|
||||||
|
assert.strictEqual(found, 'lnurl1dp68gurn8ghj7mrww3uxymm59e3xjemnw4hzu7re0ghkcmn4wfkz7urp0ylh2um9wf5kg0fhxycnv9g9w58');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can getUrlFromLnurl()', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
Lnurl.getUrlFromLnurl('LNURL1DP68GURN8GHJ7MRWW3UXYMM59E3XJEMNW4HZU7RE0GHKCMN4WFKZ7URP0YLH2UM9WF5KG0FHXYCNV9G9W58'),
|
||||||
|
'https://lntxbot.bigsun.xyz/lnurl/pay?userid=7116',
|
||||||
|
);
|
||||||
|
assert.strictEqual(Lnurl.getUrlFromLnurl('bs'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can isLnurl()', () => {
|
||||||
|
assert.ok(Lnurl.isLnurl('LNURL1DP68GURN8GHJ7MRWW3UXYMM59E3XJEMNW4HZU7RE0GHKCMN4WFKZ7URP0YLH2UM9WF5KG0FHXYCNV9G9W58'));
|
||||||
|
assert.ok(!Lnurl.isLnurl('bs'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can callLnurlPayService() and requestBolt11FromLnurlPayService()', async () => {
|
||||||
|
const LN = new Lnurl('LNURL1DP68GURN8GHJ7MRWW3UXYMM59E3XJEMNW4HZU7RE0GHKCMN4WFKZ7URP0YLH2UM9WF5KG0FHXYCNV9G9W58');
|
||||||
|
|
||||||
|
// poor-man's mock:
|
||||||
|
LN._fetchGet = LN.fetchGet;
|
||||||
|
LN.fetchGet = () => {
|
||||||
|
return {
|
||||||
|
status: 'OK',
|
||||||
|
callback: 'https://lntxbot.bigsun.xyz/lnurl/pay/callback?userid=7116',
|
||||||
|
tag: 'payRequest',
|
||||||
|
maxSendable: 1000000000,
|
||||||
|
minSendable: 1000,
|
||||||
|
metadata: '[["text/plain","Fund @overtorment account on t.me/lntxbot."]]',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const lnurlpayPayload = await LN.callLnurlPayService();
|
||||||
|
assert.deepStrictEqual(lnurlpayPayload, {
|
||||||
|
amount: 1,
|
||||||
|
callback: 'https://lntxbot.bigsun.xyz/lnurl/pay/callback?userid=7116',
|
||||||
|
description: 'Fund @overtorment account on t.me/lntxbot.',
|
||||||
|
domain: 'lntxbot.bigsun.xyz',
|
||||||
|
fixed: false,
|
||||||
|
image: undefined,
|
||||||
|
max: 1000000,
|
||||||
|
metadata: '[["text/plain","Fund @overtorment account on t.me/lntxbot."]]',
|
||||||
|
min: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// mock:
|
||||||
|
LN.fetchGet = () => {
|
||||||
|
return {
|
||||||
|
status: 'OK',
|
||||||
|
successAction: null,
|
||||||
|
routes: [],
|
||||||
|
pr:
|
||||||
|
'lnbc20n1p03s853pp58v9lrqahj2zyuzsdqqm3wnt2damlnkkuzwm8s7jkmnauhtkq4fjshp5z766racq95ncpk27nksev2ntu8wte77zd46g8uvzlnm5hhwukjrqcqzysxq9p5hsqrzjq29zewx4rezd04lpprpwsz5cesrfz30qtfkjqfw0249a3pn0uv5exzdefqqqxecqqqqqqqlgqqqq03sq9qsp52guktgy9u0xpky06n7slhjcvkassj0xpc3t9wadfsa0sl5x4fz9s9qy9qsqff5ycjg6xh3cc0vf8wxzxdajrdl9pka3nl3v37vcqj0qrdkzhsqxs8atfnxm2xenlkz7fpghlnuypux7hdp63zct3fr9px2e349kyqspu3gswx',
|
||||||
|
disposable: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const rez = await LN.requestBolt11FromLnurlPayService(2);
|
||||||
|
assert.deepStrictEqual(rez, {
|
||||||
|
status: 'OK',
|
||||||
|
successAction: null,
|
||||||
|
routes: [],
|
||||||
|
pr:
|
||||||
|
'lnbc20n1p03s853pp58v9lrqahj2zyuzsdqqm3wnt2damlnkkuzwm8s7jkmnauhtkq4fjshp5z766racq95ncpk27nksev2ntu8wte77zd46g8uvzlnm5hhwukjrqcqzysxq9p5hsqrzjq29zewx4rezd04lpprpwsz5cesrfz30qtfkjqfw0249a3pn0uv5exzdefqqqxecqqqqqqqlgqqqq03sq9qsp52guktgy9u0xpky06n7slhjcvkassj0xpc3t9wadfsa0sl5x4fz9s9qy9qsqff5ycjg6xh3cc0vf8wxzxdajrdl9pka3nl3v37vcqj0qrdkzhsqxs8atfnxm2xenlkz7fpghlnuypux7hdp63zct3fr9px2e349kyqspu3gswx',
|
||||||
|
disposable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(LN.getSuccessAction(), null);
|
||||||
|
assert.strictEqual(LN.getDomain(), 'lntxbot.bigsun.xyz');
|
||||||
|
assert.strictEqual(LN.getDescription(), 'Fund @overtorment account on t.me/lntxbot.');
|
||||||
|
assert.strictEqual(LN.getImage(), undefined);
|
||||||
|
assert.strictEqual(LN.getLnurl(), 'LNURL1DP68GURN8GHJ7MRWW3UXYMM59E3XJEMNW4HZU7RE0GHKCMN4WFKZ7URP0YLH2UM9WF5KG0FHXYCNV9G9W58');
|
||||||
|
assert.strictEqual(LN.getDisposable(), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can decipher AES', () => {
|
||||||
|
const ciphertext = 'vCWn4TMhIKubUc5+aBVfvw==';
|
||||||
|
const iv = 'eTGduB45hWTOxHj1dR+LJw==';
|
||||||
|
const preimage = 'bf62911aa53c017c27ba34391f694bc8bf8aaf59b4ebfd9020e66ac0412e189b';
|
||||||
|
|
||||||
|
assert.strictEqual(Lnurl.decipherAES(ciphertext, preimage, iv), '1234');
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user