@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/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ const {chainRates} = require('./chain');
1
2
  const {getPrices} = require('./rates');
2
3
 
3
- module.exports = {getPrices};
4
+ module.exports = {chainRates, getPrices};
package/package.json CHANGED
@@ -1,32 +1,53 @@
1
1
  {
2
+ "allowScripts": {
3
+ "protobufjs@7.6.6": true
4
+ },
2
5
  "author": {
3
6
  "name": "Alex Bosworth",
4
- "url": "https://twitter.com/alexbosworth"
7
+ "url": "https://x.com/alexbosworth"
5
8
  },
6
9
  "bugs": {
7
10
  "url": "https://github.com/alexbosworth/fiat/issues"
8
11
  },
9
12
  "dependencies": {
10
- "async": "3.2.4",
11
- "asyncjs-util": "1.2.12"
13
+ "@alexbosworth/blockchain": "4.12.0",
14
+ "async": "3.2.6",
15
+ "asyncjs-util": "1.2.12",
16
+ "ln-service": "60.1.7"
12
17
  },
13
18
  "description": "Fiat rates library",
14
19
  "engines": {
15
- "node": ">=16"
20
+ "node": ">=22"
16
21
  },
22
+ "files": [
23
+ "CHANGELOG.md",
24
+ "arrays",
25
+ "chain",
26
+ "coinbase",
27
+ "coindesk",
28
+ "coingecko",
29
+ "dates",
30
+ "docs",
31
+ "index.js",
32
+ "rates",
33
+ "storage"
34
+ ],
17
35
  "keywords": [
18
36
  "currency",
19
37
  "rates"
20
38
  ],
21
39
  "license": "MIT",
22
- "main": "index.js",
23
40
  "name": "@alexbosworth/fiat",
24
41
  "repository": {
25
42
  "type": "git",
26
- "url": "https://github.com/alexbosworth/fiat.git"
43
+ "url": "git+https://github.com/alexbosworth/fiat.git"
27
44
  },
28
45
  "scripts": {
29
- "test": "npx nyc@15.1 node --experimental-test-coverage --test"
46
+ "check-prices": "node scripts/check_prices.js",
47
+ "prepublishOnly": "node scripts/check_known_prices.js",
48
+ "test": "node --experimental-test-coverage --test test/arrays/*.js test/chain/*.js test/coinbase/*.js test/coindesk/*.js test/coingecko/*.js test/dates/*.js test/rates/*.js test/storage/*.js",
49
+ "test-all": "node --experimental-test-coverage --test test/arrays/*.js test/chain/*.js test/coinbase/*.js test/coindesk/*.js test/coingecko/*.js test/dates/*.js test/rates/*.js test/slow/*.js test/storage/*.js",
50
+ "update-known-prices": "node scripts/update_known_prices.js"
30
51
  },
31
- "version": "2.0.0"
52
+ "version": "3.0.0"
32
53
  }
@@ -2,17 +2,21 @@ const asyncAuto = require('async/auto');
2
2
  const asyncMap = require('async/map');
3
3
  const {returnResult} = require('asyncjs-util');
4
4
 
5
- const getCurrentPrice = require('./get_current_price');
5
+ const {getCoinbaseHistoricPrice} = require('./../coinbase');
6
6
  const {getCoingeckoRates} = require('./../coingecko');
7
+ const getCurrentPrice = require('./get_current_price');
8
+ const {timeOfDate} = require('./../dates');
7
9
 
8
10
  const currency = 'BTC';
9
11
  const defaultFiat = 'USD';
12
+ const historicProvider = 'coinbase';
10
13
  const {isArray} = Array;
11
14
  const uniq = arr => Array.from(new Set(arr));
12
15
 
