add price increase rejection

This commit is contained in:
Alex Bosworth 2023-05-03 16:39:03 -07:00
parent 9e9538e456
commit f9266e4efc
No known key found for this signature in database
GPG key ID: E80D2F3F311FD87E
6 changed files with 157 additions and 67 deletions

View file

@ -1,5 +1,9 @@
# Versions
## 15.5.0
- `invoice`: Add `--reject-on-amount-increase` to reject on price moves
## 15.4.3
- `accounting`: Improve `invoices` report generation time on LND 0.16.0+

3
bos
View file

@ -1043,12 +1043,14 @@ prog
.help(`Fiat rate providers: ${priceProviders.join(', ')}`)
.help('--virtual invoices cannot be used with payers who probe before pay')
.help('Only one --virtual invoice can be active at a time')
.help('--reject-on-amount-increase can only be used with --virtual invoices')
.argument('[amount]', 'Amount for invoice', STRING, '0')
.option('--for <description>', 'What is the invoice requesting payment for')
.option('--hours <expires_in>', 'Hours invoice is valid for', INT)
.option('--include-hints', 'Include the default set of hop hint channels')
.option('--node <node_name>', 'Use saved node to create invoice')
.option('--rate-provider <rate_provider>', 'Rate provider', priceProviders)
.option('--reject-on-amount-increase', 'Reject if required amount increases')
.option('--select-hints', 'Select hop hints to be added to the request')
.option('--virtual', 'Request payment over a virtual channel')
.option('--virtual-fee-rate <pm>', 'Fee rate to use on virtual channel', INT)
@ -1062,6 +1064,7 @@ prog
description: options.for,
expires_in: options.hours,
is_hinting: options.includeHints || undefined,
is_rejecting_option: !!options.rejectOnAmountIncrease || undefined,
is_selecting_hops: options.selectHints || undefined,
is_virtual: options.virtual || undefined,
lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,

View file

