lightning
Version:
Lightning Network client library
275 lines (252 loc) • 10 kB
JavaScript
const {parsePaymentRequest} = require('invoices');
const rpcAttemptHtlcAsAttempt = require('./rpc_attempt_htlc_as_attempt');
const {safeTokens} = require('./../bolt00');
const emptyHash = Buffer.alloc(32).toString('hex');
const {isArray} = Array;
const msPerSecond = 1e3;
const nanoSecsPerMillisecond = BigInt(1e6);
const routePublicKeys = route => route.hops.map(n => n.public_key);
/** Payment details from RPC payment details
{
creation_date: <Creation Date Epoch Time Seconds String>
creation_time_ns: <Creation Date Epoch Time Nanoseconds String>
failure_reason: <Payment Failure Reason String>
fee_msat: <Fee Paid in Millitokens String>
fee_sat: <Fee Paid in Tokens String>
htlcs: [{
attempt_time_ns: <HTLC Sent At Epoch Time Nanoseconds String>
failure: {
[channel_update]: {
base_fee: <Base Fee Millitokens Number>
chain_hash: <Chain Hash Buffer Object>
[chan_id]: <Numeric Channel Id String>
channel_flags: <Channel Flags Number>
extra_opaque_data: <Extra Opaque Data Buffer Object>
fee_rate: <Fee Rate Number>
htlc_maximum_msat: <Maximum HTLC Millitokens Number>
htlc_minimum_msat: <Minimum HTLC Millitokens Number>
message_flags: <Message Flags Number>
signature: <Signature Buffer Object>
time_lock_delta: <CLTV Delta Number>
timestamp: <Update Epoch Time Seconds Number>
}
code: <Failure Code String>
[failure_source_index]: <Failed Hop Index Number>
height: <Height Number>
htlc_msat: <HTLC Millitokens String>
}
resolve_time_ns: <HTLC Resolved At Epoch Time Nanoseconds String>
route: {
hops: [{
amt_to_forward: <Tokens to Forward String>
amt_to_forward_msat: <Millitokens to Forward String>
chan_id: <Numeric Format Channel Id String>
chan_capacity: <Channel Capacity Number>
expiry: <Timeout Chain Height Number>
fee: <Fee in Tokens Number>
fee_msat: <Fee in Millitokens Number>
[mpp_record]: {
payment_addr: <Payment Identifier Buffer>
total_amt_msat: <Total Payment Millitokens Amount String>
}
pub_key: <Next Hop Public Key Hex String>
tlv_payload: <Has Extra TLV Data Bool>
}]
total_amt: <Total Tokens String>
total_amt_msat: <Route Total Millitokens String>
total_fees: <Route Fee Tokens String>
total_fees_msat: <Route Total Fees Millitokens String>
total_time_lock: <Route Total Timelock Number>
}
status: <HTLC Status String>
}]
payment_hash: <Preimage SHA256 Hash Hex String>
payment_index: <Payment Index String>
payment_preimage: <Payment Secret Preimage Hex String>
payment_request: <BOLT 11 Payment Request String>
status: <Payment State String>
value: <Tokens String>
value_msat: <Paid Millitokens String>
value_sat: <Paid Tokens String>
}
@throws
<Error>
@returns
{
attempts: [{
[confirmed_at]: <Payment Attempt Succeeded At ISO 8601 Date String>
created_at: <Attempt Was Started At ISO 8601 Date String>
[failed_at]: <Payment Attempt Failed At ISO 8601 Date String>
[failure]: {
code: <Error Type Code Number>
[details]: {
[channel]: <Standard Format Channel Id String>
[height]: <Error Associated Block Height Number>
[index]: <Failed Hop Index Number>
[mtokens]: <Error Millitokens String>
[policy]: {
base_fee_mtokens: <Base Fee Millitokens String>
cltv_delta: <Locktime Delta Number>
fee_rate: <Fees Charged in Millitokens Per Million Number>
[is_disabled]: <Channel is Disabled Bool>
max_htlc_mtokens: <Maximum HLTC Millitokens Value String>
min_htlc_mtokens: <Minimum HTLC Millitokens Value String>
updated_at: <Updated At ISO 8601 Date String>
}
[timeout_height]: <Error CLTV Timeout Height Number>
[update]: {
chain: <Chain Id Hex String>
channel_flags: <Channel Flags Number>
extra_opaque_data: <Extra Opaque Data Hex String>
message_flags: <Message Flags Number>
signature: <Channel Update Signature Hex String>
}
}
message: <Error Message String>
}
[confirmed_at]: <Payment Attempt Succeeded At ISO 8601 Date String>
created_at: <Attempt Was Started At ISO 8601 Date String>
[failed_at]: <Payment Attempt Failed At ISO 8601 Date String>
is_confirmed: <Payment Attempt Succeeded Bool>
is_failed: <Payment Attempt Failed Bool>
is_pending: <Payment Attempt is Waiting For Resolution Bool>
route: {
fee: <Route Fee Tokens Number>
fee_mtokens: <Route Fee Millitokens String>
hops: [{
channel: <Standard Format Channel Id String>
channel_capacity: <Channel Capacity Tokens Number>
fee: <Fee Number>
fee_mtokens: <Fee Millitokens String>
forward: <Forward Tokens Number>
forward_mtokens: <Forward Millitokens String>
[public_key]: <Forward Edge Public Key Hex String>
[timeout]: <Timeout Block Height Number>
}]
mtokens: <Total Fee-Inclusive Millitokens String>
[payment]: <Payment Identifier Hex String>
timeout: <Timeout Block Height Number>
tokens: <Total Fee-Inclusive Tokens Number>
[total_mtokens]: <Total Millitokens String>
}
}]
created_at: <Payment at ISO-8601 Date String>
[destination]: <Destination Node Public Key Hex String>
[fee]: <Paid Routing Fee Rounded Down Tokens Number>
[fee_mtokens]: <Paid Routing Fee in Millitokens String>
hops: [<First Route Hop Public Key Hex String>]
id: <Payment Preimage Hash String>
[index]: <Payment Add Index Number>
is_confirmed: <Payment is Confirmed Bool>
is_outgoing: <Transaction Is Outgoing Bool>
mtokens: <Millitokens Sent to Destination String>
[request]: <BOLT 11 Payment Request String>
[safe_fee]: <Payment Forwarding Fee Rounded Up Tokens Number>
safe_tokens: <Payment Tokens Sent to Destination Rounded Up Number>
[secret]: <Payment Preimage Hex String>
tokens: <Rounded Down Tokens Sent to Destination Number>
}
*/
module.exports = payment => {
if (!payment) {
throw new Error('ExpectedPaymentInRpcResponse');
}
if (!payment.creation_date) {
throw new Error('ExpectedCreationDateInRpcPaymentDetails');
}
if (typeof payment.fee_sat !== 'string') {
throw new Error('ExpectedPaymentFeeInRpcPaymentDetails');
}
if (!isArray(payment.htlcs)) {
throw new Error('ExpectedHtlcsArrayInRpcPaymentDetails');
}
if (!payment.payment_hash) {
throw new Error('ExpectedPaymentHashInRpcPaymentDetails');
}
if (!payment.payment_preimage) {
throw new Error('ExpectedPaymentPreimageInRpcPaymentDetails');
}
if (typeof payment.value_sat !== 'string') {
throw new Error('ExpectedPaymentValueInRpcPaymentDetails');
}
const creationDateEpochMs = (() => {
// Exit early when creation time nanoseconds is not defined
if (payment.creation_time_ns === Number().toString()) {
return Number(payment.creation_date) * msPerSecond;
}
return Number(BigInt(payment.creation_time_ns) / nanoSecsPerMillisecond);
})();
const attempts = payment.htlcs.map(htlc => rpcAttemptHtlcAsAttempt(htlc));
const index = Number(payment.payment_index) || undefined;
const request = payment.payment_request || undefined;
// Exit early when there were no attempts
if (!attempts.length) {
const {destination} = !!request ? parsePaymentRequest({request}) : {};
return {
attempts,
destination,
index,
request,
confirmed_at: undefined,
created_at: new Date(creationDateEpochMs).toISOString(),
fee: undefined,
fee_mtokens: undefined,
hops: [],
id: payment.payment_hash,
is_confirmed: false,
is_outgoing: true,
mtokens: payment.value_msat,
safe_fee: undefined,
safe_tokens: safeTokens({mtokens: payment.value_msat}).safe,
secret: undefined,
tokens: safeTokens({mtokens: payment.value_msat}).tokens,
};
}
const hasPreimage = payment.payment_preimage !== emptyHash;
const [attempt] = attempts;
const successes = attempts.filter(n => n.is_confirmed);
const [confirmedAt] = successes.map(n => n.confirmed_at).sort().reverse();
const path = routePublicKeys(attempt.route);
const [destination, ...hops] = path.reverse();
// Exit early when the payment was never settled
if (!hasPreimage) {
return {
attempts,
destination,
index,
request,
confirmed_at: undefined,
created_at: new Date(creationDateEpochMs).toISOString(),
fee: undefined,
fee_mtokens: undefined,
hops: hops.reverse(),
id: payment.payment_hash,
is_confirmed: false,
is_outgoing: true,
mtokens: payment.value_msat,
safe_fee: undefined,
safe_tokens: safeTokens({mtokens: payment.value_msat}).safe,
secret: undefined,
tokens: safeTokens({mtokens: payment.value_msat}).tokens,
};
}
return {
attempts,
destination,
index,
request,
confirmed_at: confirmedAt || undefined,
created_at: new Date(creationDateEpochMs).toISOString(),
fee: safeTokens({mtokens: payment.fee_msat}).tokens,
fee_mtokens: payment.fee_msat,
hops: hops.reverse(),
id: payment.payment_hash,
is_confirmed: true,
is_outgoing: true,
mtokens: payment.value_msat,
safe_fee: safeTokens({mtokens: payment.fee_msat}).safe,
safe_tokens: safeTokens({mtokens: payment.value_msat}).safe,
secret: payment.payment_preimage,
tokens: safeTokens({mtokens: payment.value_msat}).tokens,
};
};