add filters and dryrun to advertise

This commit is contained in:
Alex Bosworth 2021-11-13 09:24:07 -08:00
parent 53cd8e8157
commit e75dee4220
No known key found for this signature in database
GPG key ID: E80D2F3F311FD87E
11 changed files with 850 additions and 2139 deletions

View file

@ -1,5 +1,11 @@
# Versions
## 11.11.0
- `advertise`: Add `--dryrun` to skip actually sending ads
- `advertise`: Add `--filter` to target nodes with specified capacities, channel counts
- `swap-in`: Fix command to execute when there is a liquidity lookup needed
## 11.10.0
- `find`: Add estimated disk usage to channels with a peer

34
bos
View file

@ -83,7 +83,7 @@ prog
month: options.month,
node: options.node,
rate_provider: options.rateProvider,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
year: options.year,
},
responses.returnObject({logger, reject, resolve, table}));
@ -95,6 +95,11 @@ prog
// Advertise to other nodes on the network
.command('advertise', 'Broadcast advertisement')
.help('use --filter conditions to limit broadcast scope: capacity > 1*m')
.help('--filter variables: CAPACITY/CHANNELS_COUNT/K/M')
.help('Default filter scope: channels_count > 9')
.option('--dryrun', 'Avoid actually sending advertisements')
.option('--filter <expression>', 'Require node match condition', REPEATABLE)
.option('--message <message>', 'Custom advertisement message')
.option('--node <node_name>', 'Advertise via saved node')
.action((args, options, logger) => {
@ -102,6 +107,8 @@ prog
try {
return await services.advertise({
logger,
filters: flatten([options.filter].filter(n => !!n)),
is_dry_run: !!options.dryrun,
lnd: (await lndForNode(logger, options.node)).lnd,
message: options.message,
});
@ -131,7 +138,7 @@ prog
lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
mirrors: flatten([options.mirror].filter(n => !!n)),
node: options.node,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
urls: flatten([options.url].filter(n => !!n)),
},
responses.returnObject({logger, reject, resolve}));
@ -303,7 +310,7 @@ prog
days: options.days,
is_monochrome: !!options.noColor,
lnds: (await lnd.getLnds({logger, nodes: options.node})).lnds,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
},
responses.returnChart({logger, reject, resolve, data: 'data'}));
} catch (err) {
@ -427,7 +434,7 @@ prog
return chain.getChannelCloses({
limit: options.limit,
lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
},
responses.returnObject({logger, reject, resolve}));
} catch (err) {
@ -783,7 +790,7 @@ prog
lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
max_fee_rate: options.maxFeeRate || undefined,
min_node_score: options.minScore || undefined,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
with: options.with,
},
responses.returnNumber({logger, reject, resolve, number: 'balance'}));
@ -871,7 +878,7 @@ prog
is_private: options.private,
lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
peer: options.with || undefined,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
tokens: options.amount,
},
responses.returnObject({logger, reject, resolve}));
@ -976,7 +983,7 @@ prog
is_external: options.externalFunding,
lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
public_keys: args.peerPublicKeys,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
set_fee_rates: flatten([options.setFeeRate]).filter(n => !!n),
types: flatten([options.type].filter(n => !!n)),
},
@ -1139,7 +1146,7 @@ prog
return new Promise((resolve, reject) => {
return fiat.getPrices({
from: options.from,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
symbols: args.symbols.map(n => n.toUpperCase()),
},
responses.returnObject({
@ -1277,7 +1284,7 @@ prog
return chain.recoverP2pk({
id: args.id,
lnd: (await lndForNode(logger, options.node)).lnd,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
vout: args.vout,
},
responses.returnObject({logger, reject, resolve}));
@ -1328,7 +1335,7 @@ prog
outbound_liquidity_below: options.outboundBelow,
outpoints: flatten([options.outpoint].filter(n => !!n)),
public_key: args.publicKey,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
},
responses.returnObject({logger, reject, resolve}));
} catch (err) {
@ -1346,7 +1353,7 @@ prog
return wallets.getReport({
fs: {getFile: readFile},
node: options.node,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
style: !!options.styled ? 'styled' : undefined,
},
responses.returnOutput({logger, reject, resolve}));
@ -1391,7 +1398,7 @@ prog
message: options.message,
quiz_answers: flatten([options.quiz].filter(n => !!n)),
out_through: options.out,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
},
responses.returnObject({logger, reject, resolve}));
} catch (err) {
@ -1501,6 +1508,7 @@ prog
fetch,
logger,
api_key: options.apiKey || undefined,
fs: {getFile: readFile},
in_through: options.in || undefined,
is_refund_test: options.testRefund,
lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
@ -1508,7 +1516,7 @@ prog
node: options.node,
recovery: options.recovery,
refund_address: options.refundAddress,
request: commands.fetchRequest({fetch}),
request: commands.simpleRequest,
socket: options.serviceSocket || undefined,
tokens: args.amount,
},