@ -7,29 +7,24 @@ const {getChannel} = require('ln-service');
const {getIdentity} = require('ln-service');
const {getNetwork} = require('ln-sync');
const {getNodeAlias} = require('ln-sync');
const {getPrices} = require('@alexbosworth/fiat');
const {parseAmount} = require('ln-accounting');
const {parsePaymentRequest} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const {subscribeToForwardRequests} = require('ln-service');
const getInvoiceAmount = require('./get_invoice_amount');
const signPaymentRequest = require('./sign_payment_request');
const coins = ['BTC'];
const defaultFiatRateProvider = 'coinbase';
const defaultInvoiceDescription = '';
const defaultTimeoutCheckMs = 1000 * 60 * 3;
const fiats = ['EUR', 'USD'];
const hasFiat = n => /(eur|usd)/gim.test(n);
const hoursFromNow = h => new Date(Date.now() + (h * 3600000)).toISOString();
const interval = 3000;
const {isArray} = Array;
const {isInteger} = Number;
const isNumber = n => !isNaN(n);
const mtokensAsBigUnit = n => (Number(n / BigInt(1000)) / 1e8).toFixed(8);
const networks = {btc: 'BTC', btctestnet: 'BTC', btcregtest: 'BTC'};
const parseRequest = request => parsePaymentRequest({request});
const rateAsTokens = rate => 1e10 / rate;
const times = 20 * 60 * 24;
const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
const uniq = arr => Array.from(new Set(arr));
@ -42,6 +37,7 @@ const uniq = arr => Array.from(new Set(arr));
[description]: <Invoice Description String>
[expires_in]: <Invoice Expires In Hours Number>
[is_hinting]: <Include Private Channels Bool>
[is_rejecting_option]: <Is Rejecting Amount Increases Bool>
[is_selecting_hops]: <Is Selecting Hops Bool>
[is_virtual]: <Is Using Virtual Channel for Invoice Bool>
lnd: <Authenticated LND API Object>
@ -81,6 +77,10 @@ module.exports = (args, cbk) => {
return cbk([400, 'CannotUseDefaultHintsAndAlsoSelectHints']);
}
if (!!args.is_rejecting_option && !args.is_virtual) {
return cbk([501, 'RejectingAmountChangesOnlySupportedWhenVirtual']);
}
if (!!args.is_virtual && !!args.is_hinting) {
return cbk([400, 'UsingHopHintsIsUnsupportedWithVirtualChannels']);
}
@ -114,21 +114,6 @@ module.exports = (args, cbk) => {
return cbk(null, hoursFromNow(args.expires_in));
}],
// Get the current price of BTC in USD/EUR
getFiatPrice: ['validate', ({}, cbk) => {
// Exit early when no fiat is referenced
if (!hasFiat(args.amount)) {
return cbk();
}
return getPrices({
from: args.rate_provider || defaultFiatRateProvider,
request: args.request,
symbols: [].concat(fiats),
},
cbk);
}],
// Get channels to allow for selecting individual hop hints
getChannels: ['validate', ({}, cbk) => {
// Exit early when not selecting hop hints
@ -165,48 +150,15 @@ module.exports = (args, cbk) => {
// Get wallet info
getId: ['validate', ({}, cbk) => getIdentity({lnd: args.lnd}, cbk)],
// Fiat rates
rates: [
'getFiatPrice',
'getNetwork',
({getFiatPrice, getNetwork}, cbk) =>
{
// Exit early when there is no fiat
if (!getFiatPrice) {
return cbk();
}
if (!networks[getNetwork.network]) {
return cbk([400, 'UnsupportedNetworkForFiatPriceConversion']);
}
const rates = fiats.map(fiat => {
const {rate} = getFiatPrice.tickers.find(n => n.ticker === fiat);
return {fiat, unit: rateAsTokens(rate)};
});
return cbk(null, rates);
}],
// Parse the amount
parseAmount: ['rates', ({rates}, cbk) => {
const eur = !!rates ? rates.find(n => n.fiat === 'EUR') : null;
const usd = !!rates ? rates.find(n => n.fiat === 'USD') : null;
// Variables to use in amount
const variables = {
eur: !!eur ? eur.unit : undefined,
usd: !!usd ? usd.unit : undefined,
};
try {
const {tokens} = parseAmount({variables, amount: args.amount});
return cbk(null, {tokens});
} catch (err) {
return cbk([400, 'FailedToParseAmount', {err}]);
}
// Get tokens to invoice from the amount
parseAmount: ['getNetwork', ({getNetwork}, cbk) => {
return getInvoiceAmount({
amount: args.amount,
network: getNetwork.network,
provider: args.rate_provider || defaultFiatRateProvider,
request: args.request,
},
cbk);
}],
// Select hop hint channels
@ -296,7 +248,11 @@ module.exports = (args, cbk) => {
}],
// Intercept virtual invoice forwards
interceptVirtualInvoice: ['addInvoice', ({addInvoice}, cbk) => {
interceptVirtualInvoice: [
'addInvoice',
'getNetwork',
({addInvoice, getNetwork}, cbk) =>
{
// Exit early when not intercepting the virtual forward
if (!args.is_virtual) {
return cbk();
@ -350,6 +306,32 @@ module.exports = (args, cbk) => {
return forward.reject({});
}
// Check for optionality
if (!!args.is_rejecting_option) {
try {
const {tokens} = await getInvoiceAmount({
amount: args.amount,
network: getNetwork.network,
provider: args.rate_provider || defaultFiatRateProvider,
request: args.request,
});
// Exit early and reject when the received tokens is too low
if (tokens > addInvoice.tokens) {
args.logger.error({
rejected: true,
invoice_acceptable_amount_increased: tokens,
});
return forward.reject({});
}
} catch (err) {
args.logger.error({failed_to_get_invoice_amount: err});
return forward.reject({});
}
}
args.logger.info({accepting_payment: true});
forward.settle({secret: addInvoice.secret});

View file

@ -0,0 +1,101 @@
const asyncAuto = require('async/auto');
const {getPrices} = require('@alexbosworth/fiat');
const {parseAmount} = require('ln-accounting');
const {returnResult} = require('asyncjs-util');
const defaultFiatRateProvider = 'coinbase';
const fiats = ['EUR', 'USD'];
const hasFiat = n => /(eur|usd)/gim.test(n);
const networks = {btc: 'BTC', btctestnet: 'BTC', btcregtest: 'BTC'};
const rateAsTokens = rate => 1e10 / rate;
/** Get an amount to invoice
{
amount: <Invoice Amount String>
lnd: <Authenticated LND API Object>
[provider]: <Fiat Rate Provider String>
request: <Request Function>
}
@returns via cbk or Promise
{
tokens: <Invoice Tokens Number>
}
*/
module.exports = ({amount, network, provider, request}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!amount) {
return cbk([400, 'ExpectedAmountValueToGetInvoiceAmount']);
}
if (!network) {
return cbk([400, 'ExpectedNetworkNameToGetInvoiceAmount']);
}
if (!request) {
return cbk([400, 'ExpectedRequestFunctionToGetInvoicePrice']);
}
return cbk();
},
// Get the current price of BTC in USD/EUR
getFiatPrice: ['validate', ({}, cbk) => {
// Exit early when no fiat is referenced
if (!hasFiat(amount)) {
return cbk();
}
return getPrices({
request,
from: provider || defaultFiatRateProvider,
symbols: [].concat(fiats),
},
cbk);
}],
// Fiat rates
rates: ['getFiatPrice', ({getFiatPrice}, cbk) => {
// Exit early when there is no fiat
if (!getFiatPrice) {
return cbk();
}
if (!networks[network]) {
return cbk([400, 'UnsupportedNetworkForFiatPriceConversion']);
}
const rates = fiats.map(fiat => {
const {rate} = getFiatPrice.tickers.find(n => n.ticker === fiat);
return {fiat, unit: rateAsTokens(rate)};
});
return cbk(null, rates);
}],
// Parse the amount
parseAmount: ['rates', ({rates}, cbk) => {
const eur = !!rates ? rates.find(n => n.fiat === 'EUR') : null;
const usd = !!rates ? rates.find(n => n.fiat === 'USD') : null;
// Variables to use in amount
const variables = {
eur: !!eur ? eur.unit : undefined,
usd: !!usd ? usd.unit : undefined,
};
try {
return cbk(null, {tokens: parseAmount({amount, variables}).tokens});
} catch (err) {
return cbk([400, 'FailedToParseAmount', {err}]);
}
}],
},
returnResult({reject, resolve, of: 'parseAmount'}, cbk));
});
};

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "balanceofsatoshis",
"version": "15.4.3",
"version": "15.5.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "balanceofsatoshis",
"version": "15.4.3",
"version": "15.5.0",
"license": "MIT",
"dependencies": {
"@alexbosworth/caporal": "1.4.4",

View file

@ -83,5 +83,5 @@
"postpublish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t alexbosworth/balanceofsatoshis -t alexbosworth/balanceofsatoshis:$npm_package_version --push .",
"test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/arrays/*.js test/balances/*.js test/chain/*.js test/display/*.js test/encryption/*.js test/lnd/*.js test/network/*.js test/nodes/*.js test/peers/*.js test/responses/*.js test/routing/*.js test/services/*.js test/swaps/*.js test/tags/*.js test/telegram/*.js test/wallets/*.js"
},
"version": "15.4.3"
"version": "15.5.0"
}