843 lines
28 KiB
TypeScript
843 lines
28 KiB
TypeScript
const bitcoin = require('bitcoinjs-lib');
|
|
|
|
import { action, observable, runInAction } from 'mobx';
|
|
import { randomBytes } from 'react-native-randombytes';
|
|
import { sha256 } from 'js-sha256';
|
|
import ReactNativeBlobUtil from 'react-native-blob-util';
|
|
|
|
import FundedPsbt from '../models/FundedPsbt';
|
|
import Transaction from '../models/Transaction';
|
|
import TransactionRequest, { OutPoint } from '../models/TransactionRequest';
|
|
import Payment from '../models/Payment';
|
|
|
|
import SettingsStore from './SettingsStore';
|
|
|
|
import BackendUtils from '../utils/BackendUtils';
|
|
import Base64Utils from '../utils/Base64Utils';
|
|
import { errorToUserFriendly } from '../utils/ErrorUtils';
|
|
import { localeString } from '../utils/LocaleUtils';
|
|
import { checkGraphSyncBeforePayment } from '../utils/GraphSyncUtils';
|
|
|
|
import { lnrpc } from '../proto/lightning';
|
|
import NodeInfoStore from './NodeInfoStore';
|
|
import ChannelsStore from './ChannelsStore';
|
|
import BalanceStore from './BalanceStore';
|
|
|
|
const keySendPreimageType = '5482373484';
|
|
const keySendMessageType = '34349334';
|
|
const preimageByteLength = 32;
|
|
|
|
export interface SendPaymentReq {
|
|
payment_request?: string;
|
|
amount?: string;
|
|
pubkey?: string;
|
|
max_parts?: string;
|
|
max_shard_amt?: string;
|
|
fee_limit_sat?: string;
|
|
max_fee_percent?: string;
|
|
outgoing_chan_id?: string;
|
|
last_hop_pubkey?: string;
|
|
message?: string;
|
|
amp?: boolean;
|
|
timeout_seconds?: string;
|
|
}
|
|
|
|
export default class TransactionsStore {
|
|
@observable loading = false;
|
|
@observable crafting = false;
|
|
@observable error = false;
|
|
@observable error_msg: string | null;
|
|
@observable transactions: Array<Transaction> = [];
|
|
@observable transaction: Transaction | null;
|
|
@observable showGraphSyncPrompt = false;
|
|
@observable pendingPaymentData: SendPaymentReq | null = null;
|
|
@observable payment_route: any; // Route
|
|
@observable payment_preimage: string | null;
|
|
@observable payment_fee: string | null;
|
|
@observable isIncomplete: boolean | null;
|
|
@observable payment_hash: any;
|
|
@observable payment_error: any;
|
|
@observable onchain_address: string;
|
|
@observable txid: string | null;
|
|
@observable status: string | number | null;
|
|
@observable noteKey: string;
|
|
@observable paymentStartTime: number | null = null;
|
|
@observable paymentDuration: number | null = null;
|
|
@observable donationIsPaid: boolean = false;
|
|
|
|
// in lieu of receiving txid on LND's publishTransaction
|
|
@observable publishSuccess = false;
|
|
@observable broadcast_txid: string;
|
|
@observable broadcast_err: string | null;
|
|
// coin control
|
|
@observable funded_psbt: string = '';
|
|
|
|
settingsStore: SettingsStore;
|
|
nodeInfoStore: NodeInfoStore;
|
|
channelsStore: ChannelsStore;
|
|
balanceStore: BalanceStore;
|
|
|
|
constructor(
|
|
settingsStore: SettingsStore,
|
|
nodeInfoStore: NodeInfoStore,
|
|
channelsStore: ChannelsStore,
|
|
balanceStore: BalanceStore
|
|
) {
|
|
this.settingsStore = settingsStore;
|
|
this.nodeInfoStore = nodeInfoStore;
|
|
this.channelsStore = channelsStore;
|
|
this.balanceStore = balanceStore;
|
|
}
|
|
|
|
@action
|
|
public reset = () => {
|
|
this.loading = false;
|
|
this.error = false;
|
|
this.error_msg = null;
|
|
this.transactions = [];
|
|
this.transaction = null;
|
|
this.payment_route = null;
|
|
this.payment_preimage = null;
|
|
this.isIncomplete = null;
|
|
this.payment_hash = null;
|
|
this.payment_error = null;
|
|
this.onchain_address = '';
|
|
this.txid = null;
|
|
this.publishSuccess = false;
|
|
this.status = null;
|
|
this.broadcast_txid = '';
|
|
this.broadcast_err = null;
|
|
this.funded_psbt = '';
|
|
this.paymentStartTime = null;
|
|
this.paymentDuration = null;
|
|
};
|
|
|
|
public getTransactions = async () => {
|
|
this.loading = true;
|
|
await BackendUtils.getTransactions()
|
|
.then((data: any) => {
|
|
runInAction(() => {
|
|
this.transactions = data.transactions
|
|
.slice()
|
|
.reverse()
|
|
.map((tx: any) => new Transaction(tx));
|
|
this.loading = false;
|
|
});
|
|
})
|
|
.catch(() => {
|
|
runInAction(() => {
|
|
this.transactions = [];
|
|
this.loading = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
@action
|
|
public broadcast = async (raw_final_tx: string) => {
|
|
this.loading = true;
|
|
|
|
const tx_hex = raw_final_tx.includes('=')
|
|
? Base64Utils.base64ToHex(raw_final_tx)
|
|
: raw_final_tx;
|
|
|
|
// Decode the raw transaction hex string
|
|
let txid: string;
|
|
try {
|
|
const tx = bitcoin.Transaction.fromHex(tx_hex);
|
|
// Get the transaction ID (txid)
|
|
txid = tx.getId();
|
|
} catch (e) {}
|
|
|
|
// CLN REST does not support publishTransaction; broadcast via mempool.space
|
|
if (this.settingsStore.implementation === 'cln-rest') {
|
|
const headers = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Content-Type': 'text/plain'
|
|
} as any;
|
|
|
|
const url = `https://mempool.space/${
|
|
this.nodeInfoStore.nodeInfo.isTestNet ? 'testnet/' : ''
|
|
}api/tx`;
|
|
|
|
return ReactNativeBlobUtil.fetch('POST', url, headers, tx_hex)
|
|
.then((response: any) => {
|
|
const status = response.info().status;
|
|
const data = response.data;
|
|
if (status == 200) {
|
|
runInAction(() => {
|
|
this.txid = data || txid;
|
|
this.publishSuccess = true;
|
|
this.loading = false;
|
|
this.channelsStore.resetOpenChannel();
|
|
});
|
|
return data;
|
|
} else {
|
|
const errorMsg = errorToUserFriendly(data);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
}
|
|
})
|
|
.catch((err: any) => {
|
|
const errorMsg = errorToUserFriendly(
|
|
err?.error || err?.message || err?.toString()
|
|
);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
return BackendUtils.publishTransaction({
|
|
tx_hex
|
|
})
|
|
.then((data: any) => {
|
|
if (data.publish_error) {
|
|
const errorMsg = errorToUserFriendly(data.publish_error);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
} else {
|
|
runInAction(() => {
|
|
this.txid = txid;
|
|
this.publishSuccess = true;
|
|
this.loading = false;
|
|
this.channelsStore.resetOpenChannel();
|
|
});
|
|
}
|
|
})
|
|
.catch((error: any) => {
|
|
const errorMsg = errorToUserFriendly(
|
|
error.publish_error || error.message
|
|
);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
@action
|
|
public finalizePsbtAndBroadcast = (
|
|
funded_psbt: string,
|
|
defaultAccount?: boolean
|
|
) => {
|
|
this.funded_psbt = '';
|
|
this.loading = true;
|
|
|
|
if (defaultAccount) {
|
|
return BackendUtils.finalizePsbt({ funded_psbt })
|
|
.then((data: any) => this.broadcast(data.raw_final_tx))
|
|
.catch((error: any) => {
|
|
const errorMsg = errorToUserFriendly(error.message);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
});
|
|
} else {
|
|
return new Promise((resolve) => {
|
|
try {
|
|
// Parse the PSBT
|
|
const psbt = bitcoin.Psbt.fromBase64(funded_psbt);
|
|
// Step 2: Finalize each input
|
|
psbt.data.inputs.forEach((input: any, index: number) => {
|
|
if (
|
|
!input.finalScriptSig &&
|
|
!input.finalScriptWitness
|
|
) {
|
|
psbt.finalizeInput(index); // This finalizes the input
|
|
}
|
|
});
|
|
|
|
// Step 3: Extract the transaction
|
|
const txHex = psbt.extractTransaction().toHex();
|
|
|
|
this.broadcast(txHex);
|
|
|
|
resolve(true);
|
|
} catch (error: any) {
|
|
const errorMsg = errorToUserFriendly(
|
|
error?.message || error
|
|
);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
|
|
resolve(true);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
public finalizePsbtAndBroadcastChannel = async (
|
|
signed_psbt: string,
|
|
pending_chan_ids: Array<string>
|
|
) => {
|
|
this.loading = true;
|
|
|
|
return BackendUtils.fundingStateStep({
|
|
psbt_finalize: {
|
|
signed_psbt,
|
|
pending_chan_id: pending_chan_ids[pending_chan_ids.length - 1]
|
|
}
|
|
})
|
|
.then((data: any) => {
|
|
if (data.publish_error) {
|
|
const errorMsg = errorToUserFriendly(data.publish_error);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
} else {
|
|
runInAction(() => {
|
|
try {
|
|
// Parse the PSBT
|
|
const psbt = bitcoin.Psbt.fromBase64(signed_psbt);
|
|
|
|
// Extract the finalized transaction from the PSBT
|
|
const finalizedTx = psbt.extractTransaction();
|
|
|
|
// Serialize the transaction and calculate its hash to obtain the txid
|
|
const txid = finalizedTx.getId();
|
|
this.txid = txid;
|
|
} catch (e) {}
|
|
this.publishSuccess = true;
|
|
this.loading = false;
|
|
this.channelsStore.resetOpenChannel();
|
|
});
|
|
}
|
|
})
|
|
.catch((error: any) => {
|
|
const errorMsg = errorToUserFriendly(error.message);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
public finalizeTxHexAndBroadcastChannel = async (
|
|
tx_hex: string,
|
|
pending_chan_ids: Array<string>
|
|
) => {
|
|
this.loading = true;
|
|
|
|
return BackendUtils.fundingStateStep({
|
|
psbt_finalize: {
|
|
final_raw_tx: tx_hex,
|
|
pending_chan_id: pending_chan_ids[pending_chan_ids.length - 1]
|
|
}
|
|
})
|
|
.then((data: any) => {
|
|
if (data.publish_error) {
|
|
const errorMsg = errorToUserFriendly(data.publish_error);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
} else {
|
|
runInAction(() => {
|
|
try {
|
|
// Parse the tx
|
|
const tx = bitcoin.Transaction.fromHex(tx_hex);
|
|
|
|
// Serialize the transaction and calculate its hash
|
|
const txid = tx.getId();
|
|
this.txid = txid;
|
|
} catch (e) {}
|
|
this.publishSuccess = true;
|
|
this.loading = false;
|
|
this.channelsStore.resetOpenChannel();
|
|
});
|
|
}
|
|
})
|
|
.catch((error: any) => {
|
|
const errorMsg = errorToUserFriendly(error.message);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
private sendCoinsLNDCoinControl = (
|
|
transactionRequest: TransactionRequest,
|
|
defaultAccount?: boolean
|
|
) => {
|
|
const {
|
|
utxos,
|
|
addr,
|
|
amount,
|
|
sat_per_vbyte,
|
|
account,
|
|
additional_outputs
|
|
} = transactionRequest;
|
|
const inputs: any = [];
|
|
const outputs: any = {};
|
|
|
|
if (utxos) {
|
|
utxos.forEach((input) => {
|
|
const [txid_str, output_index] = input.split(':');
|
|
inputs.push({ txid_str, output_index: Number(output_index) });
|
|
});
|
|
}
|
|
|
|
if (addr) {
|
|
outputs[addr] = Number(amount);
|
|
}
|
|
|
|
if (additional_outputs) {
|
|
additional_outputs.map((output) => {
|
|
outputs[output.address] = Number(output.satAmount);
|
|
});
|
|
}
|
|
|
|
const fundPsbtRequest = {
|
|
raw: {
|
|
outputs,
|
|
inputs
|
|
},
|
|
sat_per_vbyte: Number(sat_per_vbyte),
|
|
spend_unconfirmed: true,
|
|
account
|
|
};
|
|
|
|
BackendUtils.fundPsbt(fundPsbtRequest)
|
|
.then((data: any) => {
|
|
runInAction(() => {
|
|
this.crafting = false;
|
|
const funded_psbt: string = new FundedPsbt(
|
|
data.funded_psbt
|
|
).getFormatted();
|
|
|
|
if (account !== 'default') {
|
|
this.funded_psbt = funded_psbt;
|
|
this.loading = false;
|
|
} else {
|
|
this.finalizePsbtAndBroadcast(
|
|
funded_psbt,
|
|
defaultAccount
|
|
);
|
|
}
|
|
});
|
|
})
|
|
.catch((error: any) => {
|
|
const errorMsg = errorToUserFriendly(error.message);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.crafting = false;
|
|
this.loading = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
@action
|
|
public sendCoins = (transactionRequest: TransactionRequest) => {
|
|
this.funded_psbt = '';
|
|
this.error = false;
|
|
this.error_msg = null;
|
|
this.txid = null;
|
|
this.publishSuccess = false;
|
|
this.crafting = true;
|
|
this.loading = true;
|
|
|
|
if (transactionRequest.send_all) {
|
|
delete transactionRequest.amount;
|
|
}
|
|
|
|
if (
|
|
BackendUtils.isLNDBased() &&
|
|
transactionRequest.utxos &&
|
|
transactionRequest.utxos.length > 0 &&
|
|
transactionRequest.account === 'default' &&
|
|
BackendUtils.supportsOnchainSendMax()
|
|
) {
|
|
const outpoints: OutPoint[] = [];
|
|
transactionRequest.utxos.forEach((input) => {
|
|
const [txid_str, output_index] = input.split(':');
|
|
outpoints.push({
|
|
txid_str,
|
|
output_index: Number(output_index)
|
|
});
|
|
});
|
|
transactionRequest.outpoints = outpoints;
|
|
} else if (
|
|
(BackendUtils.isLNDBased() &&
|
|
transactionRequest.utxos &&
|
|
transactionRequest.utxos.length > 0) ||
|
|
(transactionRequest?.additional_outputs?.length &&
|
|
transactionRequest?.additional_outputs?.length > 0)
|
|
) {
|
|
return this.sendCoinsLNDCoinControl(
|
|
transactionRequest,
|
|
transactionRequest.account === 'default'
|
|
);
|
|
}
|
|
|
|
this.crafting = false;
|
|
|
|
BackendUtils.sendCoins(transactionRequest)
|
|
.then((data: any) => {
|
|
runInAction(() => {
|
|
this.txid = data.txid;
|
|
this.publishSuccess = true;
|
|
this.loading = false;
|
|
this.balanceStore.getCombinedBalance();
|
|
});
|
|
})
|
|
.catch((error: Error) => {
|
|
const errorMsg = errorToUserFriendly(error);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
@action
|
|
public checkGraphSyncBeforePayment = (
|
|
paymentData: SendPaymentReq
|
|
): boolean => {
|
|
const { settings, implementation } = this.settingsStore;
|
|
return checkGraphSyncBeforePayment(
|
|
settings,
|
|
implementation,
|
|
paymentData,
|
|
this
|
|
);
|
|
};
|
|
|
|
@action
|
|
public hideGraphSyncPrompt = () => {
|
|
this.showGraphSyncPrompt = false;
|
|
};
|
|
|
|
@action
|
|
public proceedWithPayment = async () => {
|
|
if (this.pendingPaymentData) {
|
|
this.hideGraphSyncPrompt();
|
|
this.sendPaymentInternal(this.pendingPaymentData);
|
|
this.pendingPaymentData = null;
|
|
}
|
|
};
|
|
|
|
@action
|
|
public sendPayment = (paymentData: SendPaymentReq) => {
|
|
// Check graph sync before proceeding with payment
|
|
if (!this.checkGraphSyncBeforePayment(paymentData)) {
|
|
return;
|
|
}
|
|
|
|
return this.sendPaymentInternal(paymentData);
|
|
};
|
|
|
|
@action
|
|
private sendPaymentInternal = ({
|
|
payment_request,
|
|
amount,
|
|
pubkey,
|
|
max_parts,
|
|
max_shard_amt,
|
|
fee_limit_sat,
|
|
max_fee_percent,
|
|
outgoing_chan_id,
|
|
last_hop_pubkey,
|
|
message,
|
|
amp,
|
|
timeout_seconds
|
|
}: SendPaymentReq) => {
|
|
this.paymentStartTime = Date.now();
|
|
this.paymentDuration = null;
|
|
this.loading = true;
|
|
this.error_msg = null;
|
|
this.error = false;
|
|
this.payment_route = null;
|
|
this.payment_preimage = null;
|
|
this.isIncomplete = null;
|
|
this.payment_hash = null;
|
|
this.payment_error = null;
|
|
this.status = null;
|
|
|
|
const data: any = {};
|
|
if (payment_request) {
|
|
data.payment_request = payment_request;
|
|
}
|
|
if (amount) {
|
|
data.amt = Number(amount);
|
|
}
|
|
|
|
if (pubkey) {
|
|
const preimage = randomBytes(preimageByteLength);
|
|
const secret = preimage.toString('base64');
|
|
const payment_hash = Base64Utils.hexToBase64(sha256(preimage));
|
|
|
|
data.dest = Base64Utils.hexToBase64(pubkey);
|
|
data.dest_custom_records = { [keySendPreimageType]: secret };
|
|
data.payment_hash = payment_hash;
|
|
data.pubkey = pubkey;
|
|
|
|
if (message) {
|
|
const hex_message = Base64Utils.hexToBase64(
|
|
Base64Utils.utf8ToHex(message)
|
|
);
|
|
data.dest_custom_records![keySendMessageType] = hex_message;
|
|
}
|
|
}
|
|
|
|
// multi-path payments
|
|
data.max_parts = max_parts ? max_parts : '1';
|
|
|
|
if (fee_limit_sat) {
|
|
data.fee_limit_sat = Number(fee_limit_sat);
|
|
}
|
|
|
|
// atomic multi-path payments
|
|
if (amp) {
|
|
data.amp = true;
|
|
data.no_inflight_updates = true;
|
|
}
|
|
if (max_shard_amt) {
|
|
data.max_shard_size_msat = Number(max_shard_amt) * 1000;
|
|
}
|
|
|
|
// first hop
|
|
if (outgoing_chan_id) {
|
|
data.outgoing_chan_id = outgoing_chan_id;
|
|
}
|
|
// last hop
|
|
if (last_hop_pubkey) {
|
|
// must be base64 encoded (bytes)
|
|
data.last_hop_pubkey = Base64Utils.hexToBase64(last_hop_pubkey);
|
|
}
|
|
|
|
// Tor can't handle streaming updates
|
|
if (this.settingsStore.enableTor) {
|
|
data.no_inflight_updates = true;
|
|
}
|
|
|
|
// max fee percent for c-lightning
|
|
if (
|
|
max_fee_percent &&
|
|
this.settingsStore.implementation === 'cln-rest'
|
|
) {
|
|
data.max_fee_percent = max_fee_percent;
|
|
}
|
|
|
|
// payment timeout for LND and CLN
|
|
if (
|
|
BackendUtils.isLNDBased() ||
|
|
this.settingsStore.implementation === 'cln-rest'
|
|
) {
|
|
data.timeout_seconds = Number(timeout_seconds) || 60;
|
|
}
|
|
|
|
const payFunc =
|
|
(this.settingsStore.implementation === 'cln-rest' ||
|
|
this.settingsStore.implementation === 'embedded-lnd') &&
|
|
pubkey
|
|
? BackendUtils.sendKeysend
|
|
: BackendUtils.payLightningInvoice;
|
|
|
|
if (this.settingsStore.implementation === 'lightning-node-connect') {
|
|
return payFunc(data);
|
|
} else {
|
|
payFunc(data)
|
|
.then((response: any) => {
|
|
const result = response.result || response;
|
|
this.handlePayment(result);
|
|
})
|
|
.catch((err: Error) => {
|
|
this.handlePaymentError(err);
|
|
});
|
|
}
|
|
};
|
|
|
|
public sendPaymentSilently = async ({
|
|
payment_request,
|
|
fee_limit_sat,
|
|
max_parts,
|
|
timeout_seconds
|
|
}: {
|
|
payment_request: string;
|
|
fee_limit_sat?: number;
|
|
max_parts?: string;
|
|
timeout_seconds?: number;
|
|
}) => {
|
|
const data: any = {};
|
|
|
|
if (payment_request) {
|
|
data.payment_request = payment_request;
|
|
}
|
|
|
|
if (max_parts) {
|
|
data.max_parts = max_parts || '16';
|
|
}
|
|
|
|
// payment timeout and fee limit for LND and CLN
|
|
if (
|
|
BackendUtils.isLNDBased() ||
|
|
this.settingsStore.implementation === 'cln-rest'
|
|
) {
|
|
data.fee_limit_sat = Number(fee_limit_sat) || 100;
|
|
data.timeout_seconds = Number(timeout_seconds) || 60;
|
|
}
|
|
|
|
if (this.settingsStore.enableTor) {
|
|
data.no_inflight_updates = true;
|
|
}
|
|
|
|
const payFunc = BackendUtils.payLightningInvoice;
|
|
|
|
if (this.settingsStore.implementation === 'lightning-node-connect') {
|
|
return payFunc(data);
|
|
} else {
|
|
return payFunc(data)
|
|
.then((response: any) => {
|
|
const result = response.result || response;
|
|
return result;
|
|
})
|
|
.catch((err: any) => {
|
|
console.error('Payment error:', err);
|
|
throw err;
|
|
});
|
|
}
|
|
};
|
|
|
|
@action
|
|
public handlePayment = (result: any) => {
|
|
this.loading = false;
|
|
this.payment_route = result.payment_route;
|
|
|
|
const payment = new Payment(result);
|
|
this.noteKey = payment.getNoteKey;
|
|
this.payment_preimage = payment.getPreimage;
|
|
this.payment_hash = payment.paymentHash;
|
|
this.payment_fee = payment.getFee;
|
|
this.isIncomplete = payment.isIncomplete;
|
|
|
|
const implementation = this.settingsStore.implementation;
|
|
|
|
// TODO modify enum settings for embedded LND
|
|
const status =
|
|
implementation === 'embedded-lnd'
|
|
? lnrpc.Payment.PaymentStatus[result.status]
|
|
: result.status;
|
|
|
|
const isKeysend =
|
|
result?.htlcs?.[0]?.route?.hops?.[0]?.custom_records?.[
|
|
keySendPreimageType
|
|
] != null;
|
|
|
|
const isSuccess = status === 'complete' || status === 'SUCCEEDED';
|
|
if (isSuccess && this.paymentStartTime && !this.paymentDuration) {
|
|
this.paymentDuration = (Date.now() - this.paymentStartTime) / 1000;
|
|
}
|
|
|
|
// TODO add message for in-flight transactions
|
|
if (
|
|
(status &&
|
|
status !== 'complete' &&
|
|
status !== 'SUCCEEDED' &&
|
|
status !== 'IN_FLIGHT') ||
|
|
(status && status === 'FAILED') ||
|
|
(result.payment_error && result.payment_error !== '')
|
|
) {
|
|
this.error = true;
|
|
this.payment_error =
|
|
(implementation === 'embedded-lnd'
|
|
? errorToUserFriendly(
|
|
lnrpc.PaymentFailureReason[result.failure_reason]
|
|
? new Error(
|
|
lnrpc.PaymentFailureReason[
|
|
result.failure_reason
|
|
]
|
|
)
|
|
: result.payment_error
|
|
)
|
|
: errorToUserFriendly(
|
|
result.failure_reason,
|
|
isKeysend ? ['Keysend'] : undefined
|
|
)) || errorToUserFriendly(result.payment_error);
|
|
}
|
|
// lndhub
|
|
if (result.error) {
|
|
this.error = true;
|
|
this.error_msg = errorToUserFriendly(result.message);
|
|
} else {
|
|
this.status = result.status || 'complete';
|
|
}
|
|
};
|
|
|
|
@action
|
|
public handlePaymentError = (err: Error) => {
|
|
this.error = true;
|
|
this.loading = false;
|
|
this.error_msg =
|
|
errorToUserFriendly(err) || localeString('error.sendingPayment');
|
|
};
|
|
|
|
@action
|
|
public resetBroadcast = () => {
|
|
this.error = true;
|
|
this.loading = false;
|
|
this.broadcast_txid = '';
|
|
this.broadcast_err = null;
|
|
};
|
|
|
|
public broadcastRawTxToMempoolSpace = (raw_tx_hex: string) => {
|
|
this.resetBroadcast();
|
|
const headers = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Content-Type': 'text/plain'
|
|
};
|
|
return ReactNativeBlobUtil.fetch(
|
|
'POST',
|
|
`https://mempool.space/${
|
|
this.nodeInfoStore.nodeInfo.isTestNet ? 'testnet/' : ''
|
|
}api/tx`,
|
|
headers,
|
|
raw_tx_hex
|
|
)
|
|
.then((response: any) => {
|
|
const status = response.info().status;
|
|
const data = response.data;
|
|
if (status == 200) {
|
|
runInAction(() => {
|
|
this.loading = false;
|
|
this.broadcast_txid = data;
|
|
});
|
|
return data;
|
|
} else {
|
|
runInAction(() => {
|
|
this.broadcast_err = data;
|
|
this.loading = false;
|
|
this.error = true;
|
|
});
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
runInAction(() => {
|
|
this.broadcast_err = err.error || err.toString();
|
|
this.loading = false;
|
|
});
|
|
});
|
|
};
|
|
}
|