add fund-dev command, add peer specificity to chart fees earned

This commit is contained in:
Alex Bosworth 2019-11-23 22:43:28 -08:00
parent 5cb765439b
commit d5d911b8b5
No known key found for this signature in database
GPG key ID: E80D2F3F311FD87E
16 changed files with 742 additions and 87 deletions

View file

@ -1,5 +1,10 @@
# Versions
## Version 5.4.0
- `chart-fees-earned`: Add `via` argument to show only fees related to a peer
- Add `fund-dev`: method to fund Bitcoin and Lightning development efforts
## Version 5.3.1
- Add `chart-fees-earned` to show a chart of fees earned

View file

@ -75,6 +75,9 @@ bos find "query"
# Output a summarized version of peers forwarded towards
bos forwards
# Fund people related to Bitcoin and Lightning efforts
bos fund-dev
# Send a gift of some tokens to a directly connected peer
bos gift "pubkey" "amount"

View file

@ -1,3 +1,4 @@
const shuffle = require('./shuffle');
const sortBy = require('./sort_by');
module.exports = {sortBy};
module.exports = {shuffle, sortBy};

25
arrays/shuffle.js Normal file
View file

@ -0,0 +1,25 @@
const {floor} = Math;
const {random} = Math;
/** Shuffle array
{
array: [<Element Object>]
}
@returns
{
shuffled: [<Shuffled Element Object>]
}
*/
module.exports = ({array}) => {
const shuffle = array.slice();
for (let i = shuffle.length - 1; !!i; i--) {
const j = floor(random() * (i + 1));
[shuffle[i], shuffle[j]] = [shuffle[j], shuffle[i]];
}
return {shuffled: shuffle};
};

32
bos
View file

