FIX: Lndhub lacked timeouts

This commit is contained in:
Marcos Rodriguez Velez 2025-02-03 01:29:48 -04:00
parent ec027a12df
commit a6306c53d8
3 changed files with 376 additions and 268 deletions

View file

@ -1,6 +1,7 @@
import bolt11 from 'bolt11';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { LegacyWallet } from './legacy-wallet';
import loc from '../../loc';
export class LightningCustodianWallet extends LegacyWallet {
static readonly type = 'lightningCustodianWallet';
@ -95,48 +96,64 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async createAccount(isTest: boolean = false) {
const response = await fetch(this.baseURI + '/create', {
method: 'POST',
body: JSON.stringify({ partnerid: 'bluewallet', accounttype: (isTest && 'test') || 'common' }),
headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' },
});
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
}
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/create', {
method: 'POST',
body: JSON.stringify({ partnerid: 'bluewallet', accounttype: (isTest && 'test') || 'common' }),
headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' },
signal: controller.signal,
});
clearTimeout(id);
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + (json.message ? json.message : json.error) + ' (code ' + json.code + ')');
}
if (json.error) {
throw new Error('API error: ' + (json.message ? json.message : json.error) + ' (code ' + json.code + ')');
}
if (!json.login || !json.password) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
if (!json.login || !json.password) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
this.secret = 'lndhub://' + json.login + ':' + json.password;
this.secret = 'lndhub://' + json.login + ':' + json.password;
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
}
async payInvoice(invoice: string, freeAmount: number = 0) {
const response = await fetch(this.baseURI + '/payinvoice', {
method: 'POST',
body: JSON.stringify({ invoice, amount: freeAmount }),
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
});
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/payinvoice', {
method: 'POST',
body: JSON.stringify({ invoice, amount: freeAmount }),
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
signal: controller.signal,
});
clearTimeout(id);
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
this.last_paid_invoice_result = json;
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
this.last_paid_invoice_result = json;
}
/**
@ -147,47 +164,56 @@ export class LightningCustodianWallet extends LegacyWallet {
async getUserInvoices(limit: number | false = false) {
let limitString = '';
if (limit) limitString = '?limit=' + parseInt(limit as unknown as string, 10);
const response = await fetch(this.baseURI + '/getuserinvoices' + limitString, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
});
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
}
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/getuserinvoices' + limitString, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
signal: controller.signal,
});
clearTimeout(id);
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (limit) {
// need to merge existing invoices with the ones that arrived
// but the ones received later should overwrite older ones
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
for (const oldInvoice of this.user_invoices_raw) {
// iterate all OLD invoices
let found = false;
for (const newInvoice of json) {
// iterate all NEW invoices
if (newInvoice.payment_request === oldInvoice.payment_request) found = true;
}
if (limit) {
// need to merge existing invoices with the ones that arrived
// but the ones received later should overwrite older ones
if (!found) {
// if old invoice is not found in NEW array, we simply add it:
json.push(oldInvoice);
for (const oldInvoice of this.user_invoices_raw) {
// iterate all OLD invoices
let found = false;
for (const newInvoice of json) {
// iterate all NEW invoices
if (newInvoice.payment_request === oldInvoice.payment_request) found = true;
}
if (!found) {
// if old invoice is not found in NEW array, we simply add it:
json.push(oldInvoice);
}
}
}
this.user_invoices_raw = json.sort(function (a: { timestamp: number }, b: { timestamp: number }) {
return a.timestamp - b.timestamp;
});
return this.user_invoices_raw;
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
this.user_invoices_raw = json.sort(function (a: { timestamp: number }, b: { timestamp: number }) {
return a.timestamp - b.timestamp;
});
return this.user_invoices_raw;
}
/**
@ -209,29 +235,38 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async addInvoice(amt: number, memo: string) {
const response = await fetch(this.baseURI + '/addinvoice', {
method: 'POST',
body: JSON.stringify({ amt: amt + '', memo }),
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
});
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
}
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/addinvoice', {
method: 'POST',
body: JSON.stringify({ amt: amt + '', memo }),
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
signal: controller.signal,
});
clearTimeout(id);
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (!json.r_hash || !json.pay_req) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
return json.pay_req;
if (!json.r_hash || !json.pay_req) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
return json.pay_req;
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
}
/**
@ -249,29 +284,37 @@ export class LightningCustodianWallet extends LegacyWallet {
login = this.secret.replace('lndhub://', '').split(':')[0];
password = this.secret.replace('lndhub://', '').split(':')[1];
}
const response = await fetch(this.baseURI + '/auth?type=auth', {
method: 'POST',
body: JSON.stringify({ login, password }),
headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' },
});
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/auth?type=auth', {
method: 'POST',
body: JSON.stringify({ login, password }),
headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' },
signal: controller.signal,
});
clearTimeout(id);
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!json.access_token || !json.refresh_token) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
this.refresh_token = json.refresh_token;
this.access_token = json.access_token;
this._refresh_token_created_ts = +new Date();
this._access_token_created_ts = +new Date();
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!json.access_token || !json.refresh_token) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
this.refresh_token = json.refresh_token;
this.access_token = json.access_token;
this._refresh_token_created_ts = +new Date();
this._access_token_created_ts = +new Date();
}
async checkLogin() {
@ -297,54 +340,70 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async refreshAcessToken() {
const response = await fetch(this.baseURI + '/auth?type=refresh_token', {
method: 'POST',
body: JSON.stringify({ refresh_token: this.refresh_token }),
headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' },
});
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/auth?type=refresh_token', {
method: 'POST',
body: JSON.stringify({ refresh_token: this.refresh_token }),
headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' },
signal: controller.signal,
});
clearTimeout(id);
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!json.access_token || !json.refresh_token) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
this.refresh_token = json.refresh_token;
this.access_token = json.access_token;
this._refresh_token_created_ts = +new Date();
this._access_token_created_ts = +new Date();
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!json.access_token || !json.refresh_token) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
this.refresh_token = json.refresh_token;
this.access_token = json.access_token;
this._refresh_token_created_ts = +new Date();
this._access_token_created_ts = +new Date();
}
async fetchBtcAddress() {
const response = await fetch(this.baseURI + '/getbtc', {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
});
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/getbtc', {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
signal: controller.signal,
});
clearTimeout(id);
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
}
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
this.refill_addressess = [];
this.refill_addressess = [];
for (const arr of json) {
this.refill_addressess.push(arr.address);
for (const arr of json) {
this.refill_addressess.push(arr.address);
}
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
}
@ -405,25 +464,33 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async fetchPendingTransactions() {
const response = await fetch(this.baseURI + '/getpending', {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
});
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/getpending', {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
signal: controller.signal,
});
clearTimeout(id);
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
this.pending_transactions_raw = json;
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
this.pending_transactions_raw = json;
}
async fetchTransactions() {
@ -434,30 +501,38 @@ export class LightningCustodianWallet extends LegacyWallet {
queryRes += '?limit=' + limit;
queryRes += '&offset=' + offset;
const response = await fetch(this.baseURI + '/gettxs' + queryRes, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
});
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/gettxs' + queryRes, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
signal: controller.signal,
});
clearTimeout(id);
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!Array.isArray(json)) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
this._lastTxFetch = +new Date();
this.transactions_raw = json;
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!Array.isArray(json)) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
this._lastTxFetch = +new Date();
this.transactions_raw = json;
}
getBalance() {
@ -467,34 +542,42 @@ export class LightningCustodianWallet extends LegacyWallet {
async fetchBalance(noRetry?: boolean): Promise<void> {
await this.checkLogin();
const response = await fetch(this.baseURI + '/balance', {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
});
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/balance', {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
signal: controller.signal,
});
clearTimeout(id);
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
}
if (json.error) {
if (json.code * 1 === 1 && !noRetry) {
await this.authorize();
return this.fetchBalance(true);
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!json.BTC || typeof json.BTC.AvailableBalance === 'undefined') {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
if (json.error) {
if (json.code * 1 === 1 && !noRetry) {
await this.authorize();
return this.fetchBalance(true);
}
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
this.balance = json.BTC.AvailableBalance;
this._lastBalanceFetch = +new Date();
if (!json.BTC || typeof json.BTC.AvailableBalance === 'undefined') {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
this.balance = json.BTC.AvailableBalance;
this._lastBalanceFetch = +new Date();
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
}
/**
@ -556,50 +639,66 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async fetchInfo() {
const response = await fetch(this.baseURI + '/getinfo', {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
});
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/getinfo', {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
signal: controller.signal,
});
clearTimeout(id);
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
}
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!json.identity_pubkey) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
if (!json.identity_pubkey) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
}
static async isValidNodeAddress(address: string): Promise<boolean> {
const normalizedAddress = new URL('/getinfo', address.replace(/([^:]\/)\/+/g, '$1'));
const response = await fetch(normalizedAddress.toString(), {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
});
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(normalizedAddress.toString(), {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
signal: controller.signal,
});
clearTimeout(id);
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.code && json.code !== 1) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
return true;
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.code && json.code !== 1) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
return true;
}
allowReceive() {
@ -629,29 +728,37 @@ export class LightningCustodianWallet extends LegacyWallet {
async decodeInvoiceRemote(invoice: string) {
await this.checkLogin();
const response = await fetch(this.baseURI + '/decodeinvoice?invoice=' + invoice, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
});
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.baseURI + '/decodeinvoice?invoice=' + invoice, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
signal: controller.signal,
});
clearTimeout(id);
const json = await response.json();
if (!json) {
throw new Error('API failure: ' + response.statusText);
const json = await response.json();
if (!json) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!json.payment_hash) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
return (this.decoded_invoice_raw = json);
} catch (err) {
throw new Error(loc.lnd.errorServerUnreachable);
}
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!json.payment_hash) {
throw new Error('API unexpected response: ' + JSON.stringify(json));
}
return (this.decoded_invoice_raw = json);
}
weOwnTransaction(txid: string) {

View file

@ -3,13 +3,13 @@ import DefaultPreference from 'react-native-default-preference';
import { BlueApp } from '../class';
import { GROUP_IO_BLUEWALLET } from '../blue_modules/currency';
// Function to get the value from DefaultPreference first, then fallback to AsyncStorage
// Function to get the value from DefaultPreference first, then fallback to AsyncStorage
// as DefaultPreference uses truly native storage.
// If found in AsyncStorage, migrate it to DefaultPreference and remove it from AsyncStorage.
export const getLNDHub = async (): Promise<string | undefined> => {
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
let value = await DefaultPreference.get(BlueApp.LNDHUB) as string | null;
let value = (await DefaultPreference.get(BlueApp.LNDHUB)) as string | null;
// If not found, check AsyncStorage and migrate it to DefaultPreference
if (!value) {
@ -46,4 +46,4 @@ export const clearLNDHub = async (): Promise<void> => {
} catch (error) {
console.error('Error clearing LNDHub preference:', error);
}
};
};

View file

@ -53,6 +53,7 @@
},
"lnd": {
"errorInvoiceExpired": "Invoice expired.",
"errorServerUnreachable": "Unable to connect to the server. Please check your internet connection or try again.",
"expired": "Expired",
"expiresIn": "Expires in {time} minutes",
"payButton": "Pay",