bolt12-test
Version:
Decode bolt12 type address for the lightning networks.
448 lines (406 loc) • 14.8 kB
JavaScript
var assert = require('assert');
const schnorr = require('bip-schnorr');
const {
tlv_tlv_payload,
tlv_offer,
tlv_invoice_request,
tlv_invoice,
tlv_invoice_error
}=require('./gencode.js')
const sha256 = require('js-sha256');
var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
const concat = Buffer.concat;
const {
towire_bigsize,
fromwire_bigsize,
}=require('./fundamental_types')
const ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
const isBech32={};
const ALPHABET_MAP = {};
class Recurrence{
constructor(recurrence, recurrence_paywindow=null, recurrence_limit=null, recurrence_base=null){
this.period = recurrence['period'];
this.time_unit = recurrence['time_unit'];
this.paywindow = recurrence_paywindow;
this.limit = recurrence_limit;
this.base = recurrence_base;
}
has_fixed_base() {
return this.base!=null
}
can_start_offset(){
return this.has_fixed_base && this.base['start_any_period']
}
_adjust_date(basedate, units, n, sameday){
while(true){
if (units == 'seconds'){
var ret=new Date(basedate)
ret.setSeconds(ret.getSeconds()+n);
}
else if (units == 'days'){
var ret=new Date(basedate)
ret.setDate(ret.getDate()+n);
}
else if (units == 'months'){
var ret=new Date(basedate)
ret.setMonth(ret.getMonth()+n);
}
else if (units == 'years'){
var ret=new Date(basedate)
ret.setMonth(ret.getMonth()+12*n);
}
if (!sameday || ret.getDate()==basedate.getDate())
return ret;
basedate.setDate(basedate.getDate()-1);
}
}
_get_period(n, basetime){
var basedate=new Date();
basedate.setTime(basetime*1000)
// console.log(basedate.toUTCString());
if (this.time_unit==0){
var units = 'seconds';
var sameday = false;
}
else if (this.time_unit==1){
var units = 'days';
var sameday = false;
}
else if (this.time_unit==2){
var units = 'months';
var sameday = true;
}
else if (this.time_unit==3){
var units = 'years';
var sameday = true;
}
var startdate = new Date();
startdate.setTime( this._adjust_date(basedate, units, this.period*(n), sameday));
var enddate = new Date();
enddate.setTime( this._adjust_date(startdate, units, this.period, sameday));
// console.log(enddate.toUTCString());
var start = startdate.getTime();
var end = enddate.getTime();
if(this.paywindow==null){
var paystartdate = this._adjust_date(startdate, units, -this.period,
sameday);
// console.log('paystart:', paystartdate.toUTCString());
var paystart=paystartdate.getTime();
var payend=end;
}
else{
var paystart = (start) - Number(this.paywindow['seconds_before']);
var payend = (start) + Number(this.paywindow['seconds_after']);
}
return {
'start': start/1000,
'end': end/1000,
'paystart':paystart/1000,
'payend':payend/1000
}
}
get_period(n){
if (this.limit!=null && n>this.limit && n<=0){
return null;
}
if (this.base!=null){
var basetime=this.base['basetime'];
}
return this._get_period(n, basetime);
}
get_pay_factor(period, time){
if(this.paywindow==null || !this.paywindow['proportional_amount'])
return 1;
if(time<period['start'])
return 1;
if (time > period['end'])
return 0;
return (period['end']-time)/period['end']-period['start'];
}
period_start_offset(when){
if(this.can_start_offset){
if (this.time_unit==0){
var approx_mul = 1;
}
else if (this.time_unit==1){
var approx_mul = 24 * 60 * 60;
}
else if (this.time_unit==2){
var approx_mul = 30 * 24 * 60 * 60;
}
else if (this.time_unit==3){
var approx_mul = 365 * 30 * 24 * 60 * 60;
}
var period_num=(when-self.base['basetime'])/(self.period * approx_mul)
while (true){
period = self._get_period(period_num, self.base['basetime']);
if(when < period['end']){
period_num -= 1;
}
else if (when >= this.period.end){
period_num += 1;
}
else{
return period_num;
}
}
}
else throw Error('can_start_offset is not true');
}
}
for (let z = 0; z < ALPHABET.length; z++) {
const x = ALPHABET.charAt(z);
ALPHABET_MAP[x] = z;
isBech32[x]=true;
}
function hash(buffer) {
return Buffer.from(sha256.create().update(buffer).array());
}
function taggedHash(tag, msg) {
const tagHash = hash(tag);
return hash(concat([tagHash, tagHash, Buffer.from(msg,'hex')]));
}
function convert (data, inBits, outBits) {
let value = 0
let bits = 0
const maxV = (1 << outBits) - 1
const result = []
for (let i = 0; i < data.length; ++i) {
value = (value << inBits) | data[i]
bits += inBits
while (bits >= outBits) {
bits -= outBits
result.push((value >> bits) & maxV)
}
}
if (bits > 0) {
result.push((value << (outBits - bits)) & maxV)
}
return result
}
function leaves(list_of_nodes){
parents=[]
if(list_of_nodes.length % 2==0){
for(let i=0;i<list_of_nodes.length;i+=2){
smallerSHA256=list_of_nodes[i]<list_of_nodes[i+1]?list_of_nodes[i]:list_of_nodes[i+1]
greaterSHA256=list_of_nodes[i]>list_of_nodes[i+1]?list_of_nodes[i]:list_of_nodes[i+1]
parents.push(taggedHash(Buffer.from('LnBranch'),smallerSHA256+greaterSHA256).toString('hex'))
}
}
else{
for(let i=0;i<list_of_nodes.length-1;i+=2){
smallerSHA256=list_of_nodes[i]<list_of_nodes[i+1]?list_of_nodes[i]:list_of_nodes[i+1]
greaterSHA256=list_of_nodes[i]>list_of_nodes[i+1]?list_of_nodes[i]:list_of_nodes[i+1]
parents.push(taggedHash(Buffer.from('LnBranch'),smallerSHA256+greaterSHA256).toString('hex'))
}
parents.push(list_of_nodes[list_of_nodes.length-1])
}
return parents;
}
function branch_from_tlv(alltlv, tlv){
l=taggedHash(Buffer.from('LnLeaf'),tlv).toString('hex')
lnonce=taggedHash(concat([Buffer.from('LnAll'), Buffer.from(alltlv,'hex')]),tlv).toString('hex')
smallerSHA256=l<lnonce?l:lnonce
greaterSHA256=l>lnonce?l:lnonce
return taggedHash(Buffer.from('LnBranch'),smallerSHA256+greaterSHA256).toString('hex')
}
function check_sign(msgname, merkle_root, pubkey32, bip340sig){
let msg=taggedHash(Buffer.from('lightning'+msgname+'signature'),merkle_root).toString('hex');
try{
schnorr.verify(
Buffer.from(pubkey32,'hex'),
Buffer.from(msg,'hex'),
Buffer.from(bip340sig,'hex')
)
return 'true';
}
catch(e){
return 'false';
}
}
function merkle_calc(tlv){
let alltlvs=''
for(let i=0;i<tlv.length;i++)
alltlvs+=tlv[i]
let merkle_nodes=[]
for(let i=0;i<tlv.length;i++)merkle_nodes[merkle_nodes.length]=branch_from_tlv(alltlvs,tlv[i])
while(merkle_nodes.length!=1){
merkle_nodes=leaves(merkle_nodes)
}
return merkle_nodes[0]
}
function decode(paymentReq){
if (typeof paymentReq !== 'string')
throw new Error('Lightning Payment Request must be string');
let paymentRequest='';
for(let i=0;i<paymentReq.length;i++){
if(paymentReq[i]=='\n'||paymentReq[i]=='\r'){
paymentRequest+=' ';
continue;
}
else paymentRequest+=paymentReq[i];
}
for(let i=0;i<paymentRequest.length;i++){
if(paymentRequest[i]=='+'){
let s=i,e;
i++;
while(i<paymentRequest.length && paymentRequest[i]==' '){
i++;
}
e=i;
if(e==paymentRequest.length || s==0 || paymentRequest.charAt(s-1) in isBech32==false || paymentRequest.charAt(e) in isBech32==false)
throw new Error('Lightning Payment Request must be string')
paymentRequest=paymentRequest.slice(0,s)+paymentRequest.slice(e);
}
}
if(paymentRequest.indexOf(' ')!=-1)
throw new Error('Lightning Payment Request must be string');
if(paymentRequest!=paymentRequest.toLowerCase()&&paymentRequest!=paymentRequest.toUpperCase())
throw new Error('Lightning Payment Request must be string');
paymentRequest=paymentRequest.toLowerCase();
if (paymentRequest.slice(0, 2) != 'ln')
throw new Error('Not a proper lightning payment request');
if (paymentRequest.indexOf('1')==-1)
throw new Error('Separator not present');
encodedData=paymentRequest.slice(paymentRequest.lastIndexOf('1')+1);
prefix=paymentRequest.slice(0,paymentRequest.lastIndexOf('1'));
for(let i=0; i<encodedData.length; i++){
if(encodedData.charAt(i) in isBech32==false)
throw new Error('Not a proper lightning pay request');
}
let words=[]
switch(prefix){
case "lno":
type = "lno"
TAGPARSER = tlv_offer
break;
case "lnr":
type = "lnr"
TAGPARSER = tlv_invoice_request
break;
case "lni":
type = "lni"
TAGPARSER = tlv_invoice
break;
default:
throw new Error(prefix.toString() + ' is not a proper lightning prefix')
}
for (let i=0;i<encodedData.length;i++){
words[words.length]=ALPHABET_MAP[encodedData.charAt(i)]
}
words_8bit=convert(words,5,8)
const tags = [];
const final={};
const fin_content={};
const unknowns={};
const tgcode=[]
final['string']=paymentRequest;
final['type']=type;
buffer= (Buffer.from(words_8bit));
if(words.length * 5 % 8 != 0){
buffer=buffer.slice(0,-1);
}
while(buffer.length){
let tlvs=[];
let res=fromwire_bigsize(buffer);
const tagCode=res[0];
if(tgcode.length>0)
if(tagCode<=tgcode[tgcode.length-1])
throw Error('TLVs should be in ascending order!');
tgcode.push(tagCode);
tlvs.push(Number(''+tagCode));
buffer=res[1];
res=fromwire_bigsize(buffer);
tagLength = res[0];
tlvs.push(Number(''+tagLength));
buffer = res[1];
tagWords = buffer.slice(0, Number(''+tagLength));
if (tagCode in TAGPARSER){
tagName=TAGPARSER[tagCode][0];
fin_content[tagName] = TAGPARSER[tagCode][2](Buffer.from(tagWords));
}
else if (Number(tagCode)%2==1){
unknowns[tagCode]=tagWords;
fin_content[tagcode]=tagWords.toString('hex');
}
else{
throw Error('Invalid: Unknown even field number '+tagCode);
}
tlvs.push(tagWords);
buffer=buffer.slice(Number(''+tagLength))
if(tagCode<240||tagCode>1000)
tags.push(Buffer.concat([Buffer.from(tlvs.slice(0,2)),tlvs[2]]).toString('hex'));
}
final['offer_id']=merkle_calc(tags);
if(!('description' in fin_content)){
throw Error('missing description')
}
if(!('node_id' in fin_content)){
throw Error('missing node_id')
}
final['valid']='true'
if('signature' in fin_content){
try{
check_sign(prefix=='lno'?'offer':(prefix=='lni'?'invoice':(prefix=='lnr'?'invoice_request':'bad prefix!')),
final['offer_id'],
fin_content['node_id'],
fin_content['signature'])
}
catch(e){
throw Error('Bad Signature');
}
}
final['contents']=fin_content;
return final;
}
function get_recurrence(address){
let decoded=decode(address);
if (!'recurrence' in decoded['contents']){
return null;
}
else{
recur=new Recurrence(decoded['contents']['recurrence'],
decoded['contents']['recurrence_paywindow'],
decoded['contents']['recurrence_limit'],
decoded['contents']['recurrence_base']
)
return recur;
}
}
function fetch_invoice(offer,amount=null,quantity=null,payerkey=null,counter=null){
let request= new XMLHttpRequest();
let decoded=decode(offer);
if(decoded['contents']['recurrence']==undefined ){
let link='https://bootstrap.bolt12.org/fetchinvoice/'
+(offer+'/')
+(amount!=null?amount+'/':'')
+(quantity!=null?quantity:'');
request.open('GET',link);
request.send();
request.resposeType='json'
request.onload=()=>{
if(request.status==200){
console.log(JSON.parse(request.responseText));
}
else{
console.log(`error ${request.status} \n ${request.responseText}`);
}
}
}
else{
}
}
let tlv_invoice_request_rev={};
for(const [key,value] of Object.entries(tlv_invoice_request)){
tlv_invoice_request_rev[value[0]]=Number(key);
}
function invoice_request(offer){
let resDict={};
resDict[tlv_invoice_request_rev['offer_id']] = tlv_invoice_request[tlv_invoice_request_rev['offer_id']][1](offer['offer_id']);
console.log(resDict);
}
module.exports={
decode,
get_recurrence,
fetch_invoice
}