@ -18,6 +18,7 @@ const {adjustSavedNodes} = require('./nodes');
const {authenticatedLnd} = require('./lnd');
const {exchanges} = require('./fiat');
const {findRecord} = require('./lnd');
const {fundDev} = require('./wallets');
const {getAccountingReport} = require('./balances');
const {getBalance} = require('./balances');
const {getCertValidityDays} = require('./lnd');
@ -231,6 +232,7 @@ prog
// Show a chart of fees earned
.command('chart-fees-earned', 'Get a chart of earned routing fees')
.argument('[via_peer]', 'Routing fees earned via a specified peer')
.help('Show the routing fees earned')
.option('--days <days>', 'Chart fees over the past number of days', INT, 60)
.option('--node <node_name>', 'Get fee earnings chart for saved node')
@ -240,6 +242,7 @@ prog
return getFeesChart({
days: options.days,
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
via: args.viaPeer,
},
returnChart({logger, reject, resolve, data: 'fees'}));
} catch (err) {
@ -335,6 +338,33 @@ prog
});
})
// Help with funding
.command('fund-dev', 'Fund people related to Bitcoin and Lightning efforts')
.help('Send some sats (805 default) to a worthy individual. For the cause!')
.help('Someone will be automatically chosen if no @handle specified')
.help('Nominate worthiness via GitHub issues, include proof of your support')
.option('--amount <amount>', 'Choose amount to send', INT, 805)
.option('--dryrun', 'Show what would happen without actually sending funds')
.option('--node <node_name>', 'Saved node to give funds from')
.option('--twitter-handle <nick>', 'Specifically support someone')
.action(async (args, options, logger) => {
return new Promise(async (resolve, reject) => {
try {
return fundDev({
logger,
request,
is_dry_run: options.dryrun,
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
to_twitter_account: options.twitterHandle,
tokens: options.amount,
},
returnObject({logger, reject, resolve}));
} catch (err) {
return reject(err);
}
});
})
// Give a peer some tokens
.command('gift', 'Give a direct peer some free funds off-chain')
.help('Send some funds to a connected peer')
@ -672,7 +702,7 @@ prog
.option('--budget <amount>', 'Spending amount to allow', INT, 0)
.option('--connect <connect_code>', 'Connection code', INT)
.option('--node <node_name>', 'Node to connect to Telegram', REPEATABLE)
.action(async (args, options, logger) => {
.action((args, options, logger) => {
return new Promise(async (resolve, reject) => {
try {
const nodes = flatten([options.node].filter(n => !!n)).map(node => {

View file

@ -35,7 +35,6 @@ const reserveRatio = 0.01;
lnd: <Authenticated LND gRPC API Object>
logger: <Winston Logger Object>
[max_fee]: <Maximum Fee Tokens Number>
[node]: <Node Name String>
[out_through]: <Out through peer with Public Key Hex String>
[request]: <Payment Request String>
[tokens]: <Tokens Number>

188
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "balanceofsatoshis",
"version": "5.3.1",
"version": "5.4.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -14,12 +14,12 @@
}
},
"@babel/generator": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.2.tgz",
"integrity": "sha512-WthSArvAjYLz4TcbKOi88me+KmDJdKSlfwwN8CnUYn9jBkzhq0ZEPuBfkAWIvjJ3AdEV1Cf/+eSQTnp3IDJKlQ==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.4.tgz",
"integrity": "sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg==",
"dev": true,
"requires": {
"@babel/types": "^7.7.2",
"@babel/types": "^7.7.4",
"jsesc": "^2.5.1",
"lodash": "^4.17.13",
"source-map": "^0.5.0"
@ -34,32 +34,32 @@
}
},
"@babel/helper-function-name": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.0.tgz",
"integrity": "sha512-tDsJgMUAP00Ugv8O2aGEua5I2apkaQO7lBGUq1ocwN3G23JE5Dcq0uh3GvFTChPa4b40AWiAsLvCZOA2rdnQ7Q==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz",
"integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==",
"dev": true,
"requires": {
"@babel/helper-get-function-arity": "^7.7.0",
"@babel/template": "^7.7.0",
"@babel/types": "^7.7.0"
"@babel/helper-get-function-arity": "^7.7.4",
"@babel/template": "^7.7.4",
"@babel/types": "^7.7.4"
}
},
"@babel/helper-get-function-arity": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.0.tgz",
"integrity": "sha512-tLdojOTz4vWcEnHWHCuPN5P85JLZWbm5Fx5ZsMEMPhF3Uoe3O7awrbM2nQ04bDOUToH/2tH/ezKEOR8zEYzqyw==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz",
"integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==",
"dev": true,
"requires": {
"@babel/types": "^7.7.0"
"@babel/types": "^7.7.4"
}
},
"@babel/helper-split-export-declaration": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.0.tgz",
"integrity": "sha512-HgYSI8rH08neWlAH3CcdkFg9qX9YsZysZI5GD8LjhQib/mM0jGOZOVkoUiiV2Hu978fRtjtsGsW6w0pKHUWtqA==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz",
"integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==",
"dev": true,
"requires": {
"@babel/types": "^7.7.0"
"@babel/types": "^7.7.4"
}
},
"@babel/highlight": {
@ -105,43 +105,43 @@
}
},
"@babel/parser": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.3.tgz",
"integrity": "sha512-bqv+iCo9i+uLVbI0ILzKkvMorqxouI+GbV13ivcARXn9NNEabi2IEz912IgNpT/60BNXac5dgcfjb94NjsF33A==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.4.tgz",
"integrity": "sha512-jIwvLO0zCL+O/LmEJQjWA75MQTWwx3c3u2JOTDK5D3/9egrWRRA0/0hk9XXywYnXZVVpzrBYeIQTmhwUaePI9g==",
"dev": true
},
"@babel/runtime": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.2.tgz",
"integrity": "sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.4.tgz",
"integrity": "sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"@babel/template": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.0.tgz",
"integrity": "sha512-OKcwSYOW1mhWbnTBgQY5lvg1Fxg+VyfQGjcBduZFljfc044J5iDlnDSfhQ867O17XHiSCxYHUxHg2b7ryitbUQ==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz",
"integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"@babel/parser": "^7.7.0",
"@babel/types": "^7.7.0"
"@babel/parser": "^7.7.4",
"@babel/types": "^7.7.4"
}
},
"@babel/traverse": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.2.tgz",
"integrity": "sha512-TM01cXib2+rgIZrGJOLaHV/iZUAxf4A0dt5auY6KNZ+cm6aschuJGqKJM3ROTt3raPUdIDk9siAufIFEleRwtw==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz",
"integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.5.5",
"@babel/generator": "^7.7.2",
"@babel/helper-function-name": "^7.7.0",
"@babel/helper-split-export-declaration": "^7.7.0",
"@babel/parser": "^7.7.2",
"@babel/types": "^7.7.2",
"@babel/generator": "^7.7.4",
"@babel/helper-function-name": "^7.7.4",
"@babel/helper-split-export-declaration": "^7.7.4",
"@babel/parser": "^7.7.4",
"@babel/types": "^7.7.4",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.13"
@ -165,9 +165,9 @@
}
},
"@babel/types": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.2.tgz",
"integrity": "sha512-YTf6PXoh3+eZgRCBzzP25Bugd2ngmpQVrk7kXX0i5N9BO7TFBtIgZYs7WtxtOGs8e6A4ZI7ECkbBCEHeXocvOA==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz",
"integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
@ -675,9 +675,9 @@
}
},
"bolt07": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.4.3.tgz",
"integrity": "sha512-ZZ3agtRbgEpYF+TSlUi+W0jP/0sv1DBxHD9Ii+diwaSWd13NSwpqiiNAKRzxSXnnec6/8nP+Mz+X0Mo0UjQ/Sg==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.4.4.tgz",
"integrity": "sha512-1jn3ef9lhhcd4uC9JcOHQXtFgOMuN8AEBMRnWNO/zTVOOkl0zLKEkv30PrBfwdr0i/OAIEbhTHIR/2PCGLVfxQ==",
"requires": {
"bn.js": "5.0.0"
},
@ -1500,6 +1500,12 @@
"integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==",
"dev": true
},
"diff-frag": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/diff-frag/-/diff-frag-1.0.1.tgz",
"integrity": "sha512-6/v2PC/6UTGcWPPetb9acL8foberUg/CtPdALeJUdD1B/weHNvzftoo00gYznqHGRhHEbykUGzqfG9RWOSr5yw==",
"dev": true
},
"dot-prop": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
@ -2038,6 +2044,16 @@
"safe-compare": "1.1.4",
"secp256k1": "3.7.1",
"ws": "7.2.0"
},
"dependencies": {
"bolt07": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.4.3.tgz",
"integrity": "sha512-ZZ3agtRbgEpYF+TSlUi+W0jP/0sv1DBxHD9Ii+diwaSWd13NSwpqiiNAKRzxSXnnec6/8nP+Mz+X0Mo0UjQ/Sg==",
"requires": {
"bn.js": "5.0.0"
}
}
}
}
}
@ -3298,12 +3314,59 @@
"json2csv": "4.5.4",
"ln-service": "47.4.0",
"request": "2.88.0"
},
"dependencies": {
"bn.js": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.0.0.tgz",
"integrity": "sha512-bVwDX8AF+72fIUNuARelKAlQUNtPOfG2fRxorbVvFk4zpHbqLrPdOGfVg5vrKwVzLLePqPBiATaOZNELQzmS0A=="
},
"bolt07": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.4.3.tgz",
"integrity": "sha512-ZZ3agtRbgEpYF+TSlUi+W0jP/0sv1DBxHD9Ii+diwaSWd13NSwpqiiNAKRzxSXnnec6/8nP+Mz+X0Mo0UjQ/Sg==",
"requires": {
"bn.js": "5.0.0"
}
},
"ln-service": {
"version": "47.4.0",
"resolved": "https://registry.npmjs.org/ln-service/-/ln-service-47.4.0.tgz",
"integrity": "sha512-uWT9AyXhK1vuVO+WxktZUBaD1zhrxs9L2XXH0/+ofKH5P+jm13Mt23nU0uY6E9Ad/iBpOYil7hDBrvtWbRA8TA==",
"requires": {
"@datastructures-js/priority-queue": "1.0.2",
"@grpc/proto-loader": "0.5.3",
"async": "3.1.0",
"asyncjs-util": "1.1.2",
"basicauth-middleware": "3.1.0",
"bech32": "1.1.3",
"bitcoinjs-lib": "5.1.6",
"bn.js": "5.0.0",
"body-parser": "1.19.0",
"bolt07": "1.4.3",
"compression": "1.7.4",
"cors": "2.8.5",
"dotenv": "8.2.0",
"express": "4.17.1",
"grpc": "1.24.2",
"is-base64": "1.0.0",
"is-hex": "1.1.3",
"lodash": "4.17.15",
"macaroon": "3.0.4",
"morgan": "1.9.1",
"promptly": "3.0.3",
"request": "2.88.0",
"safe-compare": "1.1.4",
"secp256k1": "3.7.1",
"ws": "7.2.0"
}
}
}
},
"ln-service": {
"version": "47.4.0",
"resolved": "https://registry.npmjs.org/ln-service/-/ln-service-47.4.0.tgz",
"integrity": "sha512-uWT9AyXhK1vuVO+WxktZUBaD1zhrxs9L2XXH0/+ofKH5P+jm13Mt23nU0uY6E9Ad/iBpOYil7hDBrvtWbRA8TA==",
"version": "47.5.0",
"resolved": "https://registry.npmjs.org/ln-service/-/ln-service-47.5.0.tgz",
"integrity": "sha512-VS6iqEv4ScP1c/AhO7cSWcYtR4ixAyESVrhJBY7FzyyPGvnixXiEYe7aggFbChvODiyEgmBON1giUaMB2R8JlQ==",
"requires": {
"@datastructures-js/priority-queue": "1.0.2",
"@grpc/proto-loader": "0.5.3",
@ -3314,7 +3377,7 @@
"bitcoinjs-lib": "5.1.6",
"bn.js": "5.0.0",
"body-parser": "1.19.0",
"bolt07": "1.4.3",
"bolt07": "1.4.4",
"compression": "1.7.4",
"cors": "2.8.5",
"dotenv": "8.2.0",
@ -4475,9 +4538,9 @@
"dev": true
},
"resolve": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.1.tgz",
"integrity": "sha512-+j3UVImywlkdEOzjNCk379mEqqUEmHgCmUDQkmVVwDkBVopfk/TqrBrr8ZfKZSnCqW/sgyuACFVjBwoB5Mw56A==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.2.tgz",
"integrity": "sha512-cAVTI2VLHWYsGOirfeYVVQ7ZDejtQ9fp4YhYckWDEkFfqbVjaT11iM8k6xSAfGFMM+gDpZjMnFssPu8we+mqFw==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
@ -5020,9 +5083,9 @@
}
},
"tap": {
"version": "14.9.2",
"resolved": "https://registry.npmjs.org/tap/-/tap-14.9.2.tgz",
"integrity": "sha512-Fyy/sjsw4eb+Hnphin4oMtDtKxmrob/vrnaIDv/F3thFFQjQFqMg8xf45zRFGHxUfezlrO6KsH8TpWNlTDINfA==",
"version": "14.10.1",
"resolved": "https://registry.npmjs.org/tap/-/tap-14.10.1.tgz",
"integrity": "sha512-GsjAtKf9WKe2Cj5/5OckVdwyz4kv4pjlidDPpNY32ikR6rkBnicU/gxwckXvV/pQ5uTDx+od6XqmVAB7ityXWg==",
"dev": true,
"requires": {
"async-hook-domain": "^1.1.2",
@ -5058,8 +5121,8 @@
"tap-mocha-reporter": "^5.0.0",
"tap-parser": "^10.0.1",
"tap-yaml": "^1.0.0",
"tcompare": "^2.3.0",
"treport": "^0.4.2",
"tcompare": "^3.0.0",
"treport": "^0.5.0",
"trivial-deferred": "^1.0.1",
"ts-node": "^8.3.0",
"typescript": "^3.6.3",
@ -5959,7 +6022,7 @@
"dev": true
},
"treport": {
"version": "0.4.2",
"version": "0.5.0",
"bundled": true,
"dev": true,
"requires": {
@ -6159,10 +6222,13 @@
}
},
"tcompare": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tcompare/-/tcompare-2.3.0.tgz",
"integrity": "sha512-fAfA73uFtFGybWGt4+IYT6UPLYVZQ4NfsP+IXEZGY0vh8e2IF7LVKafcQNMRBLqP0wzEA65LM9Tqj+FSmO8GLw==",
"dev": true
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tcompare/-/tcompare-3.0.0.tgz",
"integrity": "sha512-lRD40wHaBoVvmcCT0L7qJmR+jBl1nVP0WxbMYyOtsFza+SXD9aiRVir9aWogdQwNzY6ZwOvwDp5vg3XWQGOIVQ==",
"dev": true,
"requires": {
"diff-frag": "^1.0.1"
}
},
"telegraf": {
"version": "3.33.3",

View file

@ -16,7 +16,7 @@
"bitcoin-ops": "1.4.1",
"bitcoinjs-lib": "5.1.6",
"bolt03": "1.2.1",
"bolt07": "1.4.3",
"bolt07": "1.4.4",
"caporal": "1.3.0",
"cbor": "5.0.1",
"cert-info": "1.5.1",
@ -26,7 +26,7 @@
"ini": "1.3.5",
"inquirer": "7.0.0",
"ln-accounting": "3.1.6",
"ln-service": "47.4.0",
"ln-service": "47.5.0",
"moment": "2.24.0",
"qrcode-terminal": "0.12.0",
"request": "2.88.0",
@ -38,7 +38,7 @@
},
"description": "Lightning balance CLI",
"devDependencies": {
"tap": "14.9.2"
"tap": "14.10.1"
},
"engines": {
"node": ">=10.12.0"
@ -61,5 +61,5 @@
"scripts": {
"test": "tap test/arrays/*.js test/balances/*.js test/chain/*.js test/encryption/*.js test/fiat/*.js test/lnd/*.js test/network/*.js test/nodes/*.js test/responses/*.js test/routing/*.js test/swaps/*.js test/telegram/*.js"
},
"version": "5.3.1"
"version": "5.4.0"
}