View file

@ -63,6 +63,10 @@ module.exports = ({id, interval, network, request, retries}, cbk) => {
return cbk([503, 'FailedToGetRawTransaction', {err}]);
}
if (!!r && r.statusCode === 404) {
return cbk([404, 'TransactionNotFound']);
}
if (!isHex(transaction)) {
return cbk([503, 'ExpectedTransactionInResponse']);
}

View file

@ -4,6 +4,7 @@ const fetchRequest = require('./fetch_request');
const {marketPairs} = require('./constants');
const {peerSortOptions} = require('./constants');
const {rateProviders} = require('./constants');
const simpleRequest = require('./simple_request');
const {swapTypes} = require('./constants');
module.exports = {
@ -13,5 +14,6 @@ module.exports = {
marketPairs,
peerSortOptions,
rateProviders,
simpleRequest,
swapTypes,
};

View file

@ -0,0 +1,79 @@
const https = require('https');
const asQueryString = qs => !!qs ? `?${qs}` : '';
const httpsProtocol = 'https:';
const {keys} = Object;
const {parse} = JSON;
/** Simplified version of request method
{
[json]: <Interpret Result as JSON Bool>
[method]: <HTTPS Method String>
[timeout]: <Timeout MS Number>
url: <HTTPS URL String>
}
@returns
<Response Object>
<Response Body>
*/
module.exports = (args, cbk) => {
if (!args.url) {
return cbk([400, 'ExpectedUrlToExecuteSimpleRequest']);
}
const url = new URL(args.url);
if (url.protocol !== httpsProtocol) {
return cbk([400, 'ExpectedHttpsProtocolToExecuteSimpleRequest']);
}
let error;
const qs = url.searchParams;
// Include passed query string arguments
if (!!args.qs) {
keys(args.qs).forEach(key => qs.set(key, args.qs[key]));
}
const req = https.request({
hostname: url.hostname,
method: args.method,
path: url.pathname + asQueryString(qs.toString()),
port: url.port,
timeout: args.timeout,
},
res => {
const body = [];
// Collect response chunks
res.on('data', data => body.push(data));
// Response is finished
res.on('end', () => {
if (!!error) {
return cbk(err);
}
const combined = Buffer.concat(body).toString();
if (!args.json) {
return cbk(null, res, combined);
}
try {
return cbk(null, res, parse(combined));
} catch (err) {
return cbk(err);
}
});
});
req.on('error', err => error = err);
req.on('timeout', () => req.abort());
req.end();
return;
};

2790
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,9 +18,9 @@
"asyncjs-util": "1.2.7",
"bip66": "1.1.5",
"bitcoin-ops": "1.4.1",
"bitcoinjs-lib": "5.2.0",
"bitcoinjs-lib": "6.0.0",
"bolt01": "1.2.3",
"bolt03": "1.2.11",
"bolt03": "1.2.12",
"bolt07": "1.7.4",
"caporal": "1.4.0",
"cbor": "8.1.0",
@ -28,19 +28,19 @@
"colorette": "2.0.16",
"crypto-js": "4.1.1",
"csv-parse": "4.16.3",
"goldengate": "10.4.1",
"goldengate": "10.4.2",
"hot-formula-parser": "4.0.0",
"import-lazy": "4.0.0",
"ini": "2.0.0",
"inquirer": "8.2.0",
"invoices": "2.0.1",
"ln-accounting": "5.0.4",
"ln-service": "52.15.0",
"ln-sync": "3.0.0",
"ln-telegram": "3.4.0",
"invoices": "2.0.2",
"ln-accounting": "5.0.5",
"ln-service": "52.16.0",
"ln-sync": "3.0.1",
"ln-telegram": "3.4.2",
"moment": "2.29.1",
"paid-services": "3.1.1",
"probing": "2.0.0",
"paid-services": "3.1.2",
"probing": "2.0.1",
"psbt": "1.1.10",
"qrcode-terminal": "0.12.0",
"sanitize-filename": "1.6.3",
@ -53,6 +53,7 @@
"description": "Lightning balance CLI",
"devDependencies": {
"@alexbosworth/tap": "15.0.10",
"ecpair": "1.0.1",
"mock-lnd": "1.4.1",
"secp256k1": "4.0.2"
},
@ -80,5 +81,5 @@
"postpublish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t alexbosworth/balanceofsatoshis --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/fiat/*.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/wallets/*.js"
},
"version": "11.10.0"
"version": "11.11.0"
}

View file

