remove hot-formula-parser dependency

This commit is contained in:
Alex Bosworth 2026-07-24 13:03:30 -07:00
parent 4239ba093d
commit 3eb9ff7ff3
No known key found for this signature in database
GPG key ID: E80D2F3F311FD87E
14 changed files with 662 additions and 432 deletions

View file

@ -1,5 +1,12 @@
# Versions
## 23.0.0
### Breaking Changes
- Remove support for extended range of formulas, supported formulas are here:
https://github.com/alexbosworth/formulas#custom-functions
## 22.1.7
- Add support for LND 0.21.1

View file

@ -564,8 +564,11 @@ alias bosd="docker run -d --rm -v $HOME/.bos:/home/node/.bos alexbosworth/balanc
Some commands take formula arguments. Formulas are expressions that allow you
to perform functions and reference variables.
There is a dynamic playground here where you can play with expressions:
https://formulajs.info/functions/
Formulas are modeled after common spreadsheet formulas, but there is not a 1:1
mapping of supported functions from all spreadsheets.
See supported formula construction here:
https://github.com/alexbosworth/formulas#custom-functions
### `--avoid`

View file

@ -1,16 +0,0 @@
const {parseErrors} = require('./constants');
const defaultError = '#ERROR!';
/** Describe a hot formula parser error
{
error: <Error Type String>
}
@returns
<Display String>
*/
module.exports = ({error}) => {
return parseErrors[error] || parseErrors[defaultError];
};

View file

