mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 18:00:17 +01:00
Merge pull request #7033 from BlueWallet/ref-lnurlpay-to-ts
Ref lnurlpay to ts
This commit is contained in:
commit
947d612944
@ -3,11 +3,51 @@ import bolt11 from 'bolt11';
|
||||
import createHash from 'create-hash';
|
||||
import { createHmac } from 'crypto';
|
||||
import CryptoJS from 'crypto-js';
|
||||
// @ts-ignore theres no types for secp256k1
|
||||
import secp256k1 from 'secp256k1';
|
||||
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api
|
||||
|
||||
const ONION_REGEX = /^(http:\/\/[^/:@]+\.onion(?::\d{1,5})?)(\/.*)?$/; // regex for onion URL
|
||||
|
||||
interface LnurlPayServicePayload {
|
||||
callback: string;
|
||||
fixed: boolean;
|
||||
min: number;
|
||||
max: number;
|
||||
domain: string;
|
||||
metadata: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
amount: number;
|
||||
commentAllowed?: number;
|
||||
}
|
||||
|
||||
interface LnurlPayServiceBolt11Payload {
|
||||
pr: string;
|
||||
successAction?: any;
|
||||
disposable?: boolean;
|
||||
tag: string;
|
||||
metadata: any;
|
||||
minSendable: number;
|
||||
maxSendable: number;
|
||||
callback: string;
|
||||
commentAllowed: number;
|
||||
}
|
||||
|
||||
interface DecodedInvoice {
|
||||
destination: string;
|
||||
num_satoshis: string;
|
||||
num_millisatoshis: string;
|
||||
timestamp: string;
|
||||
fallback_addr: string;
|
||||
route_hints: any[];
|
||||
payment_hash?: string;
|
||||
description_hash?: string;
|
||||
cltv_expiry?: string;
|
||||
expiry?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/btcontract/lnurl-rfc/blob/master/lnurl-pay.md
|
||||
*/
|
||||
@ -16,15 +56,21 @@ export default class Lnurl {
|
||||
static TAG_WITHDRAW_REQUEST = 'withdrawRequest'; // type of LNURL
|
||||
static TAG_LOGIN_REQUEST = 'login'; // type of LNURL
|
||||
|
||||
constructor(url, AsyncStorage) {
|
||||
this._lnurl = url;
|
||||
private _lnurl: string;
|
||||
private _lnurlPayServiceBolt11Payload: LnurlPayServiceBolt11Payload | false;
|
||||
private _lnurlPayServicePayload: LnurlPayServicePayload | false;
|
||||
private _AsyncStorage: any;
|
||||
private _preimage: string | false;
|
||||
|
||||
constructor(url: string | false, AsyncStorage?: any) {
|
||||
this._lnurl = url || '';
|
||||
this._lnurlPayServiceBolt11Payload = false;
|
||||
this._lnurlPayServicePayload = false;
|
||||
this._AsyncStorage = AsyncStorage;
|
||||
this._preimage = false;
|
||||
}
|
||||
|
||||
static findlnurl(bodyOfText) {
|
||||
static findlnurl(bodyOfText: string): string | null {
|
||||
const res = /^(?:http.*[&?]lightning=|lightning:)?(lnurl1[02-9ac-hj-np-z]+)/.exec(bodyOfText.toLowerCase());
|
||||
if (res) {
|
||||
return res[1];
|
||||
@ -32,7 +78,7 @@ export default class Lnurl {
|
||||
return null;
|
||||
}
|
||||
|
||||
static getUrlFromLnurl(lnurlExample) {
|
||||
static getUrlFromLnurl(lnurlExample: string): string | false {
|
||||
const found = Lnurl.findlnurl(lnurlExample);
|
||||
if (!found) {
|
||||
if (Lnurl.isLightningAddress(lnurlExample)) {
|
||||
@ -49,22 +95,22 @@ export default class Lnurl {
|
||||
return Buffer.from(bech32.fromWords(decoded.words)).toString();
|
||||
}
|
||||
|
||||
static isLnurl(url) {
|
||||
static isLnurl(url: string): boolean {
|
||||
return Lnurl.findlnurl(url) !== null;
|
||||
}
|
||||
|
||||
static isOnionUrl(url) {
|
||||
static isOnionUrl(url: string): boolean {
|
||||
return Lnurl.parseOnionUrl(url) !== null;
|
||||
}
|
||||
|
||||
static parseOnionUrl(url) {
|
||||
static parseOnionUrl(url: string): [string, string] | null {
|
||||
const match = url.match(ONION_REGEX);
|
||||
if (match === null) return null;
|
||||
const [, baseURI, path] = match;
|
||||
return [baseURI, path];
|
||||
}
|
||||
|
||||
async fetchGet(url) {
|
||||
async fetchGet(url: string): Promise<any> {
|
||||
const resp = await fetch(url, { method: 'GET' });
|
||||
if (resp.status >= 300) {
|
||||
throw new Error('Bad response from server');
|
||||
@ -76,14 +122,14 @@ export default class Lnurl {
|
||||
return reply;
|
||||
}
|
||||
|
||||
decodeInvoice(invoice) {
|
||||
decodeInvoice(invoice: string): DecodedInvoice {
|
||||
const { payeeNodeKey, tags, satoshis, millisatoshis, timestamp } = bolt11.decode(invoice);
|
||||
|
||||
const decoded = {
|
||||
destination: payeeNodeKey,
|
||||
const decoded: DecodedInvoice = {
|
||||
destination: payeeNodeKey ?? '',
|
||||
num_satoshis: satoshis ? satoshis.toString() : '0',
|
||||
num_millisatoshis: millisatoshis ? millisatoshis.toString() : '0',
|
||||
timestamp: timestamp.toString(),
|
||||
timestamp: timestamp?.toString() ?? '',
|
||||
fallback_addr: '',
|
||||
route_hints: [],
|
||||
};
|
||||
@ -92,10 +138,10 @@ export default class Lnurl {
|
||||
const { tagName, data } = tags[i];
|
||||
switch (tagName) {
|
||||
case 'payment_hash':
|
||||
decoded.payment_hash = data;
|
||||
decoded.payment_hash = String(data);
|
||||
break;
|
||||
case 'purpose_commit_hash':
|
||||
decoded.description_hash = data;
|
||||
decoded.description_hash = String(data);
|
||||
break;
|
||||
case 'min_final_cltv_expiry':
|
||||
decoded.cltv_expiry = data.toString();
|
||||
@ -104,21 +150,21 @@ export default class Lnurl {
|
||||
decoded.expiry = data.toString();
|
||||
break;
|
||||
case 'description':
|
||||
decoded.description = data;
|
||||
decoded.description = String(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!decoded.expiry) decoded.expiry = '3600'; // default
|
||||
|
||||
if (parseInt(decoded.num_satoshis, 10) === 0 && decoded.num_millisatoshis > 0) {
|
||||
decoded.num_satoshis = (decoded.num_millisatoshis / 1000).toString();
|
||||
if (parseInt(decoded.num_satoshis, 10) === 0 && parseInt(decoded.num_millisatoshis, 10) > 0) {
|
||||
decoded.num_satoshis = (parseInt(decoded.num_millisatoshis, 10) / 1000).toString();
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
async requestBolt11FromLnurlPayService(amountSat, comment = '') {
|
||||
async requestBolt11FromLnurlPayService(amountSat: number, comment: string = ''): Promise<LnurlPayServiceBolt11Payload> {
|
||||
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)
|
||||
@ -132,15 +178,13 @@ export default class Lnurl {
|
||||
);
|
||||
const nonce = Math.floor(Math.random() * 2e16).toString(16);
|
||||
const separator = this._lnurlPayServicePayload.callback.indexOf('?') === -1 ? '?' : '&';
|
||||
if (this.getCommentAllowed() && comment && comment.length > this.getCommentAllowed()) {
|
||||
comment = comment.substr(0, this.getCommentAllowed());
|
||||
if (this.getCommentAllowed() && comment && comment.length > (this.getCommentAllowed() as number)) {
|
||||
comment = comment.substr(0, this.getCommentAllowed() as number);
|
||||
}
|
||||
if (comment) comment = `&comment=${encodeURIComponent(comment)}`;
|
||||
const urlToFetch =
|
||||
this._lnurlPayServicePayload.callback + separator + 'amount=' + Math.floor(amountSat * 1000) + '&nonce=' + nonce + comment;
|
||||
this._lnurlPayServiceBolt11Payload = await this.fetchGet(urlToFetch);
|
||||
if (this._lnurlPayServiceBolt11Payload.status === 'ERROR')
|
||||
throw new Error(this._lnurlPayServiceBolt11Payload.reason || 'requestBolt11FromLnurlPayService() error');
|
||||
this._lnurlPayServiceBolt11Payload = (await this.fetchGet(urlToFetch)) as LnurlPayServiceBolt11Payload;
|
||||
|
||||
// check pr description_hash, amount etc:
|
||||
const decoded = this.decodeInvoice(this._lnurlPayServiceBolt11Payload.pr);
|
||||
@ -155,11 +199,12 @@ export default class Lnurl {
|
||||
return this._lnurlPayServiceBolt11Payload;
|
||||
}
|
||||
|
||||
async callLnurlPayService() {
|
||||
async callLnurlPayService(): Promise<LnurlPayServicePayload> {
|
||||
if (!this._lnurl) throw new Error('this._lnurl is not set');
|
||||
const url = Lnurl.getUrlFromLnurl(this._lnurl);
|
||||
if (!url) throw new Error('Invalid LNURL');
|
||||
// calling the url
|
||||
const reply = await this.fetchGet(url);
|
||||
const reply = (await this.fetchGet(url)) as LnurlPayServiceBolt11Payload;
|
||||
|
||||
if (reply.tag !== Lnurl.TAG_PAY_REQUEST) {
|
||||
throw new Error('lnurl-pay expected, found tag ' + reply.tag);
|
||||
@ -168,8 +213,8 @@ export default class Lnurl {
|
||||
const data = reply;
|
||||
|
||||
// parse metadata and extract things from it
|
||||
let image;
|
||||
let description;
|
||||
let image: string | undefined;
|
||||
let description: string | undefined;
|
||||
const kvs = JSON.parse(data.metadata);
|
||||
for (let i = 0; i < kvs.length; i++) {
|
||||
const [k, v] = kvs[i];
|
||||
@ -185,14 +230,15 @@ export default class Lnurl {
|
||||
}
|
||||
|
||||
// setting the payment screen with the parameters
|
||||
const min = Math.ceil((data.minSendable || 0) / 1000);
|
||||
const max = Math.floor(data.maxSendable / 1000);
|
||||
const min = Math.ceil((data.minSendable ?? 0) / 1000);
|
||||
const max = Math.floor((data.maxSendable ?? 0) / 1000);
|
||||
|
||||
this._lnurlPayServicePayload = {
|
||||
callback: data.callback,
|
||||
fixed: min === max,
|
||||
min,
|
||||
max,
|
||||
// @ts-ignore idk
|
||||
domain: data.callback.match(/^(https|http):\/\/([^/]+)\//)[2],
|
||||
metadata: data.metadata,
|
||||
description,
|
||||
@ -204,7 +250,7 @@ export default class Lnurl {
|
||||
return this._lnurlPayServicePayload;
|
||||
}
|
||||
|
||||
async loadSuccessfulPayment(paymentHash) {
|
||||
async loadSuccessfulPayment(paymentHash: string): Promise<boolean> {
|
||||
if (!paymentHash) throw new Error('No paymentHash provided');
|
||||
let data;
|
||||
try {
|
||||
@ -224,7 +270,7 @@ export default class Lnurl {
|
||||
return true;
|
||||
}
|
||||
|
||||
async storeSuccess(paymentHash, preimage) {
|
||||
async storeSuccess(paymentHash: string, preimage: string | { data: Buffer }): Promise<void> {
|
||||
if (typeof preimage === 'object') {
|
||||
preimage = Buffer.from(preimage.data).toString('hex');
|
||||
}
|
||||
@ -241,35 +287,39 @@ export default class Lnurl {
|
||||
);
|
||||
}
|
||||
|
||||
getSuccessAction() {
|
||||
return this._lnurlPayServiceBolt11Payload.successAction;
|
||||
getSuccessAction(): any | undefined {
|
||||
return this._lnurlPayServiceBolt11Payload && 'successAction' in this._lnurlPayServiceBolt11Payload
|
||||
? this._lnurlPayServiceBolt11Payload.successAction
|
||||
: undefined;
|
||||
}
|
||||
|
||||
getDomain() {
|
||||
return this._lnurlPayServicePayload.domain;
|
||||
getDomain(): string | undefined {
|
||||
return this._lnurlPayServicePayload ? this._lnurlPayServicePayload.domain : undefined;
|
||||
}
|
||||
|
||||
getDescription() {
|
||||
return this._lnurlPayServicePayload.description;
|
||||
getDescription(): string | undefined {
|
||||
return this._lnurlPayServicePayload ? this._lnurlPayServicePayload.description : undefined;
|
||||
}
|
||||
|
||||
getImage() {
|
||||
return this._lnurlPayServicePayload.image;
|
||||
getImage(): string | undefined {
|
||||
return this._lnurlPayServicePayload ? this._lnurlPayServicePayload.image : undefined;
|
||||
}
|
||||
|
||||
getLnurl() {
|
||||
getLnurl(): string {
|
||||
return this._lnurl;
|
||||
}
|
||||
|
||||
getDisposable() {
|
||||
return this._lnurlPayServiceBolt11Payload.disposable;
|
||||
getDisposable(): boolean | undefined {
|
||||
return this._lnurlPayServiceBolt11Payload && 'disposable' in this._lnurlPayServiceBolt11Payload
|
||||
? this._lnurlPayServiceBolt11Payload.disposable
|
||||
: undefined;
|
||||
}
|
||||
|
||||
getPreimage() {
|
||||
getPreimage(): string | false {
|
||||
return this._preimage;
|
||||
}
|
||||
|
||||
static decipherAES(ciphertextBase64, preimageHex, ivBase64) {
|
||||
static decipherAES(ciphertextBase64: string, preimageHex: string, ivBase64: string): string {
|
||||
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, {
|
||||
@ -279,27 +329,30 @@ export default class Lnurl {
|
||||
}).toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
|
||||
getCommentAllowed() {
|
||||
return this?._lnurlPayServicePayload?.commentAllowed ? parseInt(this._lnurlPayServicePayload.commentAllowed, 10) : false;
|
||||
getCommentAllowed(): number | false {
|
||||
if (!this._lnurlPayServicePayload) return false;
|
||||
return this._lnurlPayServicePayload.commentAllowed ? parseInt(this._lnurlPayServicePayload.commentAllowed.toString(), 10) : false;
|
||||
}
|
||||
|
||||
getMin() {
|
||||
return this?._lnurlPayServicePayload?.min ? parseInt(this._lnurlPayServicePayload.min, 10) : false;
|
||||
getMin(): number | false {
|
||||
if (!this._lnurlPayServicePayload) return false;
|
||||
return this._lnurlPayServicePayload.min ? parseInt(this._lnurlPayServicePayload.min.toString(), 10) : false;
|
||||
}
|
||||
|
||||
getMax() {
|
||||
return this?._lnurlPayServicePayload?.max ? parseInt(this._lnurlPayServicePayload.max, 10) : false;
|
||||
getMax(): number | false {
|
||||
if (!this._lnurlPayServicePayload) return false;
|
||||
return this._lnurlPayServicePayload.max ? parseInt(this._lnurlPayServicePayload.max.toString(), 10) : false;
|
||||
}
|
||||
|
||||
getAmount() {
|
||||
getAmount(): number | false {
|
||||
return this.getMin();
|
||||
}
|
||||
|
||||
authenticate(secret) {
|
||||
authenticate(secret: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._lnurl) throw new Error('this._lnurl is not set');
|
||||
|
||||
const url = parse(Lnurl.getUrlFromLnurl(this._lnurl), true);
|
||||
const url = parse(Lnurl.getUrlFromLnurl(this._lnurl) || '', true);
|
||||
|
||||
const hmac = createHmac('sha256', secret);
|
||||
hmac.on('readable', async () => {
|
||||
@ -308,7 +361,7 @@ export default class Lnurl {
|
||||
if (!privateKey) return;
|
||||
const privateKeyBuf = Buffer.from(privateKey, 'hex');
|
||||
const publicKey = secp256k1.publicKeyCreate(privateKeyBuf);
|
||||
const signatureObj = secp256k1.sign(Buffer.from(url.query.k1, 'hex'), privateKeyBuf);
|
||||
const signatureObj = secp256k1.sign(Buffer.from(url.query.k1 as string, 'hex'), privateKeyBuf);
|
||||
const derSignature = secp256k1.signatureExport(signatureObj.signature);
|
||||
|
||||
const reply = await this.fetchGet(`${url.href}&sig=${derSignature.toString('hex')}&key=${publicKey.toString('hex')}`);
|
||||
@ -326,7 +379,7 @@ export default class Lnurl {
|
||||
});
|
||||
}
|
||||
|
||||
static isLightningAddress(address) {
|
||||
static isLightningAddress(address: string) {
|
||||
// ensure only 1 `@` present:
|
||||
if (address.split('@').length !== 2) return false;
|
||||
const splitted = address.split('@');
|
@ -218,7 +218,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
<TouchableOpacity style={styles.walletPreferredUnitView} onPress={changeWalletBalanceUnit}>
|
||||
<Text style={styles.walletPreferredUnitText}>
|
||||
{wallet.getPreferredBalanceUnit() === BitcoinUnit.LOCAL_CURRENCY
|
||||
? (preferredFiatCurrency?.endPointKey ?? FiatUnit.USD)
|
||||
? preferredFiatCurrency?.endPointKey ?? FiatUnit.USD
|
||||
: wallet.getPreferredBalanceUnit()}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
@ -248,7 +248,7 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
isLargeScreen || !horizontal ? [iStyles.rootLargeDevice, customStyle] : (customStyle ?? { ...iStyles.root, width: itemWidth }),
|
||||
isLargeScreen || !horizontal ? [iStyles.rootLargeDevice, customStyle] : customStyle ?? { ...iStyles.root, width: itemWidth },
|
||||
{ opacity, transform: [{ scale: scaleValue }] },
|
||||
]}
|
||||
>
|
||||
@ -374,27 +374,31 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
||||
|
||||
const flatListRef = useRef<FlatList<any>>(null);
|
||||
|
||||
useImperativeHandle(ref, (): any => {
|
||||
return {
|
||||
scrollToEnd: (params: { animated?: boolean | null | undefined } | undefined) => flatListRef.current?.scrollToEnd(params),
|
||||
scrollToIndex: (params: {
|
||||
animated?: boolean | null | undefined;
|
||||
index: number;
|
||||
viewOffset?: number | undefined;
|
||||
viewPosition?: number | undefined;
|
||||
}) => flatListRef.current?.scrollToIndex(params),
|
||||
scrollToItem: (params: {
|
||||
animated?: boolean | null | undefined;
|
||||
item: any;
|
||||
viewOffset?: number | undefined;
|
||||
viewPosition?: number | undefined;
|
||||
}) => flatListRef.current?.scrollToItem(params),
|
||||
scrollToOffset: (params: { animated?: boolean | null | undefined; offset: number }) => flatListRef.current?.scrollToOffset(params),
|
||||
recordInteraction: () => flatListRef.current?.recordInteraction(),
|
||||
flashScrollIndicators: () => flatListRef.current?.flashScrollIndicators(),
|
||||
getNativeScrollRef: () => flatListRef.current?.getNativeScrollRef(),
|
||||
};
|
||||
}, []);
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
(): any => {
|
||||
return {
|
||||
scrollToEnd: (params: { animated?: boolean | null | undefined } | undefined) => flatListRef.current?.scrollToEnd(params),
|
||||
scrollToIndex: (params: {
|
||||
animated?: boolean | null | undefined;
|
||||
index: number;
|
||||
viewOffset?: number | undefined;
|
||||
viewPosition?: number | undefined;
|
||||
}) => flatListRef.current?.scrollToIndex(params),
|
||||
scrollToItem: (params: {
|
||||
animated?: boolean | null | undefined;
|
||||
item: any;
|
||||
viewOffset?: number | undefined;
|
||||
viewPosition?: number | undefined;
|
||||
}) => flatListRef.current?.scrollToItem(params),
|
||||
scrollToOffset: (params: { animated?: boolean | null | undefined; offset: number }) => flatListRef.current?.scrollToOffset(params),
|
||||
recordInteraction: () => flatListRef.current?.recordInteraction(),
|
||||
flashScrollIndicators: () => flatListRef.current?.flashScrollIndicators(),
|
||||
getNativeScrollRef: () => flatListRef.current?.getNativeScrollRef(),
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onScrollToIndexFailed = (error: { averageItemLength: number; index: number }): void => {
|
||||
console.debug('onScrollToIndexFailed');
|
||||
|
@ -1,5 +1,5 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useRoute } from '@react-navigation/native';
|
||||
import { RouteProp, useRoute } from '@react-navigation/native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { I18nManager, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Icon } from '@rneui/themed';
|
||||
@ -13,38 +13,40 @@ import Button from '../../components/Button';
|
||||
import SafeArea from '../../components/SafeArea';
|
||||
import { useTheme } from '../../components/themes';
|
||||
import prompt from '../../helpers/prompt';
|
||||
import { useBiometrics, unlockWithBiometrics } from '../../hooks/useBiometrics';
|
||||
import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
|
||||
import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc';
|
||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||
import { useStorage } from '../../hooks/context/useStorage';
|
||||
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
||||
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
|
||||
import { TWallet } from '../../class/wallets/types';
|
||||
import { pop } from '../../NavigationService';
|
||||
|
||||
/**
|
||||
* if user has default currency - fiat, attempting to pay will trigger conversion from entered in input field fiat value
|
||||
* to satoshi, and attempt to pay this satoshi value, which might be a little bit off from `min` & `max` values
|
||||
* provided by LnUrl. thats why we cache initial precise conversion rate so the reverse conversion wont be off.
|
||||
*/
|
||||
const _cacheFiatToSat = {};
|
||||
type RouteParams = {
|
||||
walletID: string;
|
||||
lnurl: string;
|
||||
};
|
||||
|
||||
const LnurlPay = () => {
|
||||
const _cacheFiatToSat: Record<string, string> = {};
|
||||
|
||||
const LnurlPay: React.FC = () => {
|
||||
const { wallets } = useStorage();
|
||||
const { isBiometricUseCapableAndEnabled } = useBiometrics();
|
||||
const { walletID, lnurl } = useRoute().params;
|
||||
/** @type {LightningCustodianWallet} */
|
||||
const wallet = wallets.find(w => w.getID() === walletID);
|
||||
const [unit, setUnit] = useState(wallet.getPreferredBalanceUnit());
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [_LN, setLN] = useState();
|
||||
const [payButtonDisabled, setPayButtonDisabled] = useState(true);
|
||||
const [payload, setPayload] = useState();
|
||||
const { setParams, pop, navigate } = useExtendedNavigation();
|
||||
const [amount, setAmount] = useState();
|
||||
const route = useRoute<RouteProp<Record<string, RouteParams>, string>>();
|
||||
const { walletID, lnurl } = route.params;
|
||||
const wallet = wallets.find(w => w.getID() === walletID) as LightningCustodianWallet;
|
||||
const [unit, setUnit] = useState<BitcoinUnit>(wallet?.getPreferredBalanceUnit() ?? BitcoinUnit.BTC);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [_LN, setLN] = useState<Lnurl | undefined>();
|
||||
const [payButtonDisabled, setPayButtonDisabled] = useState<boolean>(true);
|
||||
const [payload, setPayload] = useState<any>();
|
||||
const { setParams, navigate } = useExtendedNavigation();
|
||||
const [amount, setAmount] = useState<string | undefined>();
|
||||
const { colors } = useTheme();
|
||||
const stylesHook = StyleSheet.create({
|
||||
root: {
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
|
||||
walletWrapLabel: {
|
||||
color: colors.buttonAlternativeTextColor,
|
||||
},
|
||||
@ -68,18 +70,16 @@ const LnurlPay = () => {
|
||||
setLN(ln);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [lnurl, pop]);
|
||||
}, [lnurl]);
|
||||
|
||||
useEffect(() => {
|
||||
setPayButtonDisabled(isLoading);
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (payload) {
|
||||
/** @type {Lnurl} */
|
||||
const LN = _LN;
|
||||
let originalSatAmount;
|
||||
let newAmount = (originalSatAmount = LN.getMin());
|
||||
if (payload && _LN) {
|
||||
let originalSatAmount: number | false;
|
||||
let newAmount: number | boolean | string = (originalSatAmount = _LN.getMin());
|
||||
if (!newAmount) {
|
||||
presentAlert({ message: 'Internal error: incorrect LNURL amount' });
|
||||
return;
|
||||
@ -90,22 +90,21 @@ const LnurlPay = () => {
|
||||
break;
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
newAmount = satoshiToLocalCurrency(newAmount, false);
|
||||
_cacheFiatToSat[newAmount] = originalSatAmount;
|
||||
_cacheFiatToSat[newAmount] = String(originalSatAmount);
|
||||
break;
|
||||
}
|
||||
setAmount(newAmount);
|
||||
setAmount(newAmount.toString());
|
||||
}
|
||||
}, [payload]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [payload, _LN, unit]);
|
||||
|
||||
const onWalletSelect = w => {
|
||||
const onWalletSelect = (w: TWallet) => {
|
||||
setParams({ walletID: w.getID() });
|
||||
pop();
|
||||
};
|
||||
|
||||
const pay = async () => {
|
||||
setPayButtonDisabled(true);
|
||||
/** @type {Lnurl} */
|
||||
const LN = _LN;
|
||||
if (!_LN || !amount) return;
|
||||
|
||||
const isBiometricsEnabled = await isBiometricUseCapableAndEnabled();
|
||||
if (isBiometricsEnabled) {
|
||||
@ -114,39 +113,41 @@ const LnurlPay = () => {
|
||||
}
|
||||
}
|
||||
|
||||
let amountSats = amount;
|
||||
let amountSats: number | false;
|
||||
switch (unit) {
|
||||
case BitcoinUnit.SATS:
|
||||
amountSats = parseInt(amountSats, 10); // nop
|
||||
amountSats = parseInt(amount, 10);
|
||||
break;
|
||||
case BitcoinUnit.BTC:
|
||||
amountSats = btcToSatoshi(amountSats);
|
||||
amountSats = btcToSatoshi(amount);
|
||||
break;
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
if (_cacheFiatToSat[amount]) {
|
||||
amountSats = _cacheFiatToSat[amount];
|
||||
if (_cacheFiatToSat[String(amount)]) {
|
||||
amountSats = parseInt(_cacheFiatToSat[amount], 10);
|
||||
} else {
|
||||
amountSats = btcToSatoshi(fiatToBTC(amountSats));
|
||||
amountSats = btcToSatoshi(fiatToBTC(parseFloat(amount)));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown unit type');
|
||||
}
|
||||
|
||||
let bolt11payload;
|
||||
try {
|
||||
let comment;
|
||||
if (LN.getCommentAllowed()) {
|
||||
let comment: string | undefined;
|
||||
if (_LN.getCommentAllowed()) {
|
||||
comment = await prompt('Comment', '', false, 'plain-text');
|
||||
}
|
||||
|
||||
bolt11payload = await LN.requestBolt11FromLnurlPayService(amountSats, comment);
|
||||
const bolt11payload = await _LN.requestBolt11FromLnurlPayService(amountSats, comment);
|
||||
// @ts-ignore fixme after lnurl.js converted to ts
|
||||
await wallet.payInvoice(bolt11payload.pr);
|
||||
// @ts-ignore fixme after lnurl.js converted to ts
|
||||
const decoded = wallet.decodeInvoice(bolt11payload.pr);
|
||||
setPayButtonDisabled(false);
|
||||
|
||||
// success, probably
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
if (wallet.last_paid_invoice_result && wallet.last_paid_invoice_result.payment_preimage) {
|
||||
await LN.storeSuccess(decoded.payment_hash, wallet.last_paid_invoice_result.payment_preimage);
|
||||
await _LN.storeSuccess(decoded.payment_hash, wallet.last_paid_invoice_result.payment_preimage);
|
||||
}
|
||||
|
||||
navigate('ScanLndInvoiceRoot', {
|
||||
@ -158,12 +159,12 @@ const LnurlPay = () => {
|
||||
},
|
||||
});
|
||||
setIsLoading(false);
|
||||
} catch (Err) {
|
||||
console.log(Err.message);
|
||||
} catch (err) {
|
||||
console.log((err as Error).message);
|
||||
setIsLoading(false);
|
||||
setPayButtonDisabled(false);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
return presentAlert({ message: Err.message });
|
||||
return presentAlert({ message: (err as Error).message });
|
||||
}
|
||||
};
|
||||
|
||||
@ -198,15 +199,16 @@ const LnurlPay = () => {
|
||||
const renderGotPayload = () => {
|
||||
return (
|
||||
<SafeArea>
|
||||
<ScrollView contentContainertyle={{ justifyContent: 'space-around' }}>
|
||||
<ScrollView contentContainerStyle={styles.scrollviewContainer}>
|
||||
<BlueCard>
|
||||
<AmountInput
|
||||
isLoading={isLoading}
|
||||
amount={amount && amount.toString()}
|
||||
amount={amount}
|
||||
onAmountUnitChange={setUnit}
|
||||
onChangeText={setAmount}
|
||||
disabled={payload && payload.fixed}
|
||||
unit={unit}
|
||||
// @ts-ignore idk
|
||||
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||
/>
|
||||
<BlueText style={styles.alignSelfCenter}>
|
||||
@ -234,7 +236,7 @@ const LnurlPay = () => {
|
||||
);
|
||||
};
|
||||
|
||||
return isLoading || wallet === undefined || amount === undefined ? (
|
||||
return isLoading || !wallet || amount === undefined ? (
|
||||
<View style={[styles.root, stylesHook.root]}>
|
||||
<BlueLoading />
|
||||
</View>
|
||||
@ -246,6 +248,7 @@ const LnurlPay = () => {
|
||||
export default LnurlPay;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollviewContainer: { justifyContent: 'space-around' },
|
||||
img: { width: 200, height: 200, alignSelf: 'center' },
|
||||
alignSelfCenter: {
|
||||
alignSelf: 'center',
|
@ -43,6 +43,9 @@ const LnurlPaySuccess: React.FC = () => {
|
||||
switch (successAction.tag) {
|
||||
case 'aes': {
|
||||
const preimage = lnurl.getPreimage();
|
||||
if (!preimage) {
|
||||
break;
|
||||
}
|
||||
setMessage(Lnurl.decipherAES(successAction.ciphertext, preimage, successAction.iv));
|
||||
setPreamble(successAction.description);
|
||||
break;
|
||||
|
@ -110,7 +110,7 @@ const SendDetails = () => {
|
||||
const [dumb, setDumb] = useState(false);
|
||||
const { isEditable } = routeParams;
|
||||
// if utxo is limited we use it to calculate available balance
|
||||
const balance: number = utxo ? utxo.reduce((prev, curr) => prev + curr.value, 0) : (wallet?.getBalance() ?? 0);
|
||||
const balance: number = utxo ? utxo.reduce((prev, curr) => prev + curr.value, 0) : wallet?.getBalance() ?? 0;
|
||||
const allBalance = formatBalanceWithoutSuffix(balance, BitcoinUnit.BTC, true);
|
||||
|
||||
// if cutomFee is not set, we need to choose highest possible fee for wallet balance
|
||||
|
@ -139,7 +139,7 @@ const TransactionDetails = () => {
|
||||
// okay, this txid _was_ with someone using payment codes, so we show the label edit dialog
|
||||
// and load user-defined alias for the pc if any
|
||||
|
||||
setCounterpartyLabel(counterpartyMetadata ? (counterpartyMetadata[foundPaymentCode]?.label ?? '') : '');
|
||||
setCounterpartyLabel(counterpartyMetadata ? counterpartyMetadata[foundPaymentCode]?.label ?? '' : '');
|
||||
setIsCounterpartyLabelVisible(true);
|
||||
setPaymentCode(foundPaymentCode);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user