stacker.news/lib/cln.js

170 lines
4.3 KiB
JavaScript

import fetch from 'cross-fetch'
import crypto from 'crypto'
import { getAgent } from '@/lib/proxy'
import { assertContentTypeJson } from './url'
export const createInvoice = async ({ socket, rune, cert, label, description, msats, expiry }) => {
const agent = getAgent({ hostname: socket, cert })
const url = `${agent.protocol}//${socket}/v1/invoice`
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Rune: rune,
// can be any node id, only required for CLN v23.08 and below
// see https://docs.corelightning.org/docs/rest#server
nodeId: '02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490'
},
agent,
body: JSON.stringify({
// CLN requires a unique label for every invoice
// see https://docs.corelightning.org/reference/lightning-invoice
label: crypto.randomBytes(16).toString('hex'),
description,
amount_msat: msats,
expiry
})
})
if (!res.ok) {
assertContentTypeJson(res)
}
const inv = await res.json()
if (inv.error) {
throw new Error(inv.error.message)
}
return inv
}
// https://github.com/clams-tech/rune-decoder/blob/57c2e76d1ef9ab7336f565b99de300da1c7b67ce/src/index.ts
export const decodeRune = (rune) => {
const runeBinary = Base64Binary.decode(rune)
const hashBinary = runeBinary.slice(0, 32)
const hash = binaryHashToHex(hashBinary)
const restBinary = runeBinary.slice(32)
const [uniqueId, ...restrictionStrings] = new TextDecoder().decode(restBinary).split('&')
const id = uniqueId.split('=')[1]
// invalid rune checks
if (!id) return null
if (restrictionStrings.some(invalidAscii)) return null
const restrictions = restrictionStrings.map((restriction) => {
const alternatives = restriction.split('|')
const summary = alternatives.reduce((str, alternative) => {
const [operator] = alternative.match(runeOperatorRegex) || []
if (!operator) return str
const [name, value] = alternative.split(operator)
return `${str ? `${str} OR ` : ''}${name} ${operatorToDescription(operator)} ${value}`
}, '')
return {
alternatives,
summary
}
})
return {
id,
hash,
restrictions
}
}
const Base64Binary = {
_keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
removePaddingChars: function (input) {
const lkey = this._keyStr.indexOf(input.charAt(input.length - 1))
if (lkey === 64) {
return input.substring(0, input.length - 1)
}
return input
},
decode: function (input) {
// get last chars to see if are valid
input = this.removePaddingChars(input)
input = this.removePaddingChars(input)
const bytes = parseInt(((input.length / 4) * 3).toString(), 10)
let chr1, chr2, chr3
let enc1, enc2, enc3, enc4
let i = 0
let j = 0
const uarray = new Uint8Array(bytes)
for (i = 0; i < bytes; i += 3) {
// get the 3 octects in 4 ascii chars
enc1 = this._keyStr.indexOf(input.charAt(j++))
enc2 = this._keyStr.indexOf(input.charAt(j++))
enc3 = this._keyStr.indexOf(input.charAt(j++))
enc4 = this._keyStr.indexOf(input.charAt(j++))
chr1 = (enc1 << 2) | (enc2 >> 4)
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2)
chr3 = ((enc3 & 3) << 6) | enc4
uarray[i] = chr1
if (enc3 !== 64) uarray[i + 1] = chr2
if (enc4 !== 64) uarray[i + 2] = chr3
}
return uarray
}
}
function i2hex (i) {
return ('0' + i.toString(16)).slice(-2)
}
const binaryHashToHex = (hash) => {
return hash.reduce(function (memo, i) {
return memo + i2hex(i)
}, '')
}
const runeOperatorRegex = /[=^$/~<>{}#!]/g
const operatorToDescription = (operator) => {
switch (operator) {
case '=':
return 'is equal to'
case '^':
return 'starts with'
case '$':
return 'ends with'
case '/':
return 'is not equal to'
case '~':
return 'contains'
case '<':
return 'is less than'
case '>':
return 'is greater than'
case '{':
return 'sorts before'
case '}':
return 'sorts after'
case '#':
return 'comment'
case '!':
return 'is missing'
default:
return ''
}
}
const invalidAscii = (str) => !![...str].some((char) => char.charCodeAt(0) > 127)