Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return all route hints in tagsObject #74

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion payreq.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export declare type TagsObject = {
expire_time?: number;
min_final_cltv_expiry?: number;
fallback_address?: FallbackAddress;
routing_info?: RoutingInfo;
routing_info?: RoutingInfo[];
feature_bits?: FeatureBits;
unknownTags?: UnknownTag[];
};
Expand Down
70 changes: 42 additions & 28 deletions payreq.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,14 +395,20 @@ function purposeCommitEncoder (data) {
return bech32.toWords(buffer)
}

function tagsItems (tags, tagName) {
function tagsItem (tags, tagName) {
const tag = tags.filter(item => item.tagName === tagName)
const data = tag.length > 0 ? tag[0].data : null
return data
}

function tagsItems (tags, tagName) {
const tag = tags.filter(item => item.tagName === tagName)
const data = tag.length > 0 ? tag.map((t) => t.data) : null
bufo24 marked this conversation as resolved.
Show resolved Hide resolved
return data
}

function tagsContainItem (tags, tagName) {
return tagsItems(tags, tagName) !== null
return tagsItem(tags, tagName) !== null
}

function orderKeys (unorderedObj, forDecode) {
Expand Down Expand Up @@ -510,7 +516,7 @@ function sign (inputPayReqObj, inputPrivateKey) {
let nodePublicKey, tagNodePublicKey
// If there is a payee_node_key tag convert to buffer
if (tagsContainItem(payReqObj.tags, TAGNAMES['19'])) {
tagNodePublicKey = hexToBuffer(tagsItems(payReqObj.tags, TAGNAMES['19']))
tagNodePublicKey = hexToBuffer(tagsItem(payReqObj.tags, TAGNAMES['19']))
}
// If there is payeeNodeKey attribute, convert to buffer
if (payReqObj.payeeNodeKey) {
Expand Down Expand Up @@ -610,7 +616,7 @@ function encode (inputData, addDefaults) {
throw new Error('Payment request requires feature bits with at least payment secret support flagged if payment secret is included')
}
} else {
const fB = tagsItems(data.tags, TAGNAMES['5'])
const fB = tagsItem(data.tags, TAGNAMES['5'])
if (!fB.payment_secret || (!fB.payment_secret.supported && !fB.payment_secret.required)) {
throw new Error('Payment request requires feature bits with at least payment secret support flagged if payment secret is included')
}
Expand All @@ -631,7 +637,7 @@ function encode (inputData, addDefaults) {
// If a description exists, check to make sure the buffer isn't greater than
// 639 bytes long, since 639 * 8 / 5 = 1023 words (5 bit) when padded
if (tagsContainItem(data.tags, TAGNAMES['13']) &&
Buffer.from(tagsItems(data.tags, TAGNAMES['13']), 'utf8').length > 639) {
Buffer.from(tagsItem(data.tags, TAGNAMES['13']), 'utf8').length > 639) {
throw new Error('Description is too long: Max length 639 bytes')
}

Expand All @@ -655,7 +661,7 @@ function encode (inputData, addDefaults) {

let nodePublicKey, tagNodePublicKey
// If there is a payee_node_key tag convert to buffer
if (tagsContainItem(data.tags, TAGNAMES['19'])) tagNodePublicKey = hexToBuffer(tagsItems(data.tags, TAGNAMES['19']))
if (tagsContainItem(data.tags, TAGNAMES['19'])) tagNodePublicKey = hexToBuffer(tagsItem(data.tags, TAGNAMES['19']))
// If there is payeeNodeKey attribute, convert to buffer
if (data.payeeNodeKey) nodePublicKey = hexToBuffer(data.payeeNodeKey)
if (nodePublicKey && tagNodePublicKey && !tagNodePublicKey.equals(nodePublicKey)) {
Expand All @@ -668,7 +674,7 @@ function encode (inputData, addDefaults) {
let code, addressHash, address
// If there is a fallback address tag we must check it is valid
if (tagsContainItem(data.tags, TAGNAMES['9'])) {
const addrData = tagsItems(data.tags, TAGNAMES['9'])
const addrData = tagsItem(data.tags, TAGNAMES['9'])
// Most people will just provide address so Hash and code will be undefined here
address = addrData.address
addressHash = addrData.addressHash
Expand Down Expand Up @@ -714,37 +720,37 @@ function encode (inputData, addDefaults) {
}

// If there is route info tag, check that each route has all 4 necessary info
if (tagsContainItem(data.tags, TAGNAMES['3'])) {
const routingInfo = tagsItems(data.tags, TAGNAMES['3'])
routingInfo.forEach(route => {
if (route.pubkey === undefined ||
route.short_channel_id === undefined ||
route.fee_base_msat === undefined ||
route.fee_proportional_millionths === undefined ||
route.cltv_expiry_delta === undefined) {
const routingInfo = tagsItems(data.tags, TAGNAMES['3'])
routingInfo && routingInfo.forEach(route => {
route.forEach(hop => {
if (hop.pubkey === undefined ||
hop.short_channel_id === undefined ||
hop.fee_base_msat === undefined ||
hop.fee_proportional_millionths === undefined ||
hop.cltv_expiry_delta === undefined) {
throw new Error('Routing info is incomplete')
}
if (!secp256k1.publicKeyVerify(hexToBuffer(route.pubkey))) {
if (!secp256k1.publicKeyVerify(hexToBuffer(hop.pubkey))) {
throw new Error('Routing info pubkey is not a valid pubkey')
}
const shortId = hexToBuffer(route.short_channel_id)
const shortId = hexToBuffer(hop.short_channel_id)
if (!(shortId instanceof Buffer) || shortId.length !== 8) {
throw new Error('Routing info short channel id must be 8 bytes')
}
if (typeof route.fee_base_msat !== 'number' ||
Math.floor(route.fee_base_msat) !== route.fee_base_msat) {
if (typeof hop.fee_base_msat !== 'number' ||
Math.floor(hop.fee_base_msat) !== hop.fee_base_msat) {
throw new Error('Routing info fee base msat is not an integer')
}
if (typeof route.fee_proportional_millionths !== 'number' ||
Math.floor(route.fee_proportional_millionths) !== route.fee_proportional_millionths) {
if (typeof hop.fee_proportional_millionths !== 'number' ||
Math.floor(hop.fee_proportional_millionths) !== hop.fee_proportional_millionths) {
throw new Error('Routing info fee proportional millionths is not an integer')
}
if (typeof route.cltv_expiry_delta !== 'number' ||
Math.floor(route.cltv_expiry_delta) !== route.cltv_expiry_delta) {
if (typeof hop.cltv_expiry_delta !== 'number' ||
Math.floor(hop.cltv_expiry_delta) !== hop.cltv_expiry_delta) {
throw new Error('Routing info cltv expiry delta is not an integer')
}
})
}
})

let prefix = 'ln'
prefix += coinTypeObj.bech32
Expand Down Expand Up @@ -843,7 +849,7 @@ function encode (inputData, addDefaults) {
if (sigWords) dataWords = dataWords.concat(sigWords)

if (tagsContainItem(data.tags, TAGNAMES['6'])) {
data.timeExpireDate = data.timestamp + tagsItems(data.tags, TAGNAMES['6'])
data.timeExpireDate = data.timestamp + tagsItem(data.tags, TAGNAMES['6'])
data.timeExpireDateString = new Date(data.timeExpireDate * 1000).toISOString()
}
data.timestampString = new Date(data.timestamp * 1000).toISOString()
Expand Down Expand Up @@ -972,14 +978,14 @@ function decode (paymentRequest, network) {
// be kind and provide an absolute expiration date.
// good for logs
if (tagsContainItem(tags, TAGNAMES['6'])) {
timeExpireDate = timestamp + tagsItems(tags, TAGNAMES['6'])
timeExpireDate = timestamp + tagsItem(tags, TAGNAMES['6'])
timeExpireDateString = new Date(timeExpireDate * 1000).toISOString()
}

const toSign = Buffer.concat([Buffer.from(prefix, 'utf8'), Buffer.from(convert(wordsNoSig, 5, 8))])
const payReqHash = sha256(toSign)
const sigPubkey = Buffer.from(secp256k1.ecdsaRecover(sigBuffer, recoveryFlag, payReqHash, true))
if (tagsContainItem(tags, TAGNAMES['19']) && tagsItems(tags, TAGNAMES['19']) !== sigPubkey.toString('hex')) {
if (tagsContainItem(tags, TAGNAMES['19']) && tagsItem(tags, TAGNAMES['19']) !== sigPubkey.toString('hex')) {
throw new Error('Lightning Payment Request signature pubkey does not match payee pubkey')
}

Expand Down Expand Up @@ -1011,18 +1017,26 @@ function decode (paymentRequest, network) {
}

function getTagsObject (tags) {
const result = {}
const result = {
[TAGNAMES['3']]: []
}
tags.forEach(tag => {
if (tag.tagName === unknownTagName) {
if (!result.unknownTags) {
result.unknownTags = []
}
result.unknownTags.push(tag.data)
} else if (tag.tagName === TAGNAMES['3']) {
result[tag.tagName].push(tag.data)
} else {
result[tag.tagName] = tag.data
}
})

if (result[TAGNAMES['3']].length === 0) {
delete result[TAGNAMES['3']]
}

return result
}

Expand Down
48 changes: 47 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,11 @@ fixtures.decode.valid.forEach((f) => {
const data = decoded.tagsObject[key]
const tagsData = decoded.tags.filter(item => item.tagName === key)
t.assert(tagsData.length === 1)
t.same(data, tagsData[0].data)
if (key === 'routing_info') {
t.same(data, tagsData.map((t) => t.data))
} else {
t.same(data, tagsData[0].data)
}
})

t.end()
Expand Down Expand Up @@ -323,3 +327,45 @@ tape('can encode and decode small timestamp', (t) => {
t.same(reEncoded.paymentRequest, signedData.paymentRequest)
t.end()
})

tape('can decode multiple route hints', (t) => {
const decoded = lnpayreq.decode('lnbc21n1pnyc64fpp55h2xw2y9ygl80zh5zrwf3sz' +
'skqshyvm2zzvrk7h7fxay3k0e7t9shp5jgmctxuhd' +
'clx5w4cfgr70v362tc6zn4e9h8s3tllv2j0e970fw' +
'ssxqyjw5qrzjqg6khjn0d0ed5vltq8jtw49fl3t42' +
'5hg0n7hvkynk70c8xplatfszyqsqyqqzqgpqyqqqq' +
'lgqqqqqqgqjqrzjqtc63jrkql6ptj8j9sq9jvqzwa' +
'v5rh4y3p5uugcfdte8kr8aes9kjyqsqyqqzqgpqyq' +
'qqqlgqqqqqqgqjqcqzzssp5yc07wjjnc3t9w3w5sn' +
'y6hmnq320hfqse7zsv4n3k97mujheekh7snp4qwxw' +
'ehlkj9awmypclkf06agf3u64sy7e4p6uvkk8dfja8' +
'7st8rvtz9qypqsq50ne8s8dyc04a373fhc7mcllh5' +
'ycsvj7mek8zg9w7h5kzez6u308m8ld22dggg4fysl' +
'w67jjnd94hgml0w2dw4dq7n4623n0ce6hd9sqs9cxyt'
)
t.assert(decoded.tagsObject.routing_info.length === 2)
t.assert(decoded.tagsObject.routing_info[0].length === 1)
t.assert(decoded.tagsObject.routing_info[1].length === 1)
t.end()
})

tape('can decode route hints with multiple hops', (t) => {
const decoded = lnpayreq.decode('lnbc21n1pnycmvgpp5eez89m3mmjtjxz99fv7nam' +
'faagjecw9k7xvmmsd7dg0thay8jx5shp5jgmctxu' +
'hdclx5w4cfgr70v362tc6zn4e9h8s3tllv2j0e97' +
'0fwssxqyjw5qr9yqg6khjn0d0ed5vltq8jtw49fl' +
'3t425hg0n7hvkynk70c8xplatfszyqsqyqqzqgpq' +
'yqqqqlgqqqqqqgqjqpr2672da4l9k3navq7fd654' +
'879w42jap706ajcjwmelquc8l4dxqgszqqsqqgpq' +
'ypqqqqraqqqqqqpqzgqcqzzssp5f2twpv6mkaygp' +
'w0qpqytl3m0x76thftrw4gqzp05f084susptxpqn' +
'p4qgkap5y72ewa6s08e65huc4fexryccla2906zm' +
'txxwwd9dvvlrwq79qypqsquzmg28e4g06d59t5dz' +
'hv7ms242w9uf8rkf4rsnf4495j27k6wmzqfgyvqy' +
'he6alhhrtwg7djvpvfkf62wefatmnm6sutnwpkzj' +
'd750qpyeman0'
)
t.assert(decoded.tagsObject.routing_info.length === 1)
t.assert(decoded.tagsObject.routing_info[0].length === 2)
t.end()
})
Loading