View file

@ -1,7 +1,8 @@
const {plot} = require('asciichart');
const height = 15;
const padding = '\n ';
const newLine = '\n';
const padLen = (lineLen, desc) => (Math.max(0, lineLen - desc.length) + 3) / 2;
/** Return an output result to a logger in a promise
@ -25,11 +26,23 @@ module.exports = ({data, logger, reject, resolve}) => {
return reject();
}
const chart = plot(res[data], {height});
const [line] = chart.split(newLine);
if (!!res.title) {
const padding = ' '.repeat(padLen(line.length, res.title));
logger.info(`${newLine}${padding}${res.title}`);
}
logger.info(String());
logger.info(plot(res[data], {height}));
if (!!res.description) {
logger.info(`${padding}${res.description}`);
const padding = ' '.repeat(padLen(line.length, res.description));
logger.info(`${newLine}${padding}${res.description}`);
}
logger.info(String());

View file

@ -1,5 +1,6 @@
const asyncAuto = require('async/auto');
const {getForwards} = require('ln-service');
const {getNode} = require('ln-service');
const moment = require('moment');
const {returnResult} = require('asyncjs-util');
@ -9,21 +10,25 @@ const hoursPerDay = 24;
const limit = 99999;
const minChartDays = 4;
const maxChartDays = 90;
const notFound = -1;
const uniq = arr => Array.from(new Set(arr));
/** Get data for fees chart
{
days: <Fees Earned Over Days Count Number>
lnd: <Authenticated LND gRPC API Object>
via: <Via Public Key Hex String>
}
@returns via cbk or Promise
{
description: <Chart Description String>
fees: [<Earned Fee Tokens Number>]
title: <Chart Title String>
}
*/
module.exports = ({days, lnd}, cbk) => {
module.exports = ({days, lnd, via}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
@ -39,6 +44,11 @@ module.exports = ({days, lnd}, cbk) => {
return cbk();
},
// Get node details
getNode: ['validate', ({}, cbk) => {
return !via ? cbk() : getNode({lnd, public_key: via}, cbk);
}],
// Segment measure
measure: ['validate', ({}, cbk) => {
if (days > maxChartDays) {
@ -66,10 +76,31 @@ module.exports = ({days, lnd}, cbk) => {
cbk);
}],
// Total earnings
totalEarned: ['getForwards', ({getForwards}, cbk) => {
const {forwards} = getForwards;
// Filter the forwards
forwards: ['getForwards', 'getNode', ({getForwards, getNode}, cbk) => {
if (!via) {
return cbk(null, getForwards.forwards);
}
const channelIds = uniq(getNode.channels.map(({id}) => id));
const forwards = getForwards.forwards.filter(forward => {
if (channelIds.indexOf(forward.incoming_channel) !== notFound) {
return true;
}
if (channelIds.indexOf(forward.outgoing_channel) !== notFound) {
return true;
}
return false;
});
return cbk(null, forwards);
}],
// Total earnings
totalEarned: ['forwards', ({forwards}, cbk) => {
return cbk(null, forwards.reduce((sum, {fee}) => sum + fee, Number()));
}],
@ -89,15 +120,15 @@ module.exports = ({days, lnd}, cbk) => {
// Fees earned
fees: [
'getForwards',
'forwards',
'measure',
'segments',
({getForwards, measure, segments}, cbk) =>
({forwards, measure, segments}, cbk) =>
{
const fees = [...Array(segments)].map((_, i) => {
const segment = moment().subtract(i, measure);
const segmentForwards = getForwards.forwards.filter(forward => {
const segmentForwards = forwards.filter(forward => {
const forwardDate = moment(forward.created_at);
if (segment.year() !== forwardDate.year()) {
@ -132,16 +163,32 @@ module.exports = ({days, lnd}, cbk) => {
'totalEarned',
({fees, measure, start, totalEarned}, cbk) =>
{
const feesEarned = `Fees earned in ${fees.length} ${measure}s`;
const since = `since ${start.calendar()}`;
const duration = `Earned in ${fees.length} ${measure}s`;
const earned = (totalEarned / 1e8).toFixed(8);
const since = `since ${start.calendar().toLowerCase()}`;
return cbk(null, `${feesEarned} ${since}. Total: ${earned}`);
return cbk(null, `${duration} ${since}. Total: ${earned}`);
}],
// Summary title of the fees earned
title: ['getNode', ({getNode}, cbk) => {
const title = 'Routing fees earned';
if (!via) {
return cbk(null, title);
}
return cbk(null, `${title} via ${getNode.alias}`);
}],
// Earnings
earnings: ['description', 'fees', ({description, fees}, cbk) => {
return cbk(null, {description, fees});
earnings: [
'description',
'fees',
'title',
({description, fees, title}, cbk) =>
{
return cbk(null, {description, fees, title});
}],
},
returnResult({reject, resolve, of: 'earnings'}, cbk));

View file

@ -0,0 +1,50 @@
const {test} = require('tap');
const lndDirectory = require('./../../lnd/lnd_directory');
const tests = [
{
args: {},
description: 'Operating system methods are required',
error: 'ExpectedOperatingSytemMethodsToDetermineLndDirectory',
},
{
args: {os: {}},
description: 'Homedir method is required',
error: 'ExpectedHomedirFunctionToDetermineLndDirectory',
},
{
args: {os: {homedir: () => 'homedir'}},
description: 'Platform method is required',
error: 'ExpectedPlatformFunctionToDetermineLndDirectory',
},
{
args: {os: {homedir: () => 'homedir', platform: () => 'darwin'}},
description: 'Mac directory is returned',
expected: {path: 'homedir/Library/Application Support/Lnd'},
},
{
args: {os: {homedir: () => 'homedir', platform: () => 'win32'}},
description: 'Windows directory is returned',
expected: {path: 'homedir/Lnd'},
},
{
args: {os: {homedir: () => 'homedir', platform: () => 'linux'}},
description: 'Linux directory is returned',
expected: {path: 'homedir/.lnd'},
},
];
tests.forEach(({args, description, error, expected}) => {
return test(description, async ({end, equal, throws}) => {
if (!!error) {
throws(() => lndDirectory(args), new Error(error), 'Got expected error');
} else {
const {path} = await lndDirectory(args);
equal(path, expected.path, 'Got expected path');
}
return end();
});
});

View file

@ -21,12 +21,12 @@ const tests = [
args: {data: 'elements'},
description: 'Data returns a chart and description',
expected: [
'',
'\n title\n',
' 3.00 ┤ ╭ \n 2.87 ┤ │ \n 2.73 ┤ │ \n 2.60 ┤ │ \n 2.47 ┤ │ \n 2.33 ┤ │ \n 2.20 ┤ │ \n 2.07 ┤ │ \n 1.93 ┤╭╯ \n 1.80 ┤│ \n 1.67 ┤│ \n 1.53 ┤│ \n 1.40 ┤│ \n 1.27 ┤│ \n 1.13 ┤│ \n 1.00 ┼╯ ',
'\n description',
'\n description',
'',
],
res: {description: 'description', elements: [1,2,3]},
res: {description: 'description', elements: [1,2,3], title: 'title'},
},
];
@ -52,7 +52,7 @@ tests.forEach(({args, description, error, expected, res}) => {
logger,
data: args.data,
resolve: () => {
deepIs(loggedInfo, expected, 'Got expected info');
deepIs(loggedInfo.join('\n'), expected.join('\n'), 'Got expected info');
return end();
},

View file

@ -0,0 +1,62 @@
const {test} = require('tap');
const {getFeesChart} = require('./../../routing');
const tests = [
{
args: {},
description: 'Days duration is required to get fees chart',
error: [400, 'ExpectedNumberOfDaysToGetFeesOverForChart'],
},
{
args: {days: 1},
description: 'LND is required to get fees chart',
error: [400, 'ExpectedLndToGetFeesChart'],
},
{
args: {
days: 1,
lnd: {
default: {
forwardingHistory: ({}, cbk) => cbk(null, {
forwarding_events: [],
last_offset_index: '0',
}),
getNodeInfo: ({}, cbk) => cbk(null, {
channels: [],
node: {
addresses: [],
alias: 'alias',
color: '#000000',
last_update: '1',
pub_key: 'a',
},
num_channels: 1,
total_capacity: '1',
}),
},
},
},
description: 'Fee earnings chart data is returned',
expected: {
fees: '0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0',
title: 'Routing fees earned',
},
},
];
tests.forEach(({args, description, error, expected}) => {
return test(description, async ({end, equal, rejects}) => {
if (!!error) {
rejects(getFeesChart(args), error, 'Got expected error');
} else {
const {description, fees, title} = await getFeesChart(args);
equal(!!description, true, 'Got description');
equal(fees.join(','), expected.fees, 'Got expected fees');
equal(title, expected.title, 'Got expected title');
}
return end();
});
});

250
wallets/fund_dev.js Normal file
View file

@ -0,0 +1,250 @@
const {stringify} = require('querystring');
const {URL} = require('url');
const asyncAuto = require('async/auto');
const {parsePaymentRequest} = require('ln-service');
const {payViaPaymentRequest} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const {accounts} = require('./funding');
const {getExchangeRates} = require('./../fiat');
const {probeDestination} = require('./../network');
const {shuffle} = require('./../arrays');
const baseUrl = 'https://github.com/alexbosworth/balanceofsatoshis/issues/new';
const centsPerDollar = 100;
const centsPrecision = 3;
const currency = 'USD';
const daysPerMonth = 30;
const delayMs = 1000;
const maxFee = 1337;
const maxPathfindingTimeMs = 1000 * 60 * 10;
const mtokensPerToken = BigInt(1e3);
const satsPerBtc = 1e8;
const tippinApi = 'https://api.tippin.me/v1/public/addinvoice';
/** Send some money to a worthy tippin.me receiver
{
[is_dry_run]: <Avoid Actually Sending Money Bool>
lnd: <Authenticated LND gRPC API Object>
logger: <Winston Logger Object>
request: <Request Function>
[to_twitter_account]: <Send Money to Twitter Account String>
tokens: <Send Tokens Number>
}
@returns via cbk or Promise
{
[payment]: {
proof: <Preimage Hex String>
routing_fee: <Routing Fee Tokens Number>
sent: <Tokens Number>
}
}
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.lnd) {
return cbk([400, 'ExpectedLndToFundDev']);
}
if (!args.logger) {
return cbk([400, 'ExpectedLoggerToFundDev']);
}
if (!args.request) {
return cbk([400, 'ExpectedRequestFunctionToFundDev']);
}
if (!args.tokens) {
return cbk([400, 'ExpectedTokensToFundDev']);
}
return cbk();
},
// Get exchange rate
getRate: ['validate', ({}, cbk) => getExchangeRates({symbols: []}, cbk)],
// Select a recipient
recipient: ['validate', ({}, cbk) => {
// Exit early when a @name was specified
if (/^@.*/.test(args.to_twitter_account)) {
return cbk(null, args.to_twitter_account.slice('@'.length));
}
// Exit early when a url is specified
if (/^https:\/\/twitter\.com\/.*/.test(args.to_twitter_account)) {
const {pathname} = new URL(args.to_twitter_account);
return cbk(null, pathname.slice('/'.length));
}
// Exit early when a username was specified
if (!!args.to_twitter_account) {
return cbk(null, args.to_twitter_account);
}
const [lucky] = shuffle({array: accounts}).shuffled;
return cbk(null, lucky.twitter_username);
}],
// Request an invoice for the recipient
getRequest: ['getRate', 'recipient', ({getRate, recipient}, cbk) => {
const {tickers} = getRate;
const [{rate}] = tickers;
const fiat = args.tokens / satsPerBtc * rate / centsPerDollar;
const monthly = (fiat * daysPerMonth).toFixed(centsPrecision);
args.logger.info({
will_fund: `https://twitter.com/@${recipient}`,
amount_to_send: args.tokens,
amount_to_send_fiat: `$${fiat.toFixed(centsPrecision)} ${currency}`,
if_daily_monthly_amount: `$${monthly} ${currency}`,
});
return args.request({
json: true,
url: `${tippinApi}/${recipient}/${args.tokens}`,
},
(err, r, invoice) => {
if (!!err) {
return cbk([503, 'UnexpectedErrorContactingTippinMeApi', {err}]);
}
if (!r) {
return cbk([503, 'UnexpectedLackOfResponseFromTippinMeApi']);
}
if (!!invoice && !!invoice.error) {
return cbk([503, 'TippinMeInvoiceFailed', {err: invoice.message}]);
}
if (!invoice || !invoice.lnreq) {
return cbk([503, 'UnexpectedResponseFromTippinMeApi']);
}
const request = invoice.lnreq;
try {
parsePaymentRequest({request});
} catch (err) {
return cbk([503, 'FailedToPraseTippinMePaymentRequest']);
}
if (parsePaymentRequest({request}).tokens !== args.tokens) {
return cbk([503, 'UnexpectedTokensValueFromTippinMeRequest']);
}
return cbk(null, request);
});
}],
// Delay execution
delay: ['getRequest', ({}, cbk) => setTimeout(cbk, delayMs)],
// Probe
probe: ['delay', 'getRequest', ({getRequest}, cbk) => {
args.logger.info({checking_route_to_send: args.tokens});
return probeDestination({
lnd: args.lnd,
logger: args.logger,
request: getRequest,
},
cbk);
}],
// Pay
pay: ['getRequest', 'probe', ({getRequest}, cbk) => {
if (!!args.is_dry_run) {
return cbk();
}
return payViaPaymentRequest({
lnd: args.lnd,
max_fee: maxFee,
pathfinding_timeout: maxPathfindingTimeMs,
request: getRequest,
},
cbk);
}],
// Get a shortened champion URL
championUrl: [
'getRequest',
'pay',
'recipient',
({getRequest, pay, recipient}, cbk) =>
{
if (!pay) {
return cbk();
}
const body = [
`I champion https://twitter.com/${recipient}!`,
'',
`+${args.tokens}`,
'',
`${getRequest} ${pay.secret}`,
];
const issueTemplate = {
body: body.join('\n'),
title: `Champion Funding Recipient: @${recipient}`,
};
return args.request({
form: {url: `${baseUrl}?${stringify(issueTemplate)}`},
method: 'POST',
url: 'https://git.io',
},
(err, r) => {
if (!!err) {
args.logger.error({err});
return cbk();
}
if (!r || !r.headers.location) {
args.logger.error({err: [503, 'ExpectedChampionLocationHeader']});
return cbk();
}
return cbk(null, r.headers.location);
});
}],
// Final result
result: [
'championUrl',
'pay',
'recipient',
({championUrl, pay, recipient}, cbk) =>
{
if (!pay) {
return cbk(null, {});
}
return cbk(null, {
payment: {
recipient: `https://twitter.com/${recipient}`,
funded: args.tokens,
routing_fee: Number(BigInt(pay.fee_mtokens) / mtokensPerToken),
champion_this_recipient: championUrl,
},
});
}],
},
returnResult({reject, resolve, of: 'result'}, cbk));
});
};

103
wallets/funding.json Normal file
View file

@ -0,0 +1,103 @@
{
"accounts": [
{
"twitter_username": "8bitgomes"
},
{
"twitter_username": "actuallycarlakc"
},
{
"twitter_username": "alexbosworth"
},
{
"twitter_username": "benthecarman"
},
{
"twitter_username": "bitconner"
},
{
"twitter_username": "blockstream"
},
{
"twitter_username": "breez_tech"
},
{
"twitter_username": "eiprol"
},
{
"twitter_username": "fiatjaf"
},
{
"twitter_username": "fulmolightning"
},
{
"twitter_username": "gugol"
},
{
"twitter_username": "jackmallers"
},
{
"twitter_username": "jamaljsr"
},
{
"twitter_username": "joostjgr"
},
{
"twitter_username": "juscamarena"
},
{
"twitter_username": "lightninginabox"
},
{
"twitter_username": "lightningk0ala"
},
{
"twitter_username": "ln_zap"
},
{
"twitter_username": "mrfelton"
},
{
"twitter_username": "negrunch"
},
{
"twitter_username": "niftynei"
},
{
"twitter_username": "pierre_rochard"
},
{
"twitter_username": "provoost"
},
{
"twitter_username": "renepickhardt"
},
{
"twitter_username": "roquerrt"
},
{
"twitter_username": "rusty_twit"
},
{
"twitter_username": "snyke"
},
{
"twitter_username": "stadicus3000"
},
{
"twitter_username": "starkness"
},
{
"twitter_username": "stepansnigirev"
},
{
"twitter_username": "wbobeirne"
},
{
"twitter_username": "whiteyhat"
},
{
"twitter_username": "zeusln"
}
]
}

View file

@ -1,4 +1,5 @@
const fundDev = require('./fund_dev');
const getReport = require('./get_report');
const unlockWallet = require('./unlock_wallet');
module.exports = {getReport, unlockWallet};
module.exports = {fundDev, getReport, unlockWallet};