@ -13,11 +13,13 @@ const {payViaRoutes} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const {subscribeToProbeForRoute} = require('ln-service');
const {isMatchingFilters} = require('./../display');
const {shuffle} = require('./../arrays');
const {ceil} = Math;
const cltvDelay = 144;
const createSecret = () => randomBytes(32).toString('hex');
const defaultFilter = ['channels_count > 9'];
const defaultMsg = (alias, key) => `Check out my node! ${alias} ${key}`;
const filterLimit = 10;
const hashOf = n => createHash('sha256').update(n).digest().toString('hex');
@ -25,6 +27,7 @@ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
const invoiceDescription = n => `👀 ${n}`;
const invoiceExpiration = () => new Date(Date.now() + 1000 * 60 * 60 * 24 * 5);
const invoiceTokens = 1;
const {isArray} = Array;
const keySendValueType = '5482373484';
const maxFeeTokens = 10;
const messageWithReply = (msg, req) => `${msg} (Mark seen: ${req})`;
@ -34,12 +37,15 @@ const pathTimeoutMs = 1000 * 45;
const payTimeoutMs = 1000 * 60;
const probeTimeoutMs = 1000 * 60 * 2;
const sendTokens = 10;
const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
const textMessageType = '34349334';
const utf8AsHex = utf8 => Buffer.from(utf8, 'utf8').toString('hex');
/** Advertise to nodes that accept KeySend
{
filters: [<Node Condition Filter String>]
[is_dry_run]: <Avoid Sending Advertisements Bool>
lnd: <Authenticated LND API Object>
logger: <Winston Logger Object>
[message]: <Message To Send String>
@ -50,6 +56,10 @@ module.exports = (args, cbk) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!isArray(args.filters)) {
return cbk([400, 'ExpectedArrayOfFiltersToAdvertise']);
}
if (!args.lnd) {
return cbk([400, 'ExpectedAuthenticatedLndToAdvertise']);
}
@ -95,19 +105,41 @@ module.exports = (args, cbk) => {
const {shuffled} = shuffle({array: getGraph.nodes});
// Only consider 3rd party nodes that have known features
const nodes = shuffled
const filteredNodes = shuffled
.filter(n => n.public_key !== getIdentity.public_key)
.filter(n => !!n.features.length)
.filter(node => {
.map(node => {
const channels = getGraph.channels.filter(channel => {
const {policies} = channel;
return policies.find(n => n.public_key === node.public_key);
});
return channels.length >= minChannelCount;
const filters = args.filters.length ? args.filters : defaultFilter;
const variables = {
capacity: sumOf(channels.map(n => n.capacity)),
channels_count: channels.length,
};
const isMatching = isMatchingFilters({filters, variables});
// Exit early when there is a failure with a filter
if (isMatching.failure) {
return {failure: isMatching.failure};
}
return !!isMatching.is_matching ? {node} : {};
});
const failure = filteredNodes.find(n => !!n.failure);
if (!!filteredNodes.find(n => !!n.failure)) {
return cbk([400, 'ExpectedValidFiltersForNodes', {failure}]);
}
const nodes = filteredNodes.map(n => n.node).filter(n => !!n);
args.logger.info({potential_nodes: nodes.length});
return asyncFilterLimit(nodes, filterLimit, (node, cbk) => {
@ -196,6 +228,11 @@ module.exports = (args, cbk) => {
{type: textMessageType, value: utf8AsHex(finalMessage)},
];
// Exit early when this is a dry run
if (!!args.is_dry_run) {
return args.logger.info({skipping_due_to_dry_run: node.alias});
}
// Send the payment
const paid = await payViaRoutes({
id: hashOf(hexAsBuffer(secret)),

View file

@ -39,7 +39,6 @@ const bufferAsHex = buffer => buffer.toString('hex');
const componentsSeparator = ' ';
const decBase = 10;
const defaultMaxFeeMtokens = '9000';
const derivePubKey = n => ECPair.fromPrivateKey(n).publicKey.toString('hex');
const encodeSig = (sig, hash) => Buffer.concat(Buffer.from(sig, 'hex'), hash);
const {fromBech32} = address;
const {fromHex} = Transaction;

View file

@ -70,6 +70,10 @@ module.exports = (args, cbk) => {
return cbk([400, 'ExpectedFetchFunctionToReceiveOnChain']);
}
if (!args.fs) {
return cbk([400, 'ExpectedFileSystemMethodsToReceiveOnChain']);
}
if (!args.logger) {
return cbk([400, 'ExpectedLoggerToReceiveOnChain']);
}
@ -97,6 +101,7 @@ module.exports = (args, cbk) => {
return getLiquidity({
above: args.tokens,
fs: args.fs,
is_top: true,
lnd: args.lnd,
},

View file

@ -1,6 +1,6 @@
const EventEmitter = require('events');
const {ECPair} = require('bitcoinjs-lib');
const {ECPair} = require('ecpair');
const {payments} = require('bitcoinjs-lib');
const {test} = require('@alexbosworth/tap');
const {Transaction} = require('bitcoinjs-lib');