ln-telegram
Version:
Lightning Network Telegram bot commands
240 lines (201 loc) • 7.31 kB
JavaScript
const asyncAuto = require('async/auto');
const {getIdentity} = require('ln-service');
const {getNodeAlias} = require('ln-sync');
const {returnResult} = require('asyncjs-util');
const {verifyBytesSignature} = require('ln-service');
const {formatTokens} = require('./../interface');
const {icons} = require('./../interface');
const bufFromHex = hex => Buffer.from(hex, 'hex');
const dash = ' - ';
const dateType = '34349343';
const escape = text => text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\\$&');
const formatAmt = tokens => formatTokens({tokens}).display;
const fromKeyType = '34349339';
const hexAsUtf8 = hex => Buffer.from(hex, 'hex').toString('utf8');
const hexFromBuf = buffer => buffer.toString('hex');
const {isArray} = Array;
const maxAnswer = BigInt(80518);
const messageType = '34349334';
const minAnswer = BigInt(80509);
const newLine = '\n';
const short = n => n.slice(0, 32);
const signatureType = '34349337';
const sort = (a, b) => (a < b) ? -1 : ((a > b) ? 1 : 0);
/** Get received message
{
description: <Invoice Description String>
lnd: <Authenticated LND API Object>
payments: [{
messages: [{
type: <Message Record TLV Type String>
value: <Message Record Value Hex String>
}]
}]
received: <Received Tokens Number>
via: {
alias: <Alias String>
id: <Identity String>
}
}
@returns via cbk or Promise
{
[icon]: <Message Icon String>
[message]: <Embedded Message Payment Received String>
[quiz]: [<Quiz Answer String>]
[title]: <Sender Message String>
}
*/
module.exports = ({description, lnd, payments, received, via}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (description === undefined) {
return cbk([400, 'ExpectedInvoiceDescriptionToGetReceiveMessage']);
}
if (!lnd) {
return cbk([400, 'ExpectedAuthenticatedLndToGetReceiveMessage']);
}
if (!isArray(payments)) {
return cbk([400, 'ExpectedArrayOfReceivePaymentsForReceiveMessage']);
}
if (!isArray(via)) {
return cbk([400, 'ExpectedArrayOfViaNodesForReceiveMessage']);
}
return cbk();
},
// Derive the message details
messageDetails: ['validate', ({}, cbk) => {
// Exit early when there are no TLV records
if (!payments.length) {
return cbk();
}
const [{messages}] = payments;
const quizAnswers = messages
.filter(({type}) => {
return BigInt(type) >= minAnswer && BigInt(type) <= maxAnswer;
})
.sort((a, b) => sort(BigInt(a.type), BigInt(b.type)))
.map(({value}) => hexAsUtf8(value));
const messageRecord = messages.find(({type}) => type === messageType);
// Exit early when there is no message type record
if (!messageRecord) {
return cbk();
}
return cbk(null, {
date: messages.find(({type}) => type === dateType),
from: messages.find(({type}) => type === fromKeyType),
message: messageRecord,
quiz: quizAnswers,
signature: messages.find(({type}) => type === signatureType),
});
}],
// Description of the received amount
receiveLine: ['validate', ({}, cbk) => {
const relays = `${via.map(n => short(n.alias || n.id)).join(', ')}`;
const quoted = !description ? '' : `for “${description}”`;
const got = `Received ${formatAmt(received)} ${quoted}`.trim();
return cbk(null, `${got} via ${relays}`);
}],
// Get the node public key for signature verification puroses
getPublicKey: ['messageDetails', ({messageDetails}, cbk) => {
if (!messageDetails || !messageDetails.signature) {
return cbk();
}
return getIdentity({lnd}, cbk);
}],
// Determine if the message signature is valid
isSignatureValid: [
'getPublicKey',
'messageDetails',
({getPublicKey, messageDetails}, cbk) =>
{
// Exit early when there is no signed message record
if (!messageDetails || !messageDetails.signature) {
return cbk();
}
// Exit early when the message signing details are not set
if (!messageDetails.from || !messageDetails.date) {
return cbk();
}
const preimage = Buffer.concat([
bufFromHex(messageDetails.from.value),
bufFromHex(getPublicKey.public_key),
bufFromHex(messageDetails.date.value),
bufFromHex(messageDetails.message.value),
]);
return verifyBytesSignature({
lnd,
preimage: hexFromBuf(preimage),
public_key: messageDetails.from.value,
signature: messageDetails.signature.value,
},
(err, res) => {
// Ignore errors
if (!!err) {
return cbk();
}
return cbk(null, !!res.is_valid);
});
}],
// Get the "from" node alias
getFromNode: ['messageDetails', ({messageDetails}, cbk) => {
// Exit early when there is no from node to look up
if (!messageDetails || !messageDetails.from) {
return cbk();
}
return getNodeAlias({lnd, id: messageDetails.from.value}, cbk);
}],
// Received message
receivedMessage: [
'getFromNode',
'isSignatureValid',
'messageDetails',
'receiveLine',
({getFromNode, isSignatureValid, messageDetails, receiveLine}, cbk) =>
{
// Exit early when there is no associated message
if (!messageDetails || !messageDetails.message) {
return cbk(null, receiveLine);
}
const senderMsg = bufFromHex(messageDetails.message.value).toString();
const senderLine = `Sender message: “${senderMsg}”`;
// Exit early when there is no from key
if (!messageDetails.from) {
return cbk(null, [receiveLine, senderLine].join(dash));
}
const from = getFromNode.alias || getFromNode.id;
// Exit early when there is a from key but it's not verified
if (!isSignatureValid) {
const fromLine = `Marked as from: ${from} (unverified/unsigned)`;
return cbk(null, [receiveLine, senderLine, fromLine].join(dash));
}
const signedLine = `From: ${from}`;
return cbk(null, [receiveLine, senderLine, signedLine].join(dash));
}],
// Final received message
message: [
'messageDetails',
'receivedMessage',
({messageDetails, receivedMessage}, cbk) =>
{
if (!receivedMessage) {
return cbk(null, {});
}
if (!messageDetails) {
return cbk(null, {
icon: icons.receive,
message: escape(receivedMessage)
});
}
return cbk(null, {
icon: icons.receive,
message: escape(receivedMessage),
quiz: messageDetails.quiz,
title: bufFromHex(messageDetails.message.value).toString(),
});
}],
},
returnResult({reject, resolve, of: 'message'}, cbk));
});
};