749 lines
26 KiB
TypeScript
749 lines
26 KiB
TypeScript
import url from 'url';
|
|
import { action, observable, reaction, runInAction } from 'mobx';
|
|
import BigNumber from 'bignumber.js';
|
|
import ReactNativeBlobUtil from 'react-native-blob-util';
|
|
import { Alert } from 'react-native';
|
|
import { LNURLWithdrawParams } from 'js-lnurl';
|
|
import querystring from 'querystring-es3';
|
|
|
|
import Invoice from '../models/Invoice';
|
|
import Channel from '../models/Channel';
|
|
import ChannelInfo from '../models/ChannelInfo';
|
|
import SettingsStore from './SettingsStore';
|
|
import LSPStore from './LSPStore';
|
|
import BackendUtils from '../utils/BackendUtils';
|
|
import { localeString } from '../utils/LocaleUtils';
|
|
import { errorToUserFriendly } from '../utils/ErrorUtils';
|
|
import ChannelsStore from './ChannelsStore';
|
|
import NodeInfoStore from './NodeInfoStore';
|
|
import WithdrawalRequest from '../models/WithdrawalRequest';
|
|
|
|
export default class InvoicesStore {
|
|
@observable paymentRequest: string;
|
|
@observable loading = false;
|
|
@observable error = false;
|
|
@observable error_msg: string | null;
|
|
@observable getPayReqError: string | null = null;
|
|
@observable invoices: Array<Invoice> = [];
|
|
@observable invoice: Invoice | null;
|
|
@observable onChainAddress: string | null;
|
|
@observable pay_req: Invoice | null;
|
|
@observable withdrawal_req: WithdrawalRequest | null;
|
|
@observable payment_request: string | null;
|
|
@observable payment_request_amt: string | null;
|
|
@observable creatingInvoice = false;
|
|
@observable creatingInvoiceError = false;
|
|
@observable invoicesCount: number;
|
|
@observable watchedInvoicePaid = false;
|
|
@observable watchedInvoicePaidAmt: number | string | null = null;
|
|
settingsStore: SettingsStore;
|
|
lspStore: LSPStore;
|
|
channelsStore: ChannelsStore;
|
|
nodeInfoStore: NodeInfoStore;
|
|
|
|
// lnd
|
|
@observable loadingFeeEstimate = false;
|
|
@observable feeEstimate: number | null;
|
|
@observable successProbability: number | null;
|
|
|
|
constructor(
|
|
settingsStore: SettingsStore,
|
|
lspStore: LSPStore,
|
|
channelsStore: ChannelsStore,
|
|
nodeInfoStore: NodeInfoStore
|
|
) {
|
|
this.settingsStore = settingsStore;
|
|
this.lspStore = lspStore;
|
|
this.channelsStore = channelsStore;
|
|
this.nodeInfoStore = nodeInfoStore;
|
|
|
|
reaction(
|
|
() => this.pay_req,
|
|
() => {
|
|
if (
|
|
this.pay_req &&
|
|
this.pay_req.destination &&
|
|
BackendUtils.isLNDBased()
|
|
) {
|
|
this.getRoutes(
|
|
this.pay_req.destination,
|
|
this.pay_req.getRequestAmount
|
|
);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
@action
|
|
public reset = () => {
|
|
this.paymentRequest = '';
|
|
this.onChainAddress = '';
|
|
this.loading = false;
|
|
this.error = false;
|
|
this.error_msg = null;
|
|
this.getPayReqError = null;
|
|
this.invoices = [];
|
|
this.invoice = null;
|
|
this.pay_req = null;
|
|
this.payment_request = null;
|
|
this.creatingInvoice = false;
|
|
this.creatingInvoiceError = false;
|
|
this.invoicesCount = 0;
|
|
this.loadingFeeEstimate = false;
|
|
this.feeEstimate = null;
|
|
this.successProbability = null;
|
|
this.watchedInvoicePaid = false;
|
|
this.watchedInvoicePaidAmt = null;
|
|
};
|
|
|
|
@action
|
|
private resetInvoices = () => {
|
|
this.invoices = [];
|
|
this.invoicesCount = 0;
|
|
this.loading = false;
|
|
};
|
|
|
|
@action
|
|
public getInvoices = async () => {
|
|
this.loading = true;
|
|
await BackendUtils.getInvoices()
|
|
.then((data: any) => {
|
|
runInAction(() => {
|
|
this.invoices = data.invoices
|
|
.map((invoice: any) => new Invoice(invoice))
|
|
.slice()
|
|
.reverse();
|
|
this.invoicesCount =
|
|
data.last_index_offset || this.invoices.length;
|
|
this.loading = false;
|
|
});
|
|
})
|
|
.catch(() => this.resetInvoices());
|
|
};
|
|
|
|
@action
|
|
public redeemWithdrawalRequest = async ({
|
|
invreq,
|
|
label
|
|
}: {
|
|
invreq: string;
|
|
label: string;
|
|
}): Promise<WithdrawalRequest | null> => {
|
|
this.loading = true;
|
|
this.error = false;
|
|
this.error_msg = null;
|
|
try {
|
|
let response = await BackendUtils.redeemWithdrawalRequest({
|
|
invreq,
|
|
label
|
|
});
|
|
runInAction(() => {
|
|
this.loading = false;
|
|
this.error = false;
|
|
this.error_msg = null;
|
|
});
|
|
response = new WithdrawalRequest(response);
|
|
return response;
|
|
} catch (error: any) {
|
|
let parsedError;
|
|
try {
|
|
parsedError = JSON.parse(error.message).message;
|
|
} catch (parseErr) {
|
|
parsedError = error.message;
|
|
}
|
|
runInAction(() => {
|
|
this.loading = false;
|
|
this.error = true;
|
|
this.error_msg =
|
|
parsedError ||
|
|
localeString(
|
|
'stores.InvoicesStore.errorRedeemingWithdrawalRequest'
|
|
);
|
|
});
|
|
throw new Error(parsedError);
|
|
}
|
|
};
|
|
|
|
@action
|
|
public createUnifiedInvoice = ({
|
|
memo,
|
|
value,
|
|
expirySeconds = '3600',
|
|
lnurl,
|
|
ampInvoice,
|
|
blindedPaths,
|
|
routeHints,
|
|
routeHintChannels,
|
|
addressType,
|
|
customPreimage,
|
|
noLsp,
|
|
skipOnchain
|
|
}: {
|
|
memo: string;
|
|
value: string;
|
|
expirySeconds: string;
|
|
lnurl?: LNURLWithdrawParams;
|
|
ampInvoice?: boolean;
|
|
blindedPaths?: boolean;
|
|
routeHints?: boolean;
|
|
routeHintChannels?: Channel[];
|
|
addressType?: string;
|
|
customPreimage?: string;
|
|
noLsp?: boolean;
|
|
skipOnchain?: boolean;
|
|
}) => {
|
|
this.creatingInvoice = true;
|
|
return this.createInvoice({
|
|
memo,
|
|
value,
|
|
expirySeconds,
|
|
lnurl,
|
|
ampInvoice,
|
|
blindedPaths,
|
|
routeHints,
|
|
routeHintChannels,
|
|
unified: true,
|
|
customPreimage,
|
|
noLsp
|
|
}).then(
|
|
({
|
|
rHash,
|
|
paymentRequest
|
|
}: {
|
|
rHash: string;
|
|
paymentRequest: string;
|
|
}) => {
|
|
if (BackendUtils.supportsOnchainReceiving() && !skipOnchain) {
|
|
return this.getNewAddress(
|
|
addressType
|
|
? { type: addressType, unified: true }
|
|
: { unified: true }
|
|
)
|
|
.then((onChainAddress: string) => {
|
|
runInAction(() => {
|
|
this.onChainAddress = onChainAddress;
|
|
this.payment_request = paymentRequest;
|
|
this.loading = false;
|
|
this.creatingInvoice = false;
|
|
});
|
|
return { rHash, onChainAddress };
|
|
})
|
|
.catch(() => {
|
|
runInAction(() => {
|
|
this.loading = false;
|
|
this.creatingInvoice = false;
|
|
});
|
|
});
|
|
} else {
|
|
runInAction(() => {
|
|
this.payment_request = paymentRequest;
|
|
this.creatingInvoice = false;
|
|
});
|
|
return { rHash };
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
@action
|
|
public createInvoice = async ({
|
|
memo,
|
|
value,
|
|
expirySeconds = '3600',
|
|
lnurl,
|
|
ampInvoice,
|
|
blindedPaths,
|
|
routeHints,
|
|
routeHintChannels,
|
|
unified,
|
|
customPreimage,
|
|
noLsp
|
|
}: {
|
|
memo: string;
|
|
value: string;
|
|
expirySeconds: string;
|
|
lnurl?: LNURLWithdrawParams;
|
|
ampInvoice?: boolean;
|
|
blindedPaths?: boolean;
|
|
routeHints?: boolean;
|
|
routeHintChannels?: Channel[];
|
|
unified?: boolean;
|
|
customPreimage?: string;
|
|
noLsp?: boolean;
|
|
}) => {
|
|
this.lspStore?.resetFee();
|
|
this.payment_request = null;
|
|
this.payment_request_amt = null;
|
|
if (!unified) this.creatingInvoice = true;
|
|
this.creatingInvoiceError = false;
|
|
this.error_msg = null;
|
|
|
|
const req: any = {
|
|
memo,
|
|
value,
|
|
expiry_seconds: expirySeconds
|
|
};
|
|
|
|
if (ampInvoice) req.is_amp = true;
|
|
if (blindedPaths) req.is_blinded = true;
|
|
if (routeHints) {
|
|
if (routeHintChannels?.length) {
|
|
const routeHints = [];
|
|
for (const routeHintChannel of routeHintChannels) {
|
|
let channelInfo =
|
|
this.channelsStore.chanInfo[
|
|
routeHintChannel.channelId!
|
|
];
|
|
if (!channelInfo) {
|
|
try {
|
|
channelInfo = new ChannelInfo(
|
|
await BackendUtils.getChannelInfo(
|
|
routeHintChannel.channelId
|
|
)
|
|
);
|
|
} catch (error: any) {
|
|
const error_msg = error?.toString();
|
|
runInAction(() => {
|
|
this.creatingInvoiceError = true;
|
|
this.creatingInvoice = false;
|
|
this.error_msg =
|
|
error_msg ||
|
|
localeString(
|
|
'stores.InvoicesStore.errorCreatingInvoice'
|
|
);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
const remotePolicy =
|
|
channelInfo.node1Pub ===
|
|
this.nodeInfoStore.nodeInfo.nodeId
|
|
? channelInfo.node2Policy
|
|
: channelInfo.node1Policy;
|
|
routeHints.push({
|
|
hop_hints: [
|
|
{
|
|
node_id: routeHintChannel.remotePubkey,
|
|
chan_id:
|
|
routeHintChannel.peer_scid_alias &&
|
|
routeHintChannel.peer_scid_alias !== '0'
|
|
? routeHintChannel.peer_scid_alias
|
|
: routeHintChannel.channelId, // must not be converted to Number as this might lead to a loss of precision
|
|
fee_base_msat: Number(
|
|
remotePolicy?.fee_base_msat
|
|
),
|
|
fee_proportional_millionths: Number(
|
|
remotePolicy?.fee_rate_milli_msat
|
|
),
|
|
cltv_expiry_delta: remotePolicy?.time_lock_delta
|
|
}
|
|
]
|
|
});
|
|
}
|
|
req.route_hints = routeHints;
|
|
} else {
|
|
req.private = true;
|
|
}
|
|
}
|
|
|
|
if (customPreimage) req.preimage = customPreimage;
|
|
|
|
if (
|
|
BackendUtils.supportsFlowLSP() &&
|
|
this.settingsStore.settings?.enableLSP &&
|
|
value &&
|
|
value !== '0' &&
|
|
!noLsp
|
|
) {
|
|
const info: any = this.lspStore?.info;
|
|
const method =
|
|
info.connection_methods && info.connection_methods[0];
|
|
|
|
try {
|
|
await this.channelsStore.connectPeer(
|
|
{
|
|
host: `${method.address}:${method.port}`,
|
|
node_pubkey_string: info.pubkey,
|
|
local_funding_amount: ''
|
|
},
|
|
false,
|
|
true
|
|
);
|
|
} catch (e) {}
|
|
|
|
await this.lspStore.getZeroConfFee(
|
|
Number(new BigNumber(value).times(1000))
|
|
);
|
|
|
|
if (new BigNumber(value).gt(this.lspStore.zeroConfFee || 0)) {
|
|
req.value = new BigNumber(value).minus(
|
|
this.lspStore.zeroConfFee || 0
|
|
);
|
|
}
|
|
}
|
|
|
|
if (this.lspStore.flow_error) {
|
|
this.creatingInvoice = false;
|
|
return;
|
|
}
|
|
|
|
return BackendUtils.createInvoice(req)
|
|
.then(async (data: any) => {
|
|
if (data.error) {
|
|
const data_message = data.message?.toString();
|
|
const data_error = data.error.toString();
|
|
runInAction(() => {
|
|
this.creatingInvoiceError = true;
|
|
if (!unified) this.creatingInvoice = false;
|
|
const errString = data_message || data_error;
|
|
this.error_msg =
|
|
errString === 'Bad arguments' &&
|
|
this.settingsStore.implementation === 'lndhub' &&
|
|
req.value === '0'
|
|
? localeString(
|
|
'stores.InvoicesStore.zeroAmountLndhub'
|
|
)
|
|
: errString ||
|
|
localeString(
|
|
'stores.InvoicesStore.errorCreatingInvoice'
|
|
);
|
|
});
|
|
}
|
|
const invoice = new Invoice(data);
|
|
|
|
runInAction(() => {
|
|
if (!unified)
|
|
this.payment_request = invoice.getPaymentRequest;
|
|
this.payment_request_amt = value;
|
|
|
|
if (!unified) this.creatingInvoice = false;
|
|
});
|
|
|
|
let jit_bolt11: string = '';
|
|
if (
|
|
BackendUtils.supportsFlowLSP() &&
|
|
this.settingsStore.settings?.enableLSP &&
|
|
value !== '0' &&
|
|
!noLsp
|
|
) {
|
|
await this.lspStore
|
|
.getZeroConfInvoice(invoice.getPaymentRequest)
|
|
.then((response: any) => {
|
|
jit_bolt11 = response;
|
|
});
|
|
}
|
|
|
|
if (lnurl) {
|
|
const u = url.parse(lnurl.callback);
|
|
const qs = querystring.parse(u.query);
|
|
qs.k1 = lnurl.k1;
|
|
qs.pr = jit_bolt11 || invoice.getPaymentRequest;
|
|
u.search = querystring.stringify(qs);
|
|
u.query = querystring.stringify(qs);
|
|
|
|
ReactNativeBlobUtil.fetch('get', url.format(u))
|
|
.then((response: any) => {
|
|
try {
|
|
const data = response.json();
|
|
return data;
|
|
} catch (err) {
|
|
return {
|
|
status: 'ERROR',
|
|
reason: response.text()
|
|
};
|
|
}
|
|
})
|
|
.catch((err: any) => ({
|
|
status: 'ERROR',
|
|
reason: err.message
|
|
}))
|
|
.then((data: any) => {
|
|
if (data.status === 'ERROR') {
|
|
Alert.alert(
|
|
`[error] ${lnurl.domain} says:`,
|
|
data.reason,
|
|
[
|
|
{
|
|
text: localeString('general.error'),
|
|
onPress: () => void 0
|
|
}
|
|
],
|
|
{ cancelable: false }
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
rHash: invoice.getFormattedRhash,
|
|
paymentRequest: jit_bolt11
|
|
? jit_bolt11
|
|
: invoice.getPaymentRequest
|
|
};
|
|
})
|
|
.catch((error: any) => {
|
|
const error_msg = error?.toString();
|
|
runInAction(() => {
|
|
this.creatingInvoiceError = true;
|
|
this.creatingInvoice = false;
|
|
this.error_msg =
|
|
error_msg ||
|
|
localeString(
|
|
'stores.InvoicesStore.errorCreatingInvoice'
|
|
);
|
|
});
|
|
});
|
|
};
|
|
|
|
@action
|
|
public fetchIncomingChannel = async (hash: string) => {
|
|
try {
|
|
const response = await BackendUtils.lookupInvoice({ r_hash: hash });
|
|
const enhancedPath: any[] = [];
|
|
|
|
if (response?.htlcs?.length) {
|
|
response.htlcs.forEach((htlc: any) => {
|
|
if (htlc.state === 'SETTLED') {
|
|
const amount = htlc.amt_msat
|
|
? Math.round(Number(htlc.amt_msat) / 1000)
|
|
: 0;
|
|
|
|
const route: any[] = [];
|
|
|
|
if (htlc.chan_id) {
|
|
const chanId = String(htlc.chan_id);
|
|
|
|
const channel = this.channelsStore?.channels.find(
|
|
(c) =>
|
|
String(c.chan_id) === chanId ||
|
|
String(c.channelId) === chanId ||
|
|
(c.channel_point &&
|
|
c.channel_point.split(':')[0] ===
|
|
chanId)
|
|
);
|
|
|
|
const pubKey =
|
|
channel?.remotePubkey ||
|
|
htlc.custom_records?.['34349334']?.toString(
|
|
'hex'
|
|
) ||
|
|
localeString('general.unknown');
|
|
|
|
const alias =
|
|
(pubKey !== localeString('general.unknown') &&
|
|
this.channelsStore?.nodes?.[pubKey]
|
|
?.alias) ||
|
|
this.channelsStore?.aliasesByChannelId?.[
|
|
chanId
|
|
] ||
|
|
(pubKey !== localeString('general.unknown') &&
|
|
this.channelsStore?.aliasMap?.get(pubKey));
|
|
|
|
route.push({
|
|
pubKey,
|
|
chan_id: chanId,
|
|
node: alias || pubKey,
|
|
alias,
|
|
forwarded: amount,
|
|
fee: 0
|
|
});
|
|
}
|
|
|
|
enhancedPath.push(route);
|
|
}
|
|
});
|
|
}
|
|
|
|
return enhancedPath;
|
|
} catch (error) {
|
|
console.error(error);
|
|
return [];
|
|
}
|
|
};
|
|
|
|
@action
|
|
public setWatchedInvoicePaid = (amount?: string | number) => {
|
|
this.watchedInvoicePaid = true;
|
|
if (amount) this.watchedInvoicePaidAmt = amount;
|
|
};
|
|
|
|
@action
|
|
public getNewAddress = (params: any) => {
|
|
if (!params.unified) {
|
|
this.creatingInvoice = true;
|
|
this.error_msg = null;
|
|
}
|
|
// ZEUS-2396
|
|
// https://github.com/ZeusLN/zeus/issues/2396
|
|
if (params.account && params.account !== 'default') {
|
|
delete params.type;
|
|
}
|
|
this.onChainAddress = null;
|
|
return BackendUtils.getNewAddress(params)
|
|
.then((data: any) => {
|
|
const address =
|
|
data.address ||
|
|
data.bech32 ||
|
|
data.p2tr ||
|
|
(data[0] && data[0].address);
|
|
runInAction(() => {
|
|
if (!params.unified) this.onChainAddress = address;
|
|
if (!params.unified) this.creatingInvoice = false;
|
|
});
|
|
return address;
|
|
})
|
|
.catch((error: any) => {
|
|
const error_msg = error.toString();
|
|
runInAction(() => {
|
|
this.error_msg =
|
|
error_msg ||
|
|
localeString(
|
|
'stores.InvoicesStore.errorGeneratingAddress'
|
|
);
|
|
this.creatingInvoice = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
@action
|
|
public getNewChangeAddress = (params: any) => {
|
|
if (!params.unified) {
|
|
this.creatingInvoice = true;
|
|
this.error_msg = null;
|
|
}
|
|
|
|
params.change = true;
|
|
|
|
this.onChainAddress = null;
|
|
return BackendUtils.getNewChangeAddress(params)
|
|
.then((data: any) => {
|
|
const address = data.addr;
|
|
runInAction(() => {
|
|
if (!params.unified) this.onChainAddress = address;
|
|
if (!params.unified) this.creatingInvoice = false;
|
|
});
|
|
return address;
|
|
})
|
|
.catch((error: any) => {
|
|
const error_msg = error?.toString();
|
|
runInAction(() => {
|
|
this.error_msg =
|
|
error_msg ||
|
|
localeString(
|
|
'stores.InvoicesStore.errorGeneratingAddress'
|
|
);
|
|
this.creatingInvoice = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
public clearAddress = () => (this.onChainAddress = null);
|
|
|
|
private clearPaymentRequest = () => (this.payment_request = null);
|
|
|
|
@action
|
|
public clearPayReq = () => {
|
|
this.pay_req = null;
|
|
this.getPayReqError = null;
|
|
};
|
|
|
|
@action
|
|
public clearUnified = () => {
|
|
this.clearAddress();
|
|
this.clearPaymentRequest();
|
|
this.error_msg = null;
|
|
this.creatingInvoiceError = false;
|
|
};
|
|
|
|
@action
|
|
public getPayReq = (paymentRequest: string) => {
|
|
this.loading = true;
|
|
this.pay_req = null;
|
|
this.paymentRequest = paymentRequest;
|
|
this.feeEstimate = null;
|
|
|
|
return BackendUtils.decodePaymentRequest([paymentRequest])
|
|
.then((data: any) => {
|
|
runInAction(() => {
|
|
this.pay_req = new Invoice(data);
|
|
this.getPayReqError = null;
|
|
this.loading = false;
|
|
});
|
|
return;
|
|
})
|
|
.catch((error: Error) => {
|
|
const error_friendly = errorToUserFriendly(error);
|
|
runInAction(() => {
|
|
this.pay_req = null;
|
|
this.getPayReqError = error_friendly;
|
|
this.loading = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
@action
|
|
public getWithdrawalReq = (withdrawalReq: string) => {
|
|
this.loading = true;
|
|
this.withdrawal_req = null;
|
|
this.paymentRequest = withdrawalReq;
|
|
this.feeEstimate = null;
|
|
|
|
return BackendUtils.decodePaymentRequest([withdrawalReq])
|
|
.then((data: any) => {
|
|
runInAction(() => {
|
|
this.withdrawal_req = new WithdrawalRequest(data);
|
|
this.getPayReqError = null;
|
|
this.loading = false;
|
|
});
|
|
return;
|
|
})
|
|
.catch((error: Error) => {
|
|
const error_friendly = errorToUserFriendly(error);
|
|
runInAction(() => {
|
|
this.withdrawal_req = null;
|
|
this.getPayReqError = error_friendly;
|
|
this.loading = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
@action
|
|
private getRoutesError = () => {
|
|
this.loadingFeeEstimate = false;
|
|
this.feeEstimate = null;
|
|
this.successProbability = null;
|
|
};
|
|
|
|
@action
|
|
private getRoutes = (destination: string, amount: string | number) => {
|
|
this.loadingFeeEstimate = true;
|
|
this.feeEstimate = null;
|
|
this.successProbability = null;
|
|
|
|
return BackendUtils.getRoutes([destination, amount])
|
|
.then((data: any) => {
|
|
runInAction(() => {
|
|
this.loadingFeeEstimate = false;
|
|
this.successProbability = data.success_prob
|
|
? data.success_prob * 100
|
|
: 0;
|
|
|
|
const routes = data.routes;
|
|
if (routes) {
|
|
routes.forEach((route: any) => {
|
|
// expect lnd to pick the cheapest route
|
|
if (this.feeEstimate) {
|
|
if (route.total_fees < this.feeEstimate) {
|
|
this.feeEstimate = route.total_fees;
|
|
}
|
|
} else {
|
|
this.feeEstimate = route.total_fees || 0;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
})
|
|
.catch(() => this.getRoutesError());
|
|
};
|
|
}
|