@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.
- package/CHANGELOG.md +22 -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 -8
- 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 -77
- package/test/coindesk/test_get_coindesk_current_price.js +0 -95
- package/test/coingecko/test_get_coingecko_rates.js +0 -93
- package/test/rates/test_get_current_price.js +0 -74
- package/test/rates/test_get_prices.js +0 -78
package/index.js
CHANGED
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://
|
|
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
|
-
"
|
|
11
|
-
"
|
|
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": ">=
|
|
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
|
-
"
|
|
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": "
|
|
52
|
+
"version": "3.0.0"
|
|
32
53
|
}
|
package/rates/get_prices.js
CHANGED
|
@@ -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
|
|
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
|
|
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,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
|
+
};
|
package/storage/index.js
ADDED
|
@@ -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,*~}'
|