13
16
  /** Get exchange rates from a rate provider
14
17
 
15
18
  {
19
+ [date]: <Rates At the Hour of ISO 8601 Date String>
16
20
  from: <From Rate Provider String>
17
21
  request: <Request Function> {url}, (err, {statusCode}, body) => {}
18
22
  symbols: [<Fiat Symbol String>] // empty defaults to USD
@@ -21,13 +25,13 @@ const uniq = arr => Array.from(new Set(arr));
21
25
  @returns via cbk or Promise
22
26
  {
23
27
  tickers: [{
24
- date: <Rate Updated At ISO 8601 Date String>
28
+ date: <Rate Updated At or Hour Priced At ISO 8601 Date String>
25
29
  rate: <Exchange Rate in Cents Number>
26
30
  ticker: <Ticker Symbol String>
27
31
  }]
28
32
  }
29
33
  */
30
- module.exports = ({from, request, symbols}, cbk) => {
34
+ module.exports = ({date, from, request, symbols}, cbk) => {
31
35
  return new Promise((resolve, reject) => {
32
36
  return asyncAuto({
33
37
  // Check arguments
@@ -44,6 +48,15 @@ module.exports = ({from, request, symbols}, cbk) => {
44
48
  return cbk([400, 'ExpectedSymbolsToGetFiatExchangeRates']);
45
49
  }
46
50
 
51
+ if (date !== undefined && timeOfDate({date}).time === undefined) {
52
+ return cbk([400, 'ExpectedValidDateToGetExchangeRatesAtDate']);
53
+ }
54
+
55
+ // Coinbase has hourly prices back to 2015, for USD, EUR and GBP
56
+ if (date !== undefined && from !== historicProvider) {
57
+ return cbk([400, 'ExpectedCoinbaseRateProviderForRatesAtDate']);
58
+ }
59
+
47
60
  return cbk();
48
61
  },
49
62
 
@@ -60,10 +73,34 @@ module.exports = ({from, request, symbols}, cbk) => {
60
73
  return cbk(null, uniq(prices));
61
74
  }],
62
75
 
76
+ // Get the prices at the hour of the date
77
+ getHistoricPrices: ['priceSymbols', ({priceSymbols}, cbk) => {
78
+ // Exit early when the current prices are wanted
79
+ if (date === undefined) {
80
+ return cbk();
81
+ }
82
+
83
+ return asyncMap(priceSymbols, (fiat, cbk) => {
84
+ return getCoinbaseHistoricPrice({
85
+ date,
86
+ fiat,
87
+ request,
88
+ },
89
+ (err, res) => {
90
+ if (!!err) {
91
+ return cbk(err);
92
+ }
93
+
94
+ return cbk(null, {date: res.date, rate: res.cents, ticker: fiat});
95
+ });
96
+ },
97
+ cbk);
98
+ }],
99
+
63
100
  // Batch fetch prices
64
101
  getBatchPrices: ['priceSymbols', ({priceSymbols}, cbk) => {
65
- // Exit early when batch rate lookups are supported
66
- if (from !== 'coingecko') {
102
+ // Exit early when batch rate lookups are not supported
103
+ if (date !== undefined || from !== 'coingecko') {
67
104
  return cbk();
68
105
  }
69
106
 
@@ -72,8 +109,8 @@ module.exports = ({from, request, symbols}, cbk) => {
72
109
 
73
110
  // Fetch all the prices
74
111
  getPrices: ['priceSymbols', ({priceSymbols}, cbk) => {
75
- // Exit early when batch rate lookups are supported
76
- if (from === 'coingecko') {
112
+ // Exit early when the prices are at a date or are fetched in a batch
113
+ if (date !== undefined || from === 'coingecko') {
77
114
  return cbk();
78
115
  }
79
116
 
@@ -98,9 +135,15 @@ module.exports = ({from, request, symbols}, cbk) => {
98
135
  // Final set of prices
99
136
  prices: [
100
137
  'getBatchPrices',
138
+ 'getHistoricPrices',
101
139
  'getPrices',
102
- ({getBatchPrices, getPrices}, cbk) =>
140
+ ({getBatchPrices, getHistoricPrices, getPrices}, cbk) =>
103
141
  {
142
+ // Exit early when the prices are at a date
143
+ if (!!getHistoricPrices) {
144
+ return cbk(null, {tickers: getHistoricPrices});
145
+ }
146
+
104
147
  // Exit early when price fetching was batched
105
148
  if (!!getBatchPrices) {
106
149
  return cbk(null, getBatchPrices);
@@ -0,0 +1,4 @@
1
+ {
2
+ "fileVersion": 1,
3
+ "fsMethods": ["getFile", "makeDirectory", "renameFile", "writeFile"]
4
+ }
@@ -0,0 +1,169 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const {fileVersion} = require('./constants');
5
+ const {isCalendarDay} = require('./../dates');
6
+
7
+ const isObject = n => !!n && typeof n === 'object' && !Array.isArray(n);
8
+ const {keys} = Object;
9
+ const missingFileCode = 'ENOENT';
10
+ const {parse} = JSON;
11
+
12
+ /** Get the prices stored in a prices file
13
+
14
+ {
15
+ fs: {
16
+ getFile: <Read File Contents Function> (path, cbk) => {}
17
+ }
18
+ path: <Prices File Path String>
19
+ }
20
+
21
+ @returns via cbk or Promise
22
+ {
23
+ priors: {
24
+ <Day YYYY-MM-DD String>: {
25
+ [agreement]: <Spike Strengths Agreement With Prior Weights Number>
26
+ [blocks]: [<First Block Height Number>, <Last Block Height Number>]
27
+ cents: <USD Cents Per BTC Number>
28
+ [confidence]: <Best Position Score Over Best Alias Score Number>
29
+ [hash]: <Last Block Hash Hex String>
30
+ [outputs]: <Payment Outputs Considered Count Number>
31
+ [payments]: <Estimated Round USD Payments Count Number>
32
+ [rivalry]: <Best Rival Position Score Over Best Score Number>
33
+ [round_shift]: <Price Change If Round BTC Values Were Counted Number>
34
+ [dispersion]: <Peak Width Beyond Smoothing As Price Fraction Number>
35
+ [stability]: <Largest Price Change From Perturbed Refinement Number>
36
+ [version]: <Method Version String>
37
+ }
38
+ }
39
+ provisional: {
40
+ <Day YYYY-MM-DD String>: {
41
+ blocks: [<First Block Height Number>, <Last Block Height Number>]
42
+ cents: <USD Cents Per BTC Number>
43
+ hash: <Last Block Hash Hex String>
44
+ version: <Method Version String>
45
+ }
46
+ }
47
+ rates: {
48
+ <Day YYYY-MM-DD String>: {
49
+ [agreement]: <Spike Strengths Agreement With Prior Weights Number>
50
+ [blocks]: [<First Block Height Number>, <Last Block Height Number>]
51
+ cents: <USD Cents Per BTC Number>
52
+ [confidence]: <Best Position Score Over Best Alias Score Number>
53
+ [hash]: <Last Block Hash Hex String>
54
+ [outputs]: <Payment Outputs Considered Count Number>
55
+ [payments]: <Estimated Round USD Payments Count Number>
56
+ [rivalry]: <Best Rival Position Score Over Best Score Number>
57
+ [round_shift]: <Price Change If Round BTC Values Were Counted Number>
58
+ [dispersion]: <Peak Width Beyond Smoothing As Price Fraction Number>
59
+ [stability]: <Largest Price Change From Perturbed Refinement Number>
60
+ [version]: <Method Version String>
61
+ }
62
+ }
63
+ }
64
+ */
65
+ module.exports = ({fs, path}, cbk) => {
66
+ return new Promise((resolve, reject) => {
67
+ return asyncAuto({
68
+ // Check arguments
69
+ validate: cbk => {
70
+ if (!fs || typeof fs.getFile !== 'function') {
71
+ return cbk([400, 'ExpectedFileSystemMethodsToGetPricesFile']);
72
+ }
73
+
74
+ if (!path) {
75
+ return cbk([400, 'ExpectedPathToGetPricesFile']);
76
+ }
77
+
78
+ return cbk();
79
+ },
80
+
81
+ // Read the file
82
+ getFile: ['validate', ({}, cbk) => {
83
+ return fs.getFile(path, (err, res) => {
84
+ // Exit early when there is no file yet
85
+ if (!!err && err.code === missingFileCode) {
86
+ return cbk();
87
+ }
88
+
89
+ // An unreadable file is an error so that it is never written over
90
+ if (!!err) {
91
+ return cbk([503, 'FailedToReadPricesFile', {err}]);
92
+ }
93
+
94
+ // Exit early when there is nothing in the file
95
+ if (!res || !res.length) {
96
+ return cbk();
97
+ }
98
+
99
+ return cbk(null, res);
100
+ });
101
+ }],
102
+
103
+ // Parse the file contents
104
+ parseFile: ['getFile', ({getFile}, cbk) => {
105
+ // Exit early when there is no file yet
106
+ if (!getFile) {
107
+ return cbk(null, {});
108
+ }
109
+
110
+ const parsed = (() => {
111
+ try {
112
+ return {contents: parse(getFile.toString())};
113
+ } catch (err) {
114
+ return {err};
115
+ }
116
+ })();
117
+
118
+ // Exit early when the file is not JSON
119
+ if (!!parsed.err) {
120
+ return cbk([503, 'FailedToParsePricesFile', {err: parsed.err}]);
121
+ }
122
+
123
+ return cbk(null, parsed.contents);
124
+ }],
125
+
126
+ // Check the contents and pull out the prices
127
+ prices: ['getFile', 'parseFile', ({getFile, parseFile}, cbk) => {
128
+ // Exit early when there is no file yet
129
+ if (!getFile) {
130
+ return cbk(null, {priors: {}, provisional: {}, rates: {}});
131
+ }
132
+
133
+ if (!isObject(parseFile)) {
134
+ return cbk([503, 'ExpectedPricesObjectInPricesFile']);
135
+ }
136
+
137
+ const {version} = parseFile;
138
+
139
+ // The file format has a version of its own, apart from the method's
140
+ if (version !== undefined && version !== fileVersion) {
141
+ return cbk([503, 'UnsupportedPricesFileVersion']);
142
+ }
143
+
144
+ // Only the entries that are prices for days of the calendar are read
145
+ const read = section => {
146
+ const entries = isObject(section) ? section : {};
147
+
148
+ return keys(entries).reduce((sum, day) => {
149
+ const isDay = isCalendarDay({day}).is_calendar_day;
150
+
151
+ if (isDay && isObject(entries[day])) {
152
+ sum[day] = entries[day];
153
+ }
154
+
155
+ return sum;
156
+ },
157
+ {});
158
+ };
159
+
160
+ return cbk(null, {
161
+ priors: read(parseFile.priors),
162
+ provisional: read(parseFile.provisional),
163
+ rates: read(parseFile.rates),
164
+ });
165
+ }],
166
+ },
167
+ returnResult({reject, resolve, of: 'prices'}, cbk));
168
+ });
169
+ };
@@ -0,0 +1,4 @@
1
+ const getPricesFile = require('./get_prices_file');
2
+ const putPricesFile = require('./put_prices_file');
3
+
4
+ module.exports = {getPricesFile, putPricesFile};
@@ -0,0 +1,31 @@
1
+ const {isCalendarDay} = require('./../dates');
2
+
3
+ const isDay = day => isCalendarDay({day}).is_calendar_day;
4
+ const isObject = n => !!n && typeof n === 'object' && !Array.isArray(n);
5
+ const {keys} = Object;
6
+ const {values} = Object;
7
+
8
+ /** Determine whether a value is a set of prices by day
9
+
10
+ {
11
+ prices: <Prices By Day Object>
12
+ }
13
+
14
+ @returns
15
+ {
16
+ is_prices: <Value Is a Set of Prices By Day Bool>
17
+ }
18
+ */
19
+ module.exports = ({prices}) => {
20
+ // Exit early when the prices are not an object
21
+ if (!isObject(prices)) {
22
+ return {is_prices: false};
23
+ }
24
+
25
+ // Exit early when a price is not for a day of the calendar
26
+ if (!keys(prices).every(isDay)) {
27
+ return {is_prices: false};
28
+ }
29
+
30
+ return {is_prices: values(prices).every(isObject)};
31
+ };
@@ -0,0 +1,252 @@
1
+ const {dirname} = require('path');
2
+
3
+ const asyncAuto = require('async/auto');
4
+ const {returnResult} = require('asyncjs-util');
5
+
6
+ const {fileVersion} = require('./constants');
7
+ const {fsMethods} = require('./constants');
8
+ const getPricesFile = require('./get_prices_file');
9
+ const isPrices = require('./is_prices');
10
+ const tempPath = require('./temp_path');
11
+
12
+ const asJson = obj => JSON.stringify(obj, null, 2);
13
+ const {assign} = Object;
14
+ const {keys} = Object;
15
+ const lastBlock = n => Array.isArray(n.blocks) ? n.blocks[1] : -Infinity;
16
+ const provisionalDays = 2;
17
+ const replaced = ['priors', 'provisional'];
18
+ const types = ['priors', 'provisional', 'rates'];
19
+
20
+ /** Store prices in a prices file
21
+
22
+ {
23
+ fs: {
24
+ getFile: <Read File Contents Function> (path, cbk) => {}
25
+ makeDirectory: <Make Directory Function> (path, cbk) => {}
26
+ renameFile: <Rename File Function> (from, to, cbk) => {}
27
+ writeFile: <Write File Contents Function> (path, contents, cbk) => {}
28
+ }
29
+ path: <Prices File Path String>
30
+ priors: {
31
+ <Day YYYY-MM-DD String>: {
32
+ [agreement]: <Spike Strengths Agreement With Prior Weights Number>
33
+ [blocks]: [<First Block Height Number>, <Last Block Height Number>]
34
+ cents: <USD Cents Per BTC Number>
35
+ [confidence]: <Best Position Score Over Best Alias Score Number>
36
+ [hash]: <Last Block Hash Hex String>
37
+ [outputs]: <Payment Outputs Considered Count Number>
38
+ [payments]: <Estimated Round USD Payments Count Number>
39
+ [rivalry]: <Best Rival Position Score Over Best Score Number>
40
+ [round_shift]: <Price Change If Round BTC Values Were Counted Number>
41
+ [dispersion]: <Peak Width Beyond Smoothing As Price Fraction Number>
42
+ [stability]: <Largest Price Change From Perturbed Refinement Number>
43
+ [version]: <Method Version String>
44
+ }
45
+ }
46
+ [provisional]: {
47
+ <Day YYYY-MM-DD String>: {
48
+ blocks: [<First Block Height Number>, <Last Block Height Number>]
49
+ cents: <USD Cents Per BTC Number>
50
+ hash: <Last Block Hash Hex String>
51
+ version: <Method Version String>
52
+ }
53
+ }
54
+ rates: {
55
+ <Day YYYY-MM-DD String>: {
56
+ [agreement]: <Spike Strengths Agreement With Prior Weights Number>
57
+ [blocks]: [<First Block Height Number>, <Last Block Height Number>]
58
+ cents: <USD Cents Per BTC Number>
59
+ [confidence]: <Best Position Score Over Best Alias Score Number>
60
+ [hash]: <Last Block Hash Hex String>
61
+ [outputs]: <Payment Outputs Considered Count Number>
62
+ [payments]: <Estimated Round USD Payments Count Number>
63
+ [rivalry]: <Best Rival Position Score Over Best Score Number>
64
+ [round_shift]: <Price Change If Round BTC Values Were Counted Number>
65
+ [dispersion]: <Peak Width Beyond Smoothing As Price Fraction Number>
66
+ [stability]: <Largest Price Change From Perturbed Refinement Number>
67
+ [version]: <Method Version String>
68
+ }
69
+ }
70
+ [removed]: [{
71
+ day: <Day YYYY-MM-DD String>
72
+ hash: <Last Block Hash Hex String>
73
+ }]
74
+ }
75
+
76
+ @returns via cbk or Promise
77
+ {
78
+ priors: {
79
+ <Day YYYY-MM-DD String>: {
80
+ [agreement]: <Spike Strengths Agreement With Prior Weights Number>
81
+ [blocks]: [<First Block Height Number>, <Last Block Height Number>]
82
+ cents: <USD Cents Per BTC Number>
83
+ [confidence]: <Best Position Score Over Best Alias Score Number>
84
+ [hash]: <Last Block Hash Hex String>
85
+ [outputs]: <Payment Outputs Considered Count Number>
86
+ [payments]: <Estimated Round USD Payments Count Number>
87
+ [rivalry]: <Best Rival Position Score Over Best Score Number>
88
+ [round_shift]: <Price Change If Round BTC Values Were Counted Number>
89
+ [dispersion]: <Peak Width Beyond Smoothing As Price Fraction Number>
90
+ [stability]: <Largest Price Change From Perturbed Refinement Number>
91
+ [version]: <Method Version String>
92
+ }
93
+ }
94
+ provisional: {
95
+ <Day YYYY-MM-DD String>: {
96
+ blocks: [<First Block Height Number>, <Last Block Height Number>]
97
+ cents: <USD Cents Per BTC Number>
98
+ hash: <Last Block Hash Hex String>
99
+ version: <Method Version String>
100
+ }
101
+ }
102
+ rates: {
103
+ <Day YYYY-MM-DD String>: {
104
+ [agreement]: <Spike Strengths Agreement With Prior Weights Number>
105
+ [blocks]: [<First Block Height Number>, <Last Block Height Number>]
106
+ cents: <USD Cents Per BTC Number>
107
+ [confidence]: <Best Position Score Over Best Alias Score Number>
108
+ [hash]: <Last Block Hash Hex String>
109
+ [outputs]: <Payment Outputs Considered Count Number>
110
+ [payments]: <Estimated Round USD Payments Count Number>
111
+ [rivalry]: <Best Rival Position Score Over Best Score Number>
112
+ [round_shift]: <Price Change If Round BTC Values Were Counted Number>
113
+ [dispersion]: <Peak Width Beyond Smoothing As Price Fraction Number>
114
+ [stability]: <Largest Price Change From Perturbed Refinement Number>
115
+ [version]: <Method Version String>
116
+ }
117
+ }
118
+ }
119
+ */
120
+ module.exports = (args, cbk) => {
121
+ const {fs, path, priors, provisional, rates, removed} = args;
122
+
123
+ return new Promise((resolve, reject) => {
124
+ return asyncAuto({
125
+ // Check arguments
126
+ validate: cbk => {
127
+ if (!fs || !fsMethods.every(n => typeof fs[n] === 'function')) {
128
+ return cbk([400, 'ExpectedFileSystemMethodsToPutPricesFile']);
129
+ }
130
+
131
+ if (!path) {
132
+ return cbk([400, 'ExpectedPathToPutPricesFile']);
133
+ }
134
+
135
+ // Prices are keyed by days of the calendar as YYYY-MM-DD
136
+ if (!isPrices({prices: priors}).is_prices) {
137
+ return cbk([400, 'ExpectedPriorsToPutPricesFile']);
138
+ }
139
+
140
+ const isProvisional = isPrices({prices: provisional}).is_prices;
141
+
142
+ if (provisional !== undefined && !isProvisional) {
143
+ return cbk([400, 'ExpectedProvisionalPricesToPutPricesFile']);
144
+ }
145
+
146
+ if (!isPrices({prices: rates}).is_prices) {
147
+ return cbk([400, 'ExpectedRatesToPutPricesFile']);
148
+ }
149
+
150
+ if (removed !== undefined && !Array.isArray(removed)) {
151
+ return cbk([400, 'ExpectedRemovedPricesToPutPricesFile']);
152
+ }
153
+
154
+ return cbk();
155
+ },
156
+
157
+ // Make sure the directory exists, it may already be there
158
+ makeDirectory: ['validate', ({}, cbk) => {
159
+ return fs.makeDirectory(dirname(path), () => cbk());
160
+ }],
161
+
162
+ // Get the prices in the file, a file that cannot be read is not written
163
+ getFile: ['makeDirectory', ({}, cbk) => getPricesFile({fs, path}, cbk)],
164
+
165
+ // Merge with the prices that are already in the file
166
+ merge: ['getFile', ({getFile}, cbk) => {
167
+ const merged = {
168
+ priors: assign({}, priors),
169
+ provisional: assign({}, provisional),
170
+ rates: assign({}, rates),
171
+ version: fileVersion,
172
+ };
173
+
174
+ keys(getFile.rates).forEach(day => {
175
+ merged.rates[day] = merged.rates[day] || getFile.rates[day];
176
+ });
177
+
178
+ keys(getFile.priors).forEach(day => {
179
+ merged.priors[day] = merged.priors[day] || getFile.priors[day];
180
+ });
181
+
182
+ // The provisional price derived from the later blocks is kept
183
+ keys(getFile.provisional).forEach(day => {
184
+ const mine = merged.provisional[day];
185
+ const theirs = getFile.provisional[day];
186
+
187
+ if (!mine || lastBlock(theirs) > lastBlock(mine)) {
188
+ merged.provisional[day] = theirs;
189
+ }
190
+ });
191
+
192
+ // A full day rate replaces the other prices of its version
193
+ keys(merged.rates).forEach(day => {
194
+ const {version} = merged.rates[day];
195
+
196
+ const isSameVersion = type => {
197
+ const other = merged[type][day];
198
+
199
+ return !!other && other.version === version;
200
+ };
201
+
202
+ replaced.filter(isSameVersion).forEach(type => {
203
+ delete merged[type][day];
204
+ });
205
+ });
206
+
207
+ const days = keys(merged.provisional).sort();
208
+
209
+ // Only the provisional prices of the latest days are kept
210
+ days.slice(Number(), -provisionalDays).forEach(day => {
211
+ delete merged.provisional[day];
212
+ });
213
+
214
+ // A price from blocks that were replaced is removed for good
215
+ (removed || []).forEach(({day, hash}) => {
216
+ types.filter(type => {
217
+ return !!merged[type][day] && merged[type][day].hash === hash;
218
+ })
219
+ .forEach(type => delete merged[type][day]);
220
+ });
221
+
222
+ return cbk(null, merged);
223
+ }],
224
+
225
+ // Each write has its own temporary file
226
+ temp: ['merge', ({}, cbk) => cbk(null, tempPath({path}).path)],
227
+
228
+ // Write to a temporary file so a partial write is never seen
229
+ write: ['merge', 'temp', ({merge, temp}, cbk) => {
230
+ return fs.writeFile(temp, asJson(merge), err => {
231
+ if (!!err) {
232
+ return cbk([503, 'FailedToWritePricesFile', {err}]);
233
+ }
234
+
235
+ return cbk();
236
+ });
237
+ }],
238
+
239
+ // Atomically swap the temporary file in place of the prices file
240
+ swap: ['merge', 'temp', 'write', ({merge, temp}, cbk) => {
241
+ return fs.renameFile(temp, path, err => {
242
+ if (!!err) {
243
+ return cbk([503, 'FailedToReplacePricesFile', {err}]);
244
+ }
245
+
246
+ return cbk(null, merge);
247
+ });
248
+ }],
249
+ },
250
+ returnResult({reject, resolve, of: 'swap'}, cbk));
251
+ });
252
+ };
@@ -0,0 +1,22 @@
1
+ const {randomBytes} = require('crypto');
2
+
3
+ const bufferAsHex = buffer => buffer.toString('hex');
4
+ const randomIdBytes = 4;
5
+
6
+ /** Get the path of a temporary file of its own next to a file
7
+
8
+ {
9
+ path: <File Path String>
10
+ }
11
+
12
+ @returns
13
+ {
14
+ path: <Temporary File Path String>
15
+ }
16
+ */
17
+ module.exports = ({path}) => {
18
+ // The pid and random bytes keep writers from sharing a temporary file
19
+ const id = bufferAsHex(randomBytes(randomIdBytes));
20
+
21
+ return {path: `${path}.${process.pid}.${id}`};
22
+ };
package/.gitattributes DELETED
@@ -1 +0,0 @@
1
- package-lock.json binary
package/.tm_properties DELETED
@@ -1 +0,0 @@
1
- exclude = '{$exclude,node_modules,*~}'