@alexbosworth/fiat 2.0.0 → 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.
Files changed (105) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +93 -1
  3. package/arrays/index.js +3 -0
  4. package/arrays/sort_by.js +44 -0
  5. package/chain/add_days.js +22 -0
  6. package/chain/blocks/block_header_hash.js +29 -0
  7. package/chain/blocks/block_header_time.js +25 -0
  8. package/chain/blocks/find_complete_day.js +90 -0
  9. package/chain/blocks/find_height_for_time.js +93 -0
  10. package/chain/blocks/get_block_hash.js +60 -0
  11. package/chain/blocks/get_block_header.js +45 -0
  12. package/chain/blocks/get_block_payments.js +100 -0
  13. package/chain/blocks/get_blocks_for_date.js +110 -0
  14. package/chain/blocks/get_median_time.js +92 -0
  15. package/chain/blocks/index.js +19 -0
  16. package/chain/blocks/payment_outputs_from_block.js +178 -0
  17. package/chain/blocks/sample_heights.js +33 -0
  18. package/chain/blocks/window_heights.js +34 -0
  19. package/chain/chain_rates.js +129 -0
  20. package/chain/constants.json +26 -0
  21. package/chain/estimates/check_known_estimates.js +119 -0
  22. package/chain/estimates/current_estimates.js +66 -0
  23. package/chain/estimates/estimate_and_remember.js +199 -0
  24. package/chain/estimates/estimate_day_price.js +361 -0
  25. package/chain/estimates/estimate_record.js +107 -0
  26. package/chain/estimates/estimate_windows.js +246 -0
  27. package/chain/estimates/find_starting_day.js +86 -0
  28. package/chain/estimates/forget_estimates.js +113 -0
  29. package/chain/estimates/get_chain_price.js +167 -0
  30. package/chain/estimates/get_price_for_date.js +607 -0
  31. package/chain/estimates/get_price_from_blocks.js +281 -0
  32. package/chain/estimates/get_provisional_price.js +163 -0
  33. package/chain/estimates/index.js +25 -0
  34. package/chain/estimates/is_alias_ratio.js +38 -0
  35. package/chain/estimates/is_supported_estimate.js +81 -0
  36. package/chain/estimates/nearest_estimate.js +49 -0
  37. package/chain/estimates/price_window.js +25 -0
  38. package/chain/estimates/remember_estimate.js +83 -0
  39. package/chain/estimates/store_estimate.js +198 -0
  40. package/chain/histogram/best_position.js +63 -0
  41. package/chain/histogram/histogram_excess.js +50 -0
  42. package/chain/histogram/histogram_peak.js +87 -0
  43. package/chain/histogram/implied_prices_histogram.js +53 -0
  44. package/chain/histogram/index.js +33 -0
  45. package/chain/histogram/is_round_tokens.js +26 -0
  46. package/chain/histogram/kernel_width.js +25 -0
  47. package/chain/histogram/lower_bound.js +28 -0
  48. package/chain/histogram/noise_of_neighbors.js +28 -0
  49. package/chain/histogram/noise_strength.js +35 -0
  50. package/chain/histogram/noise_strengths.js +29 -0
  51. package/chain/histogram/outputs_histogram.js +80 -0
  52. package/chain/histogram/parabola_peak.js +28 -0
  53. package/chain/histogram/payments_near_position.js +31 -0
  54. package/chain/histogram/peak_width.js +37 -0
  55. package/chain/histogram/position_score.js +38 -0
  56. package/chain/histogram/score_position.js +94 -0
  57. package/chain/histogram/smooth_bins.js +29 -0
  58. package/chain/histogram/sorted_neighbors.js +89 -0
  59. package/chain/histogram/spike_strengths.js +33 -0
  60. package/chain/histogram/template_agreement.js +27 -0
  61. package/chain/index.js +3 -0
  62. package/chain/known/earliest_supported_day.js +12 -0
  63. package/chain/known/index.js +11 -0
  64. package/chain/known/known_prices.js +43 -0
  65. package/chain/known/known_prices.json +1836 -0
  66. package/chain/known/method_version.js +26 -0
  67. package/chain/known/parse_known_prices.js +67 -0
  68. package/chain/outputs/alias_shifts.js +31 -0
  69. package/chain/outputs/cap_near_round_values.js +83 -0
  70. package/chain/outputs/cap_repeated_values.js +33 -0
  71. package/chain/outputs/dollar_bin_for_price.js +19 -0
  72. package/chain/outputs/estimate_from_outputs.js +321 -0
  73. package/chain/outputs/index.js +11 -0
  74. package/chain/outputs/peak_around_price.js +86 -0
  75. package/chain/outputs/price_for_dollar_bin.js +21 -0
  76. package/chain/outputs/price_from_outputs.js +98 -0
  77. package/chain/outputs/refine_price.js +73 -0
  78. package/chain/outputs/round_amounts.js +49 -0
  79. package/chain/outputs/template.json +22 -0
  80. package/coinbase/constants.json +5 -0
  81. package/coinbase/get_coinbase_day_candles.js +155 -0
  82. package/coinbase/get_coinbase_historic_price.js +135 -0
  83. package/coinbase/index.js +7 -1
  84. package/dates/constants.json +3 -0
  85. package/dates/day_of_date.js +26 -0
  86. package/dates/index.js +5 -0
  87. package/dates/is_calendar_day.js +16 -0
  88. package/dates/time_of_date.js +61 -0
  89. package/docs/METHODOLOGY.md +984 -0
  90. package/index.js +2 -1
  91. package/package.json +29 -8
  92. package/rates/get_prices.js +51 -8
  93. package/storage/constants.json +4 -0
  94. package/storage/get_prices_file.js +169 -0
  95. package/storage/index.js +4 -0
  96. package/storage/is_prices.js +31 -0
  97. package/storage/put_prices_file.js +252 -0
  98. package/storage/temp_path.js +22 -0
  99. package/.gitattributes +0 -1
  100. package/.tm_properties +0 -1
  101. package/test/coinbase/test_get_coinbase_current_price.js +0 -77
  102. package/test/coindesk/test_get_coindesk_current_price.js +0 -95
  103. package/test/coingecko/test_get_coingecko_rates.js +0 -93
  104. package/test/rates/test_get_current_price.js +0 -74
  105. package/test/rates/test_get_prices.js +0 -78
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
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
+
3
25
  ## 2.0.0
4
26
 
5
27
  - Require node.js 16 or higher
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.
@@ -0,0 +1,3 @@
1
+ const sortBy = require('./sort_by');
2
+
3
+ module.exports = {sortBy};
@@ -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
+ };