@ -1,6 +1,5 @@
const chartAliasForPeer = require('./chart_alias_for_peer');
const describeConfidence = require('./describe_confidence');
const describeParseError = require('./describe_parse_error');
const describeRoute = require('./describe_route');
const describeRoutingFailure = require('./describe_routing_failure');
const formatFeeRate = require('./format_fee_rate');
@ -12,7 +11,6 @@ const sumsForSegment = require('./sums_for_segment');
module.exports = {
chartAliasForPeer,
describeConfidence,
describeParseError,
describeRoute,
describeRoutingFailure,
formatFeeRate,

View file

@ -1,10 +1,7 @@
const {Parser} = require('hot-formula-parser');
const describeParseError = require('./describe_parse_error');
const {evaluateFormula} = require('@alexbosworth/formulas');
const {assign} = Object;
const defaultVariables = {btc: 1e8, k: 1e3, m: 1e6, mm: 1e6};
const {keys} = Object;
/** Determine if variables are consistent with filters
@ -32,25 +29,14 @@ module.exports = ({filters, variables}) => {
const vars = {};
[defaultVariables, variables].forEach(n => assign(vars, n));
[defaultVariables, variables].forEach(variable => assign(vars, variable));
const filtered = filters.map(formula => {
const parser = new Parser();
keys(vars).forEach(key => {
parser.setVariable(key.toLowerCase(), vars[key]);
parser.setVariable(key.toUpperCase(), vars[key]);
return;
});
const parsed = parser.parse(formula);
if (!!parsed.error) {
return {formula, error: describeParseError({error: parsed.error})};
try {
return !evaluateFormula({constants: vars, formula}).result;
} catch (err) {
return {formula, error: err.message};
}
return parsed.result === false;
});
const [errored] = filtered.filter(n => !!n.error);

781
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,7 @@
"@alexbosworth/blockchain": "3.2.0",
"@alexbosworth/caporal": "2.0.0",
"@alexbosworth/fiat": "2.0.0",
"@alexbosworth/formulas": "1.4.0",
"@alexbosworth/html2unicode": "1.1.5",
"@alexbosworth/node-fetch": "2.6.2",
"abort-controller": "3.0.0",
@ -29,19 +30,18 @@
"colorette": "2.0.20",
"crypto-js": "4.2.0",
"csv-parse": "7.0.1",
"goldengate": "16.0.3",
"grammy": "1.44.0",
"hot-formula-parser": "4.0.0",
"goldengate": "16.0.4",
"grammy": "1.45.1",
"import-lazy": "4.0.0",
"ini": "7.0.0",
"inquirer": "14.0.2",
"ln-accounting": "10.0.3",
"ln-service": "59.2.0",
"ln-accounting": "10.0.4",
"ln-service": "59.2.2",
"ln-sync": "8.0.3",
"ln-telegram": "8.0.3",
"ln-telegram": "8.0.4",
"minimist": "1.2.8",
"moment": "2.30.1",
"paid-services": "8.0.3",
"paid-services": "8.0.4",
"probing": "7.0.0",
"qrcode-terminal": "0.12.0",
"sanitize-filename": "1.6.4",
@ -78,7 +78,7 @@
"integration-tests": "node test/integration",
"postpack": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag -s v$PACKAGE_VERSION -m v$PACKAGE_VERSION && git push github --tags",
"postpublish": "docker buildx build --platform linux/amd64,linux/arm64 -t alexbosworth/balanceofsatoshis -t alexbosworth/balanceofsatoshis:$npm_package_version --push .",
"test": "npx nyc@17.1.0 node --experimental-test-coverage --test 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"
"test": "node --experimental-test-coverage --test 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": "22.1.7"
"version": "23.0.0"
}

View file

@ -12,6 +12,7 @@ const tok = n => Number(BigInt(n) / BigInt(1e3));
{
channels: [{
capacity: <Channel Token Capacity Number>
id: <Standard Format Channel Id String>
local_balance: <Channel Local Balance Tokens Number>
partner_public_key: <Peer Public Key Hex String>
@ -97,20 +98,25 @@ module.exports = ({channels, filters, policies, tags, query}) => {
const feeRates = peerPolicies.filter(n => n.fee_rate !== undefined);
const pendingPayments = withPeer.map(n => n.pending_payments.length);
const matching = isMatchingFilters({
filters: filters || [],
variables: {
capacity: sumOf(withPeer.map(n => n.capacity)),
heights: withPeer.map(n => {
return decodeChanId({channel: n.id}).block_height;
}),
inbound_base_fee: max(...feeRates.map(n => tok(n.base_fee_mtokens))),
inbound_fee_rate: max(...feeRates.map(n => n.fee_rate)),
inbound_liquidity: sumOf(withPeer.map(n => n.remote_balance)),
outbound_liquidity: sumOf(withPeer.map(n => n.local_balance)),
pending_payments: sumOf(pendingPayments),
},
});
const maxBaseFee = max(...feeRates.map(n => tok(n.base_fee_mtokens)));
const maxFeeRate = max(...feeRates.map(n => n.fee_rate));
const variables = {
capacity: sumOf(withPeer.map(n => n.capacity)),
heights: withPeer.map(n => {
return decodeChanId({channel: n.id}).block_height;
}),
inbound_liquidity: sumOf(withPeer.map(n => n.remote_balance)),
outbound_liquidity: sumOf(withPeer.map(n => n.local_balance)),
pending_payments: sumOf(pendingPayments),
};
if (!!feeRates.length) {
variables.inbound_base_fee = maxBaseFee;
variables.inbound_fee_rate = maxFeeRate;
}
const matching = isMatchingFilters({variables, filters: filters || []});
if (!!matching.failure) {
return matching;

View file

@ -1,4 +1,4 @@
const {Parser} = require('hot-formula-parser');
const {evaluateFormula} = require('@alexbosworth/formulas');
const {isArray} = Array;
const {keys} = Object;
@ -72,7 +72,7 @@ module.exports = args => {
throw new Error('ExpectedTorStatusToCheckOpenRequestRules');
}
const variables = {
const constants = {
btc: 1e8,
capacities: args.capacities,
capacity: args.capacity,
@ -84,46 +84,14 @@ module.exports = args => {
local_balance: args.local_balance,
m: 1e6,
mm: 1e6,
obsolete: args.is_obsolete,
obsolete: !!args.is_obsolete,
private: args.is_private,
public_key: args.public_key,
tor: args.is_tor,
};
const parser = new Parser();
// Add the variables to the parser
keys(variables).forEach(key => {
parser.setVariable(key.toLowerCase(), variables[key]);
parser.setVariable(key.toUpperCase(), variables[key]);
return;
});
const violation = args.rules.find(rule => {
const parsed = parser.parse(rule);
switch (parsed.error) {
case '#DIV/0!':
throw new Error('CannotDivideByZeroInOpenRequestRule');
case '#ERROR!':
throw new Error('FailedToParseSpecifiedOpenRequestRule');
case '#N/A':
case '#NAME?':
throw new Error('UnrecognizedVariableOrFunctionInRequestRule');
case '#NUM':
throw new Error('InvalidNumberFoundInRequestRule');
case '#VALUE!':
throw new Error('UnexpectedValueTypeInRequestRule');
default:
// Rules must evaluate as truthy
return !parsed.result;
}
return !evaluateFormula({constants, formula: rule}).result;
});
return {rule: violation};

View file

@ -1,18 +1,15 @@
const asyncAuto = require('async/auto');
const asyncMap = require('async/map');
const {evaluateFormula} = require('@alexbosworth/formulas');
const {findKey} = require('ln-sync');
const {getChannel} = require('ln-service');
const {getHeight} = require('ln-service');
const {getNode} = require('ln-service');
const {Parser} = require('hot-formula-parser');
const {returnResult} = require('asyncjs-util');
const {describeParseError} = require('./../display');
const amountVariables = {btc: 1e8, k: 1e3, m: 1e6, mm: 1e6};
const asFormula = n => ({formula: n.slice(0, n.length-67), key: n.slice(-66)});
const asOutFilter = n => ({out_filter: n.slice(67), key: n.slice(0, 66)});
const {assign} = Object;
const channelFromEdge = edge => edge.slice(0, -2);
const decodePair = n => n.split('/');
const flatten = arr => [].concat(...arr);
@ -25,7 +22,6 @@ const isFormula = n => /(.*)\/0[2-3][0-9A-F]{64}$/gim.test(n);
const isOutFilter = n => /^0[2-3][0-9A-F]{64}\/(.*)/gim.test(n);
const isPair = n => !!n && /^0[2-3][0-9A-F]{64}\/0[2-3][0-9A-F]{64}$/i.test(n);
const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/gim.test(n);
const {keys} = Object;
const pairAsIgnore = (a, b) => ({from_public_key: a, to_public_key: b});
const uniq = arr => Array.from(new Set(arr));
@ -255,35 +251,25 @@ module.exports = (args, cbk) => {
return;
}
const parser = new Parser();
const variables = {};
try {
const parsed = evaluateFormula({
formula,
constants: {
...amountVariables,
capacity,
height,
age: getHeight.current_block_height - height,
base_fee: Number(outPolicy.base_fee_mtokens) || Number(),
fee_rate: outPolicy.fee_rate || Number(),
opposite_fee_rate: peerPolicy.fee_rate || Number(),
},
});
assign(variables, amountVariables);
assign(variables, {
capacity,
height,
age: getHeight.current_block_height - height,
base_fee: Number(outPolicy.base_fee_mtokens) || Number(),
fee_rate: outPolicy.fee_rate || Number(),
opposite_fee_rate: peerPolicy.fee_rate || Number(),
});
keys(variables).forEach(key => {
parser.setVariable(key.toLowerCase(), variables[key]);
parser.setVariable(key.toUpperCase(), variables[key]);
return;
});
const parsed = parser.parse(formula);
if (!!parsed.error) {
return {error: describeParseError({error: parsed.error})};
}
if (parsed.result === false) {
return;
if (!parsed.result) {
return;
}
} catch (err) {
return {error: err.message};
}
return {
@ -333,35 +319,25 @@ module.exports = (args, cbk) => {
return;
}
const parser = new Parser();
const variables = {};
try {
const parsed = evaluateFormula({
formula,
constants: {
...amountVariables,
capacity,
height,
age: getHeight.current_block_height - height,
base_fee: Number(inPolicy.base_fee_mtokens) || Number(),
fee_rate: inPolicy.fee_rate || Number(),
opposite_fee_rate: outPolicy.fee_rate || Number(),
},
});
assign(variables, amountVariables);
assign(variables, {
capacity,
height,
age: getHeight.current_block_height - height,
base_fee: Number(inPolicy.base_fee_mtokens) || Number(),
fee_rate: inPolicy.fee_rate || Number(),
opposite_fee_rate: outPolicy.fee_rate || Number(),
});
keys(variables).forEach(key => {
parser.setVariable(key.toLowerCase(), variables[key]);
parser.setVariable(key.toUpperCase(), variables[key]);
return;
});
const parsed = parser.parse(formula);
if (!!parsed.error) {
return {error: describeParseError({error: parsed.error})};
}
if (parsed.result === false) {
return;
if (!parsed.result) {
return;
}
} catch (err) {
return {error: err.message};
}
return {

View file

@ -1,4 +1,4 @@
const {Parser} = require('hot-formula-parser');
const {evaluateFormula} = require('@alexbosworth/formulas');
const bipsAsPpm = bips => bips * 1e2;
const {ceil} = Math;
@ -12,7 +12,7 @@ const percentAsPpm = percent => percent * 1e4;
inbound_liquidity: <Inbound Tokens Number>
outbound_liquidity: <Outbound Tokens Number>
node_rates: [{
key: <Node Key String>
key: <Label Prefixed Node Key String>
rate: <Node PPM Rate Number>
}]
}
@ -28,31 +28,30 @@ module.exports = args => {
return {};
}
const parser = new Parser();
const rates = args.node_rates.reduce((sum, n) => {
sum[n.key] = n.rate;
parser.setFunction('BIPS', params => bipsAsPpm(params.slice().pop()));
parser.setFunction('PERCENT', params => percentAsPpm(params.slice().pop()));
parser.setVariable('INBOUND', args.inbound_liquidity);
parser.setVariable('INBOUND_FEE_RATE', args.inbound_fee_rate);
parser.setVariable('OUTBOUND', args.outbound_liquidity);
return sum;
},
{});
args.node_rates.forEach(({key, rate}) => parser.setVariable(key, rate));
try {
const {result} = evaluateFormula({
constants: {
...rates,
inbound: args.inbound_liquidity,
inbound_fee_rate: args.inbound_fee_rate,
outbound: args.outbound_liquidity,
},
formula: args.fee_rate,
functions: {
bips: bipsAsPpm,
percent: percentAsPpm,
},
});
const parsedRate = parser.parse(args.fee_rate.toUpperCase());
switch (parsedRate.error) {
case null:
break;
case '#DIV/0!':
return {failure: 'FeeRateCalculationCannotDivideByZeroFormula'};
case '#ERROR!':
return {failure: 'FailedToParseFeeRateFormula'};
default:
return {failure: 'UnrecognizedVariableOrFunctionInFeeRateFormula'};
return {rate: ceil(result)};
} catch (err) {
return {failure: err.message};
}
return {rate: ceil(parsedRate.result)};
};

View file

@ -15,7 +15,7 @@ const tests = [
description: 'An invalid filter returns a failure',
expected: {
failure: {
error: 'UnrecognizedVariableOrFunctionInFormula',
error: 'ExpectedAllKnownConstantsInFormulaToEvaluate',
formula: 'foo',
},
},

View file

@ -7,6 +7,7 @@ const {findTagMatch} = require('./../../peers');
const makeArgs = overrides => {
const args = {
channels: [{
capacity: 3,
id: '1x1x1',
local_balance: 1,
partner_public_key: Buffer.alloc(33, 3).toString('hex'),
@ -36,13 +37,17 @@ const tests = [
args: makeArgs({filters: ['invalid formula']}),
description: 'A failed formula is provided',
expected: {
failure: {error: 'FailedToParseFormula', formula: 'invalid formula'},
failure: {
error: 'UnexpectedTrailingTokenForFormulaParsing',
formula: 'invalid formula',
},
},
},
{
args: makeArgs({
channels: [
{
capacity: 2e8,
id: '1x1x1',
local_balance: 1e3,
partner_public_key: Buffer.alloc(33, 3).toString('hex'),
@ -50,6 +55,7 @@ const tests = [
remote_balance: 1,
},
{
capacity: 2e8,
id: '2x2x2',
local_balance: 1e8,
partner_public_key: Buffer.alloc(33, 2).toString('hex'),

View file

@ -42,17 +42,17 @@ const tests = [
{
args: makeArgs({fee_rate: '1/0'}),
description: 'Cannot divide by zero',
expected: {failure: 'FeeRateCalculationCannotDivideByZeroFormula'},
expected: {failure: 'ExpectedNonZeroDivisorForFormulaEvaluation'},
},
{
args: makeArgs({fee_rate: '/'}),
description: 'Formula must be valid',
expected: {failure: 'FailedToParseFeeRateFormula'},
expected: {failure: 'UnexpectedPrimaryTokenForFormulaParsing'},
},
{
args: makeArgs({fee_rate: 'fee_rate'}),
description: 'Formula must be valid',
expected: {failure: 'UnrecognizedVariableOrFunctionInFeeRateFormula'},
expected: {failure: 'ExpectedAllKnownConstantsInFormulaToEvaluate'},
},
];