@alexbosworth/fiat 1.0.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +93 -1
- package/arrays/index.js +3 -0
- package/arrays/sort_by.js +44 -0
- package/chain/add_days.js +22 -0
- package/chain/blocks/block_header_hash.js +29 -0
- package/chain/blocks/block_header_time.js +25 -0
- package/chain/blocks/find_complete_day.js +90 -0
- package/chain/blocks/find_height_for_time.js +93 -0
- package/chain/blocks/get_block_hash.js +60 -0
- package/chain/blocks/get_block_header.js +45 -0
- package/chain/blocks/get_block_payments.js +100 -0
- package/chain/blocks/get_blocks_for_date.js +110 -0
- package/chain/blocks/get_median_time.js +92 -0
- package/chain/blocks/index.js +19 -0
- package/chain/blocks/payment_outputs_from_block.js +178 -0
- package/chain/blocks/sample_heights.js +33 -0
- package/chain/blocks/window_heights.js +34 -0
- package/chain/chain_rates.js +129 -0
- package/chain/constants.json +26 -0
- package/chain/estimates/check_known_estimates.js +119 -0
- package/chain/estimates/current_estimates.js +66 -0
- package/chain/estimates/estimate_and_remember.js +199 -0
- package/chain/estimates/estimate_day_price.js +361 -0
- package/chain/estimates/estimate_record.js +107 -0
- package/chain/estimates/estimate_windows.js +246 -0
- package/chain/estimates/find_starting_day.js +86 -0
- package/chain/estimates/forget_estimates.js +113 -0
- package/chain/estimates/get_chain_price.js +167 -0
- package/chain/estimates/get_price_for_date.js +607 -0
- package/chain/estimates/get_price_from_blocks.js +281 -0
- package/chain/estimates/get_provisional_price.js +163 -0
- package/chain/estimates/index.js +25 -0
- package/chain/estimates/is_alias_ratio.js +38 -0
- package/chain/estimates/is_supported_estimate.js +81 -0
- package/chain/estimates/nearest_estimate.js +49 -0
- package/chain/estimates/price_window.js +25 -0
- package/chain/estimates/remember_estimate.js +83 -0
- package/chain/estimates/store_estimate.js +198 -0
- package/chain/histogram/best_position.js +63 -0
- package/chain/histogram/histogram_excess.js +50 -0
- package/chain/histogram/histogram_peak.js +87 -0
- package/chain/histogram/implied_prices_histogram.js +53 -0
- package/chain/histogram/index.js +33 -0
- package/chain/histogram/is_round_tokens.js +26 -0
- package/chain/histogram/kernel_width.js +25 -0
- package/chain/histogram/lower_bound.js +28 -0
- package/chain/histogram/noise_of_neighbors.js +28 -0
- package/chain/histogram/noise_strength.js +35 -0
- package/chain/histogram/noise_strengths.js +29 -0
- package/chain/histogram/outputs_histogram.js +80 -0
- package/chain/histogram/parabola_peak.js +28 -0
- package/chain/histogram/payments_near_position.js +31 -0
- package/chain/histogram/peak_width.js +37 -0
- package/chain/histogram/position_score.js +38 -0
- package/chain/histogram/score_position.js +94 -0
- package/chain/histogram/smooth_bins.js +29 -0
- package/chain/histogram/sorted_neighbors.js +89 -0
- package/chain/histogram/spike_strengths.js +33 -0
- package/chain/histogram/template_agreement.js +27 -0
- package/chain/index.js +3 -0
- package/chain/known/earliest_supported_day.js +12 -0
- package/chain/known/index.js +11 -0
- package/chain/known/known_prices.js +43 -0
- package/chain/known/known_prices.json +1836 -0
- package/chain/known/method_version.js +26 -0
- package/chain/known/parse_known_prices.js +67 -0
- package/chain/outputs/alias_shifts.js +31 -0
- package/chain/outputs/cap_near_round_values.js +83 -0
- package/chain/outputs/cap_repeated_values.js +33 -0
- package/chain/outputs/dollar_bin_for_price.js +19 -0
- package/chain/outputs/estimate_from_outputs.js +321 -0
- package/chain/outputs/index.js +11 -0
- package/chain/outputs/peak_around_price.js +86 -0
- package/chain/outputs/price_for_dollar_bin.js +21 -0
- package/chain/outputs/price_from_outputs.js +98 -0
- package/chain/outputs/refine_price.js +73 -0
- package/chain/outputs/round_amounts.js +49 -0
- package/chain/outputs/template.json +22 -0
- package/coinbase/constants.json +5 -0
- package/coinbase/get_coinbase_day_candles.js +155 -0
- package/coinbase/get_coinbase_historic_price.js +135 -0
- package/coinbase/index.js +7 -1
- package/dates/constants.json +3 -0
- package/dates/day_of_date.js +26 -0
- package/dates/index.js +5 -0
- package/dates/is_calendar_day.js +16 -0
- package/dates/time_of_date.js +61 -0
- package/docs/METHODOLOGY.md +984 -0
- package/index.js +2 -1
- package/package.json +29 -11
- package/rates/get_prices.js +51 -8
- package/storage/constants.json +4 -0
- package/storage/get_prices_file.js +169 -0
- package/storage/index.js +4 -0
- package/storage/is_prices.js +31 -0
- package/storage/put_prices_file.js +252 -0
- package/storage/temp_path.js +22 -0
- package/.gitattributes +0 -1
- package/.tm_properties +0 -1
- package/test/coinbase/test_get_coinbase_current_price.js +0 -75
- package/test/coindesk/test_get_coindesk_current_price.js +0 -93
- package/test/coingecko/test_get_coingecko_rates.js +0 -91
- package/test/rates/test_get_current_price.js +0 -72
- package/test/rates/test_get_prices.js +0 -76
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
- Add `chainRates` to make a `getChainRate` method that gets the USD price of
|
|
6
|
+
BTC in cents on the day of a date from chain data via LND, for dates from
|
|
7
|
+
October 20, 2021, pricing each day as the time weighted median of windows
|
|
8
|
+
of about two hours of its blocks and refusing a day whose price the data do
|
|
9
|
+
not support rather than guessing, answering the days up to the release from
|
|
10
|
+
prices shipped with the package and keeping derived prices in a versioned
|
|
11
|
+
prices file that records how every price was derived, along with the
|
|
12
|
+
provisional price of the current day, which is reused for about an hour,
|
|
13
|
+
removing a stored price for good once a reorganization replaces its blocks,
|
|
14
|
+
and keeping a stored price from blocks that LND has not reached yet, as
|
|
15
|
+
when it is catching up after being down
|
|
16
|
+
- Add `date` to `getPrices` to get the rates at the hour of a date from
|
|
17
|
+
Coinbase Exchange, in USD, EUR or GBP, back to 2015 without an API key,
|
|
18
|
+
fetching the hourly prices of a day once for all of its dates when the same
|
|
19
|
+
request function is used
|
|
20
|
+
|
|
21
|
+
### Breaking Changes
|
|
22
|
+
|
|
23
|
+
- Require node.js 22 or higher
|
|
24
|
+
|
|
25
|
+
## 2.0.0
|
|
26
|
+
|
|
27
|
+
- Require node.js 16 or higher
|
|
28
|
+
|
|
3
29
|
## 1.0.4
|
|
4
30
|
|
|
5
31
|
- Add `getPrices` method to get fiat prices
|
package/README.md
CHANGED
|
@@ -2,11 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
Methods for working with fiat prices.
|
|
4
4
|
|
|
5
|
+
`chainRates` derives prices from chain data alone; how and why is described in
|
|
6
|
+
[docs/METHODOLOGY.md](docs/METHODOLOGY.md). The prices of the days up to a
|
|
7
|
+
release ship with the package in `chain/known/known_prices.json`.
|
|
8
|
+
|
|
9
|
+
## `chainRates`
|
|
10
|
+
|
|
11
|
+
Make a method that gets historic USD prices of BTC from chain data
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
fs: {
|
|
15
|
+
getFile: <Read File Contents Function> (path, cbk) => {}
|
|
16
|
+
makeDirectory: <Make Directory Function> (path, cbk) => {}
|
|
17
|
+
renameFile: <Rename File Function> (from, to, cbk) => {}
|
|
18
|
+
writeFile: <Write File Contents Function> (path, contents, cbk) => {}
|
|
19
|
+
}
|
|
20
|
+
lnd: <Authenticated LND API Object>
|
|
21
|
+
[logger]: <Winston Logger Object>
|
|
22
|
+
path: <Prices File Path String>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@throws
|
|
26
|
+
<Error>
|
|
27
|
+
|
|
28
|
+
@returns
|
|
29
|
+
{
|
|
30
|
+
getChainRate: <Get Chain Rate Method> ({
|
|
31
|
+
currency: <Currency To Price String> // BTC
|
|
32
|
+
date: <Date To Price ISO 8601 Date String> // priced on its day in UTC
|
|
33
|
+
fiat: <Fiat Currency To Price In String> // USD
|
|
34
|
+
}, cbk) => (err, {
|
|
35
|
+
cents: <US Cents Per BTC on the Day of the Date Number>
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
5
39
|
## `getPrices`
|
|
6
40
|
|
|
7
41
|
Get exchange rates from a rate provider
|
|
8
42
|
|
|
9
43
|
{
|
|
44
|
+
[date]: <Rates At the Hour of ISO 8601 Date String>
|
|
10
45
|
from: <From Rate Provider String>
|
|
11
46
|
request: <Request Function> {url}, (err, {statusCode}, body) => {}
|
|
12
47
|
symbols: [<Fiat Symbol String>] // empty defaults to USD
|
|
@@ -15,8 +50,65 @@ Get exchange rates from a rate provider
|
|
|
15
50
|
@returns via cbk or Promise
|
|
16
51
|
{
|
|
17
52
|
tickers: [{
|
|
18
|
-
date: <Rate Updated At ISO 8601 Date String>
|
|
53
|
+
date: <Rate Updated At or Hour Priced At ISO 8601 Date String>
|
|
19
54
|
rate: <Exchange Rate in Cents Number>
|
|
20
55
|
ticker: <Ticker Symbol String>
|
|
21
56
|
}]
|
|
22
57
|
}
|
|
58
|
+
|
|
59
|
+
## Scripts
|
|
60
|
+
|
|
61
|
+
- `npm test` runs the quick tests and reports their coverage
|
|
62
|
+
- `npm run test-all` also runs the slower tests in `test/slow`, which derive
|
|
63
|
+
prices from simulated chains, and reports the coverage of all the tests
|
|
64
|
+
- `npm run check-prices` compares chain prices with the hourly prices of days
|
|
65
|
+
on Coinbase Exchange, with the options described under Validation in the
|
|
66
|
+
methodology. Without days it checks the latest three complete days, or as
|
|
67
|
+
many as `--days`
|
|
68
|
+
- `npm run update-known-prices` brings the shipped prices up to date, as
|
|
69
|
+
described under Releasing
|
|
70
|
+
- `npm publish` first runs `scripts/check_known_prices.js`, which stops the
|
|
71
|
+
publish when the shipped prices are not ready for a release
|
|
72
|
+
|
|
73
|
+
For example, to compare two days priced without a neighboring price as a
|
|
74
|
+
guide:
|
|
75
|
+
|
|
76
|
+
npm run check-prices -- 2024-08-05 2024-08-08 --unguided
|
|
77
|
+
|
|
78
|
+
The scripts that read the chain reach Bitcoin Core at `http://127.0.0.1:8332`
|
|
79
|
+
with the cookie in the default data directory, or as set by `BITCOIN_RPC_URL`,
|
|
80
|
+
`BITCOIN_RPC_COOKIE`, or `BITCOIN_RPC_USER` and `BITCOIN_RPC_PASSWORD`.
|
|
81
|
+
|
|
82
|
+
## Releasing
|
|
83
|
+
|
|
84
|
+
A release normally ships the prices of the days up to it, so that lookups of
|
|
85
|
+
those days do not have to derive them from the chain:
|
|
86
|
+
|
|
87
|
+
1. With Bitcoin Core synced, run `npm run update-known-prices`. It derives
|
|
88
|
+
the prices of the last week of shipped days again, which have to come out
|
|
89
|
+
the same, and then adds a price for every complete day after them. Each
|
|
90
|
+
new price has to be within the range its day traded in on Coinbase
|
|
91
|
+
Exchange and within 5% of the median of its hourly prices there, and a day
|
|
92
|
+
that fails a check stops the run without changing the table
|
|
93
|
+
2. Look over the summary at the end of the run, which shows how far the new
|
|
94
|
+
prices are from the median of the hourly prices of their days. A higher
|
|
95
|
+
median or 95th percentile, or a bias, means the prices agree with the
|
|
96
|
+
exchange less well than before, even though every day passed its checks
|
|
97
|
+
3. Run `npm run test-all`, which includes the slow tests that `npm test`
|
|
98
|
+
leaves out
|
|
99
|
+
4. Commit `chain/known/known_prices.json` with the release
|
|
100
|
+
5. Run `npm publish`, which checks that the shipped prices are of the current
|
|
101
|
+
version of the method, start on the earliest supported day, have a price
|
|
102
|
+
for every day and reach to within a week of the present, and stops when
|
|
103
|
+
they don't
|
|
104
|
+
|
|
105
|
+
When the method changes version, every day is derived again, which takes
|
|
106
|
+
several hours, and the summary also shows how far the prices moved from the
|
|
107
|
+
last table.
|
|
108
|
+
|
|
109
|
+
To release without new prices, publish with `npm publish --ignore-scripts`,
|
|
110
|
+
which skips the check. The table still has to be of the current version of
|
|
111
|
+
the method, since prices of another version are not used, so first run
|
|
112
|
+
`node scripts/check_known_prices.js` and see that the age of the prices is the
|
|
113
|
+
only problem it reports. Lookups of the days after the last shipped price then
|
|
114
|
+
derive them from the chain.
|
package/arrays/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const {isArray} = Array;
|
|
2
|
+
|
|
3
|
+
const equalTo = 0;
|
|
4
|
+
const greaterThan = 1;
|
|
5
|
+
const lessThan = -1;
|
|
6
|
+
|
|
7
|
+
/** Sort array by attribute, lowest to highest
|
|
8
|
+
|
|
9
|
+
{
|
|
10
|
+
array: [<Array Element Object>]
|
|
11
|
+
attribute: <Attribute String>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@throws
|
|
15
|
+
<Error>
|
|
16
|
+
|
|
17
|
+
@returns
|
|
18
|
+
{
|
|
19
|
+
sorted: [<Sorted Element Object>]
|
|
20
|
+
}
|
|
21
|
+
*/
|
|
22
|
+
module.exports = ({array, attribute}) => {
|
|
23
|
+
if (!isArray(array)) {
|
|
24
|
+
throw new Error('ExpectedArrayToSortByAttribute');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!attribute) {
|
|
28
|
+
throw new Error('ExpectedAttributeToSortArrayBy');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sorted = array.slice().sort((a, b) => {
|
|
32
|
+
if (a[attribute] > b[attribute]) {
|
|
33
|
+
return greaterThan;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (b[attribute] > a[attribute]) {
|
|
37
|
+
return lessThan;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return equalTo;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {sorted};
|
|
44
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const {dayLength} = require('./../dates/constants');
|
|
2
|
+
const {msPerDay} = require('./constants');
|
|
3
|
+
|
|
4
|
+
const dayStart = day => Date.parse(`${day}T00:00:00.000Z`);
|
|
5
|
+
|
|
6
|
+
/** Add days to a day
|
|
7
|
+
|
|
8
|
+
{
|
|
9
|
+
day: <Day YYYY-MM-DD String>
|
|
10
|
+
days: <Days to Add Number>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@returns
|
|
14
|
+
{
|
|
15
|
+
day: <Day YYYY-MM-DD String>
|
|
16
|
+
}
|
|
17
|
+
*/
|
|
18
|
+
module.exports = ({day, days}) => {
|
|
19
|
+
const date = new Date(dayStart(day) + days * msPerDay);
|
|
20
|
+
|
|
21
|
+
return {day: date.toISOString().substring(Number(), dayLength)};
|
|
22
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const {createHash} = require('crypto');
|
|
2
|
+
|
|
3
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
4
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
5
|
+
const isHeaderHex = n => /^[0-9A-F]{160}$/i.test(n);
|
|
6
|
+
const sha256 = buffer => createHash('sha256').update(buffer).digest();
|
|
7
|
+
|
|
8
|
+
/** Get the hash of a block from its header
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
header: <Raw Block Header Hex String>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@throws
|
|
15
|
+
<Error>
|
|
16
|
+
|
|
17
|
+
@returns
|
|
18
|
+
{
|
|
19
|
+
hash: <Block Hash Hex String>
|
|
20
|
+
}
|
|
21
|
+
*/
|
|
22
|
+
module.exports = ({header}) => {
|
|
23
|
+
if (!isHeaderHex(header)) {
|
|
24
|
+
throw new Error('ExpectedRawBlockHeaderHexStringToGetBlockHeaderHash');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Block ids are the double SHA256 of the header, shown byte-reversed
|
|
28
|
+
return {hash: bufferAsHex(sha256(sha256(hexAsBuffer(header))).reverse())};
|
|
29
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
2
|
+
const isHeaderHex = n => /^([0-9A-F]{2}){80,}$/i.test(n);
|
|
3
|
+
const timeOffset = 68;
|
|
4
|
+
|
|
5
|
+
/** Get the time of a block from its header
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
header: <Raw Block Header or Block Hex String>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@throws
|
|
12
|
+
<Error>
|
|
13
|
+
|
|
14
|
+
@returns
|
|
15
|
+
{
|
|
16
|
+
time: <Block Header Timestamp Epoch Seconds Number>
|
|
17
|
+
}
|
|
18
|
+
*/
|
|
19
|
+
module.exports = ({header}) => {
|
|
20
|
+
if (!isHeaderHex(header)) {
|
|
21
|
+
throw new Error('ExpectedRawBlockHeaderHexStringToGetBlockHeaderTime');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {time: hexAsBuffer(header).readUInt32LE(timeOffset)};
|
|
25
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncDetectSeries = require('async/detectSeries');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
|
|
5
|
+
const addDays = require('./../add_days');
|
|
6
|
+
const {dayLength} = require('./../../dates/constants');
|
|
7
|
+
const getBlocksForDate = require('./get_blocks_for_date');
|
|
8
|
+
const getMedianTime = require('./get_median_time');
|
|
9
|
+
const {msPerSecond} = require('./../constants');
|
|
10
|
+
|
|
11
|
+
const isHeight = n => Number.isInteger(n) && n >= 0;
|
|
12
|
+
const maxDaysBack = 7;
|
|
13
|
+
const yesterday = -1;
|
|
14
|
+
|
|
15
|
+
/** Find the most recent complete day of blocks before the chain tip
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
height: <Current Best Chain Height Number>
|
|
19
|
+
lnd: <Authenticated LND API Object>
|
|
20
|
+
[times]: <Block Times By Height Map Object>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@returns via cbk or Promise
|
|
24
|
+
{
|
|
25
|
+
day: <Day YYYY-MM-DD String>
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
module.exports = (args, cbk) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
return asyncAuto({
|
|
31
|
+
// Check arguments
|
|
32
|
+
validate: cbk => {
|
|
33
|
+
if (!isHeight(args.height)) {
|
|
34
|
+
return cbk([400, 'ExpectedCurrentChainHeightToFindCompleteDay']);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!args.lnd) {
|
|
38
|
+
return cbk([400, 'ExpectedAuthenticatedLndToFindCompleteDay']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return cbk();
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// Block times are kept to avoid retrieving them twice
|
|
45
|
+
times: ['validate', ({}, cbk) => cbk(null, args.times || new Map())],
|
|
46
|
+
|
|
47
|
+
// Get the median time of the chain tip
|
|
48
|
+
getTipTime: ['times', ({times}, cbk) => {
|
|
49
|
+
return getMedianTime({
|
|
50
|
+
times,
|
|
51
|
+
height: args.height,
|
|
52
|
+
lnd: args.lnd,
|
|
53
|
+
},
|
|
54
|
+
cbk);
|
|
55
|
+
}],
|
|
56
|
+
|
|
57
|
+
// Walk back from the day before the tip until a complete day is found
|
|
58
|
+
findDay: ['getTipTime', 'times', ({getTipTime, times}, cbk) => {
|
|
59
|
+
const tipDate = new Date(getTipTime.time * msPerSecond).toISOString();
|
|
60
|
+
|
|
61
|
+
const tipDay = tipDate.substring(Number(), dayLength);
|
|
62
|
+
|
|
63
|
+
const days = [...Array(maxDaysBack).keys()].map(daysBack => {
|
|
64
|
+
return addDays({day: tipDay, days: yesterday - daysBack}).day;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return asyncDetectSeries(days, (date, cbk) => {
|
|
68
|
+
return getBlocksForDate({
|
|
69
|
+
date,
|
|
70
|
+
times,
|
|
71
|
+
height: args.height,
|
|
72
|
+
lnd: args.lnd,
|
|
73
|
+
},
|
|
74
|
+
(err, res) => cbk(err, !err && !!res.is_complete));
|
|
75
|
+
},
|
|
76
|
+
cbk);
|
|
77
|
+
}],
|
|
78
|
+
|
|
79
|
+
// The most recent complete day
|
|
80
|
+
day: ['findDay', ({findDay}, cbk) => {
|
|
81
|
+
if (!findDay) {
|
|
82
|
+
return cbk([503, 'FailedToFindCompleteDayBeforeChainTip']);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return cbk(null, {day: findDay});
|
|
86
|
+
}],
|
|
87
|
+
},
|
|
88
|
+
returnResult({reject, resolve, of: 'day'}, cbk));
|
|
89
|
+
});
|
|
90
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncTimesSeries = require('async/timesSeries');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
|
|
5
|
+
const getMedianTime = require('./get_median_time');
|
|
6
|
+
|
|
7
|
+
const {ceil} = Math;
|
|
8
|
+
const {floor} = Math;
|
|
9
|
+
const isHeight = n => Number.isInteger(n) && n >= 0;
|
|
10
|
+
const {log2} = Math;
|
|
11
|
+
|
|
12
|
+
/** Find the heights where the median time past of blocks crosses a time
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
height: <Current Best Chain Height Number>
|
|
16
|
+
lnd: <Authenticated LND API Object>
|
|
17
|
+
low: <Lowest Height to Consider Number>
|
|
18
|
+
time: <Time Epoch Seconds Number>
|
|
19
|
+
[times]: <Block Times By Height Map Object>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@returns via cbk or Promise
|
|
23
|
+
{
|
|
24
|
+
at_or_after: <First Height With Median Time At Or After Time Number>
|
|
25
|
+
before: <Last Height With Median Time Before Time Number>
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
module.exports = (args, cbk) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
return asyncAuto({
|
|
31
|
+
// Check arguments
|
|
32
|
+
validate: cbk => {
|
|
33
|
+
if (!isHeight(args.height)) {
|
|
34
|
+
return cbk([400, 'ExpectedCurrentChainHeightToFindHeightForTime']);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!args.lnd) {
|
|
38
|
+
return cbk([400, 'ExpectedAuthenticatedLndToFindHeightForTime']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!isHeight(args.low)) {
|
|
42
|
+
return cbk([400, 'ExpectedLowestHeightToFindHeightForTime']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!isHeight(args.time)) {
|
|
46
|
+
return cbk([400, 'ExpectedTimeToFindHeightForTime']);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return cbk();
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Binary search for the first height with a median time at or after
|
|
53
|
+
search: ['validate', ({}, cbk) => {
|
|
54
|
+
const bounds = {high: args.height + 1, low: args.low};
|
|
55
|
+
const {lnd} = args;
|
|
56
|
+
const times = args.times || new Map();
|
|
57
|
+
|
|
58
|
+
// A binary search takes a bounded number of steps
|
|
59
|
+
const steps = ceil(log2(bounds.high - bounds.low + 1)) + 1;
|
|
60
|
+
|
|
61
|
+
return asyncTimesSeries(steps, (i, cbk) => {
|
|
62
|
+
// Exit early when the search has narrowed to a single height
|
|
63
|
+
if (bounds.low >= bounds.high) {
|
|
64
|
+
return cbk();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const mid = floor((bounds.low + bounds.high) / 2);
|
|
68
|
+
|
|
69
|
+
return getMedianTime({lnd, times, height: mid}, (err, res) => {
|
|
70
|
+
if (!!err) {
|
|
71
|
+
return cbk(err);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (res.time >= args.time) {
|
|
75
|
+
bounds.high = mid;
|
|
76
|
+
} else {
|
|
77
|
+
bounds.low = mid + 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return cbk();
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
err => cbk(err, bounds.low));
|
|
84
|
+
}],
|
|
85
|
+
|
|
86
|
+
// Determine the crossing heights
|
|
87
|
+
crossing: ['search', ({search}, cbk) => {
|
|
88
|
+
return cbk(null, {at_or_after: search, before: search - 1});
|
|
89
|
+
}],
|
|
90
|
+
},
|
|
91
|
+
returnResult({reject, resolve, of: 'crossing'}, cbk));
|
|
92
|
+
});
|
|
93
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {returnResult} = require('asyncjs-util');
|
|
3
|
+
|
|
4
|
+
const blockHeaderHash = require('./block_header_hash');
|
|
5
|
+
const getBlockHeader = require('./get_block_header');
|
|
6
|
+
|
|
7
|
+
const isHeight = n => Number.isInteger(n) && n >= 0;
|
|
8
|
+
|
|
9
|
+
/** Get the hash of the block at a height in the chain
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
height: <Block Height Number>
|
|
13
|
+
lnd: <Authenticated LND API Object>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@returns via cbk or Promise
|
|
17
|
+
{
|
|
18
|
+
hash: <Block Hash Hex String>
|
|
19
|
+
}
|
|
20
|
+
*/
|
|
21
|
+
module.exports = ({height, lnd}, cbk) => {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
return asyncAuto({
|
|
24
|
+
// Check arguments
|
|
25
|
+
validate: cbk => {
|
|
26
|
+
if (!isHeight(height)) {
|
|
27
|
+
return cbk([400, 'ExpectedBlockHeightToGetBlockHash']);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!lnd) {
|
|
31
|
+
return cbk([400, 'ExpectedAuthenticatedLndToGetBlockHash']);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return cbk();
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Get the block header
|
|
38
|
+
getHeader: ['validate', ({}, cbk) => getBlockHeader({height, lnd}, cbk)],
|
|
39
|
+
|
|
40
|
+
// Hash the block header
|
|
41
|
+
getHash: ['getHeader', ({getHeader}, cbk) => {
|
|
42
|
+
const hashed = (() => {
|
|
43
|
+
try {
|
|
44
|
+
return blockHeaderHash({header: getHeader.header});
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
|
|
50
|
+
// Exit early when the header cannot be hashed
|
|
51
|
+
if (!hashed.hash) {
|
|
52
|
+
return cbk([503, 'ExpectedBlockHeaderToGetBlockHash']);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return cbk(null, {hash: hashed.hash});
|
|
56
|
+
}],
|
|
57
|
+
},
|
|
58
|
+
returnResult({reject, resolve, of: 'getHash'}, cbk));
|
|
59
|
+
});
|
|
60
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {getBlockHeader} = require('ln-service');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
|
|
5
|
+
const isHeight = n => Number.isInteger(n) && n >= 0;
|
|
6
|
+
|
|
7
|
+
/** Get the header of a block in the chain using the LND chain backend
|
|
8
|
+
|
|
9
|
+
{
|
|
10
|
+
height: <Block Height Number>
|
|
11
|
+
lnd: <Authenticated LND API Object>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@returns via cbk or Promise
|
|
15
|
+
{
|
|
16
|
+
header: <Raw Block Header Hex String>
|
|
17
|
+
}
|
|
18
|
+
*/
|
|
19
|
+
module.exports = ({height, lnd}, cbk) => {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
return asyncAuto({
|
|
22
|
+
// Check arguments
|
|
23
|
+
validate: cbk => {
|
|
24
|
+
if (!isHeight(height)) {
|
|
25
|
+
return cbk([400, 'ExpectedBlockHeightToGetBlockHeader']);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!lnd) {
|
|
29
|
+
return cbk([400, 'ExpectedAuthenticatedLndToGetBlockHeader']);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return cbk();
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Get the block header
|
|
36
|
+
getHeader: ['validate', ({}, cbk) => getBlockHeader({height, lnd}, cbk)],
|
|
37
|
+
|
|
38
|
+
// The block header
|
|
39
|
+
header: ['getHeader', ({getHeader}, cbk) => {
|
|
40
|
+
return cbk(null, {header: getHeader.header});
|
|
41
|
+
}],
|
|
42
|
+
},
|
|
43
|
+
returnResult({reject, resolve, of: 'header'}, cbk));
|
|
44
|
+
});
|
|
45
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {getBlock} = require('ln-service');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
|
|
5
|
+
const {blockHeaderHexLength} = require('./../constants');
|
|
6
|
+
const blockHeaderTime = require('./block_header_time');
|
|
7
|
+
const paymentOutputsFromBlock = require('./payment_outputs_from_block');
|
|
8
|
+
|
|
9
|
+
const flatten = arr => [].concat(...arr);
|
|
10
|
+
const headerFromBlock = block => block.slice(Number(), blockHeaderHexLength);
|
|
11
|
+
const isHeight = n => Number.isInteger(n) && n >= 0;
|
|
12
|
+
|
|
13
|
+
/** Get the payment outputs and the time of a block
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
[blocks]: <Retrieved Blocks Map Object>
|
|
17
|
+
height: <Block Height Number>
|
|
18
|
+
lnd: <Authenticated LND API Object>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@returns via cbk or Promise
|
|
22
|
+
{
|
|
23
|
+
outputs: [<Payment Output Tokens Number>]
|
|
24
|
+
time: <Block Header Time Number>
|
|
25
|
+
transactions: <Payment Transactions Count Number>
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
module.exports = ({blocks, height, lnd}, cbk) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
return asyncAuto({
|
|
31
|
+
// Check arguments
|
|
32
|
+
validate: cbk => {
|
|
33
|
+
if (!!blocks && !(blocks instanceof Map)) {
|
|
34
|
+
return cbk([400, 'ExpectedBlocksMapToGetBlockPayments']);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!isHeight(height)) {
|
|
38
|
+
return cbk([400, 'ExpectedBlockHeightToGetBlockPayments']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!lnd) {
|
|
42
|
+
return cbk([400, 'ExpectedAuthenticatedLndToGetBlockPayments']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return cbk();
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Get the block unless it was already retrieved
|
|
49
|
+
getBlock: ['validate', ({}, cbk) => {
|
|
50
|
+
// Exit early when the block was already retrieved
|
|
51
|
+
if (!!blocks && blocks.has(height)) {
|
|
52
|
+
return cbk();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return getBlock({height, lnd}, cbk);
|
|
56
|
+
}],
|
|
57
|
+
|
|
58
|
+
// Pull out the payment outputs and the time of the block
|
|
59
|
+
payments: ['getBlock', ({getBlock}, cbk) => {
|
|
60
|
+
// Exit early when the block was already retrieved
|
|
61
|
+
if (!getBlock) {
|
|
62
|
+
return cbk(null, blocks.get(height));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parsed = (() => {
|
|
66
|
+
try {
|
|
67
|
+
const {transactions} = paymentOutputsFromBlock({
|
|
68
|
+
block: getBlock.block,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const header = headerFromBlock(getBlock.block);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
payments: {
|
|
75
|
+
outputs: flatten(transactions.map(tx => tx.outputs)),
|
|
76
|
+
time: blockHeaderTime({header}).time,
|
|
77
|
+
transactions: transactions.length,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return {err};
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
|
|
85
|
+
// Exit early when the block cannot be parsed
|
|
86
|
+
if (!!parsed.err) {
|
|
87
|
+
return cbk([503, 'FailedToParseBlockToGetPrice', {err: parsed.err}]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Keep the block for other estimates from the same blocks
|
|
91
|
+
if (!!blocks) {
|
|
92
|
+
blocks.set(height, parsed.payments);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return cbk(null, parsed.payments);
|
|
96
|
+
}],
|
|
97
|
+
},
|
|
98
|
+
returnResult({reject, resolve, of: 'payments'}, cbk));
|
|
99
|
+
});
|
|
100
|
+
};
|