@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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {returnResult} = require('asyncjs-util');
|
|
3
|
+
|
|
4
|
+
const {confirmationsAfterDay} = require('./../constants');
|
|
5
|
+
const findHeightForTime = require('./find_height_for_time');
|
|
6
|
+
const {isCalendarDay} = require('./../../dates');
|
|
7
|
+
const {msPerSecond} = require('./../constants');
|
|
8
|
+
|
|
9
|
+
const dayFormat = /^\d{4}-\d{2}-\d{2}$/;
|
|
10
|
+
const isHeight = n => Number.isInteger(n) && n >= 0;
|
|
11
|
+
const range = (a, b) => [...Array(b - a).keys()].map(i => a + i);
|
|
12
|
+
const secondsPerDay = 60 * 60 * 24;
|
|
13
|
+
|
|
14
|
+
/** Get the heights of the blocks that belong to a UTC calendar day
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
date: <Day YYYY-MM-DD String>
|
|
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
|
+
heights: [<Block Height Number>]
|
|
26
|
+
is_complete: <Day Is Complete Bool>
|
|
27
|
+
}
|
|
28
|
+
*/
|
|
29
|
+
module.exports = (args, cbk) => {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
return asyncAuto({
|
|
32
|
+
// Check arguments
|
|
33
|
+
validate: cbk => {
|
|
34
|
+
if (!args.date || !dayFormat.test(args.date)) {
|
|
35
|
+
return cbk([400, 'ExpectedYyyyMmDdDateToGetBlocksForDate']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!isCalendarDay({day: args.date}).is_calendar_day) {
|
|
39
|
+
return cbk([400, 'ExpectedValidDateToGetBlocksForDate']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!isHeight(args.height)) {
|
|
43
|
+
return cbk([400, 'ExpectedCurrentChainHeightToGetBlocksForDate']);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!args.lnd) {
|
|
47
|
+
return cbk([400, 'ExpectedAuthenticatedLndToGetBlocksForDate']);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return cbk();
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Determine the start and end times of the day
|
|
54
|
+
times: ['validate', ({}, cbk) => {
|
|
55
|
+
const start = Date.parse(`${args.date}T00:00:00.000Z`) / msPerSecond;
|
|
56
|
+
|
|
57
|
+
return cbk(null, {start, end: start + secondsPerDay});
|
|
58
|
+
}],
|
|
59
|
+
|
|
60
|
+
// Block times are kept to avoid retrieving them twice
|
|
61
|
+
known: ['validate', ({}, cbk) => cbk(null, args.times || new Map())],
|
|
62
|
+
|
|
63
|
+
// Find the first block on the day
|
|
64
|
+
getFirst: ['known', 'times', ({known, times}, cbk) => {
|
|
65
|
+
return findHeightForTime({
|
|
66
|
+
height: args.height,
|
|
67
|
+
lnd: args.lnd,
|
|
68
|
+
low: Number(),
|
|
69
|
+
time: times.start,
|
|
70
|
+
times: known,
|
|
71
|
+
},
|
|
72
|
+
cbk);
|
|
73
|
+
}],
|
|
74
|
+
|
|
75
|
+
// Find the block after the last block on the day
|
|
76
|
+
getNext: [
|
|
77
|
+
'getFirst',
|
|
78
|
+
'known',
|
|
79
|
+
'times',
|
|
80
|
+
({getFirst, known, times}, cbk) =>
|
|
81
|
+
{
|
|
82
|
+
return findHeightForTime({
|
|
83
|
+
height: args.height,
|
|
84
|
+
lnd: args.lnd,
|
|
85
|
+
low: getFirst.at_or_after,
|
|
86
|
+
time: times.end,
|
|
87
|
+
times: known,
|
|
88
|
+
},
|
|
89
|
+
cbk);
|
|
90
|
+
}],
|
|
91
|
+
|
|
92
|
+
// Determine the blocks on the day
|
|
93
|
+
blocks: ['getFirst', 'getNext', ({getFirst, getNext}, cbk) => {
|
|
94
|
+
const first = getFirst.at_or_after;
|
|
95
|
+
const next = getNext.at_or_after;
|
|
96
|
+
|
|
97
|
+
const heights = range(first, next);
|
|
98
|
+
|
|
99
|
+
// A day is complete when it has blocks and enough blocks after it
|
|
100
|
+
const confirmed = next + confirmationsAfterDay <= args.height + 1;
|
|
101
|
+
|
|
102
|
+
return cbk(null, {
|
|
103
|
+
heights,
|
|
104
|
+
is_complete: !!heights.length && confirmed,
|
|
105
|
+
});
|
|
106
|
+
}],
|
|
107
|
+
},
|
|
108
|
+
returnResult({reject, resolve, of: 'blocks'}, cbk));
|
|
109
|
+
});
|
|
110
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncMapLimit = require('async/mapLimit');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
|
|
5
|
+
const blockHeaderTime = require('./block_header_time');
|
|
6
|
+
const {fetchLimit} = require('./../constants');
|
|
7
|
+
const getBlockHeader = require('./get_block_header');
|
|
8
|
+
|
|
9
|
+
const {floor} = Math;
|
|
10
|
+
const isHeight = n => Number.isInteger(n) && n >= 0;
|
|
11
|
+
const {max} = Math;
|
|
12
|
+
const medianWindowBlocks = 11;
|
|
13
|
+
const range = (a, b) => [...Array(b - a).keys()].map(i => a + i);
|
|
14
|
+
|
|
15
|
+
/** Get the median time past of a block
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
height: <Block 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
|
+
time: <Median Time Past Epoch Seconds Number>
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
module.exports = ({height, lnd, times}, cbk) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
return asyncAuto({
|
|
31
|
+
// Check arguments
|
|
32
|
+
validate: cbk => {
|
|
33
|
+
if (!isHeight(height)) {
|
|
34
|
+
return cbk([400, 'ExpectedBlockHeightToGetMedianTime']);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!lnd) {
|
|
38
|
+
return cbk([400, 'ExpectedAuthenticatedLndToGetMedianTime']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return cbk();
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// Get the times of the block and the blocks before it
|
|
45
|
+
getTimes: ['validate', ({}, cbk) => {
|
|
46
|
+
const known = times || new Map();
|
|
47
|
+
const start = max(Number(), height - medianWindowBlocks + 1);
|
|
48
|
+
|
|
49
|
+
const heights = range(start, height + 1);
|
|
50
|
+
|
|
51
|
+
return asyncMapLimit(heights, fetchLimit, (height, cbk) => {
|
|
52
|
+
// Exit early when the time of the block is already known
|
|
53
|
+
if (known.has(height)) {
|
|
54
|
+
return cbk(null, known.get(height));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return getBlockHeader({height, lnd}, (err, res) => {
|
|
58
|
+
if (!!err) {
|
|
59
|
+
return cbk(err);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const {time} = (() => {
|
|
63
|
+
try {
|
|
64
|
+
return blockHeaderTime({header: res.header});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
|
|
70
|
+
// Exit early when the header has no time to read
|
|
71
|
+
if (time === undefined) {
|
|
72
|
+
return cbk([503, 'ExpectedBlockHeaderToGetMedianTime']);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
known.set(height, time);
|
|
76
|
+
|
|
77
|
+
return cbk(null, time);
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
cbk);
|
|
81
|
+
}],
|
|
82
|
+
|
|
83
|
+
// The median time is the middle of the sorted times
|
|
84
|
+
median: ['getTimes', ({getTimes}, cbk) => {
|
|
85
|
+
const sorted = getTimes.slice().sort((a, b) => a - b);
|
|
86
|
+
|
|
87
|
+
return cbk(null, {time: sorted[floor(sorted.length / 2)]});
|
|
88
|
+
}],
|
|
89
|
+
},
|
|
90
|
+
returnResult({reject, resolve, of: 'median'}, cbk));
|
|
91
|
+
});
|
|
92
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const blockHeaderHash = require('./block_header_hash');
|
|
2
|
+
const findCompleteDay = require('./find_complete_day');
|
|
3
|
+
const getBlockHash = require('./get_block_hash');
|
|
4
|
+
const getBlockPayments = require('./get_block_payments');
|
|
5
|
+
const getBlocksForDate = require('./get_blocks_for_date');
|
|
6
|
+
const getMedianTime = require('./get_median_time');
|
|
7
|
+
const sampleHeights = require('./sample_heights');
|
|
8
|
+
const windowHeights = require('./window_heights');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
blockHeaderHash,
|
|
12
|
+
findCompleteDay,
|
|
13
|
+
getBlockHash,
|
|
14
|
+
getBlockPayments,
|
|
15
|
+
getBlocksForDate,
|
|
16
|
+
getMedianTime,
|
|
17
|
+
sampleHeights,
|
|
18
|
+
windowHeights,
|
|
19
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const {compactIntAsNumber} = require('@alexbosworth/blockchain');
|
|
2
|
+
|
|
3
|
+
const {blockHeaderByteLength} = require('./../constants');
|
|
4
|
+
const {maxTokens} = require('./../constants');
|
|
5
|
+
const {minTokens} = require('./../constants');
|
|
6
|
+
|
|
7
|
+
const byteCountHash = 32;
|
|
8
|
+
const byteCountInt32 = 4;
|
|
9
|
+
const byteCountInt64 = 8;
|
|
10
|
+
const byteCountMarkerFlag = 2;
|
|
11
|
+
const coinbaseInputIndex = 0xffffffff;
|
|
12
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
13
|
+
const isHex = n => !!n && !(n.length % 2) && /^[0-9A-F]*$/i.test(n);
|
|
14
|
+
const maxInputsCount = 5;
|
|
15
|
+
const maxWitnessBytes = 500;
|
|
16
|
+
const opReturn = 0x6a;
|
|
17
|
+
const paymentOutputsCount = 2;
|
|
18
|
+
const witnessFlagValue = 1;
|
|
19
|
+
const witnessMarkerValue = 0;
|
|
20
|
+
|
|
21
|
+
/** Derive payment-like outputs from a raw block for on-chain price analysis
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
block: <Hex Encoded Raw Block String>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@throws
|
|
28
|
+
<Error>
|
|
29
|
+
|
|
30
|
+
@returns
|
|
31
|
+
{
|
|
32
|
+
transactions: [{
|
|
33
|
+
outputs: [<Output Tokens Number>]
|
|
34
|
+
}]
|
|
35
|
+
}
|
|
36
|
+
*/
|
|
37
|
+
module.exports = ({block}) => {
|
|
38
|
+
if (!isHex(block)) {
|
|
39
|
+
throw new Error('ExpectedHexEncodedBlockToDerivePaymentOutputs');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const buffer = hexAsBuffer(block);
|
|
43
|
+
|
|
44
|
+
// Exit early with error when the block header data is cut short
|
|
45
|
+
if (buffer.length <= blockHeaderByteLength) {
|
|
46
|
+
throw new Error('ExpectedBlockHeaderAndTransactionsToDeriveOutputs');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const transactions = [];
|
|
50
|
+
|
|
51
|
+
// A compact int count of transactions follows the block header
|
|
52
|
+
const txCount = compactIntAsNumber({
|
|
53
|
+
encoded: buffer,
|
|
54
|
+
start: blockHeaderByteLength,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const readCount = () => {
|
|
58
|
+
const count = compactIntAsNumber({encoded: buffer, start: cursor});
|
|
59
|
+
|
|
60
|
+
cursor += count.bytes;
|
|
61
|
+
|
|
62
|
+
return count.number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let cursor = blockHeaderByteLength + txCount.bytes;
|
|
66
|
+
let remaining = txCount.number;
|
|
67
|
+
|
|
68
|
+
// Iterate forward through the block, parsing each transaction
|
|
69
|
+
while (remaining--) {
|
|
70
|
+
const outputs = [];
|
|
71
|
+
|
|
72
|
+
let hasOpReturn = false;
|
|
73
|
+
let isCoinbase = false;
|
|
74
|
+
let isOverWitnessLimit = false;
|
|
75
|
+
|
|
76
|
+
cursor += byteCountInt32;
|
|
77
|
+
|
|
78
|
+
// SegWit transactions have a marker and flag following the version
|
|
79
|
+
const isSegWit = buffer[cursor] === witnessMarkerValue &&
|
|
80
|
+
buffer[cursor + 1] === witnessFlagValue;
|
|
81
|
+
|
|
82
|
+
if (isSegWit) {
|
|
83
|
+
cursor += byteCountMarkerFlag;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const inputsCount = readCount();
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < inputsCount; i++) {
|
|
89
|
+
const hash = buffer.subarray(cursor, cursor + byteCountHash);
|
|
90
|
+
|
|
91
|
+
cursor += byteCountHash;
|
|
92
|
+
|
|
93
|
+
const vout = buffer.readUInt32LE(cursor);
|
|
94
|
+
|
|
95
|
+
cursor += byteCountInt32;
|
|
96
|
+
|
|
97
|
+
// A coinbase input spends a null outpoint
|
|
98
|
+
if (vout === coinbaseInputIndex && hash.every(n => !n)) {
|
|
99
|
+
isCoinbase = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const scriptLength = readCount();
|
|
103
|
+
|
|
104
|
+
cursor += scriptLength + byteCountInt32;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const outputsCount = readCount();
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < outputsCount; i++) {
|
|
110
|
+
const tokens = Number(buffer.readBigUInt64LE(cursor));
|
|
111
|
+
|
|
112
|
+
cursor += byteCountInt64;
|
|
113
|
+
|
|
114
|
+
const scriptLength = readCount();
|
|
115
|
+
|
|
116
|
+
if (!!scriptLength && buffer[cursor] === opReturn) {
|
|
117
|
+
hasOpReturn = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
cursor += scriptLength;
|
|
121
|
+
|
|
122
|
+
outputs.push(tokens);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// SegWit places a witness stack for every input after the outputs
|
|
126
|
+
if (isSegWit) {
|
|
127
|
+
for (let i = 0; i < inputsCount; i++) {
|
|
128
|
+
const itemsCount = readCount();
|
|
129
|
+
|
|
130
|
+
let witnessBytes = 0;
|
|
131
|
+
|
|
132
|
+
for (let j = 0; j < itemsCount; j++) {
|
|
133
|
+
const itemLength = readCount();
|
|
134
|
+
|
|
135
|
+
cursor += itemLength;
|
|
136
|
+
|
|
137
|
+
witnessBytes += itemLength;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (witnessBytes > maxWitnessBytes) {
|
|
141
|
+
isOverWitnessLimit = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
cursor += byteCountInt32;
|
|
147
|
+
|
|
148
|
+
// Exit early with error when the transaction runs past the block data
|
|
149
|
+
if (cursor > buffer.length) {
|
|
150
|
+
throw new Error('UnexpectedTransactionLengthInBlockToDeriveOutputs');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Exit early when the transaction does not look like a regular payment
|
|
154
|
+
if (isCoinbase || hasOpReturn || isOverWitnessLimit) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (inputsCount > maxInputsCount || outputsCount !== paymentOutputsCount) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const tokens = outputs.filter(n => n > minTokens && n < maxTokens);
|
|
163
|
+
|
|
164
|
+
// Exit early when no output is in the analyzed range
|
|
165
|
+
if (!tokens.length) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
transactions.push({outputs: tokens});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Exit early with error when there is extra data after the transactions
|
|
173
|
+
if (cursor !== buffer.length) {
|
|
174
|
+
throw new Error('UnexpectedDataAfterBlockTransactionsToDeriveOutputs');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {transactions};
|
|
178
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const {floor} = Math;
|
|
2
|
+
const range = n => [...Array(n).keys()];
|
|
3
|
+
|
|
4
|
+
/** Pick evenly spaced heights to get a quick sample of a range of blocks
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
count: <Sample Blocks Count Number>
|
|
8
|
+
heights: [<Block Height Number>]
|
|
9
|
+
[initial]: <Blocks In the First Sample Count Number>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@returns
|
|
13
|
+
{
|
|
14
|
+
heights: [<Sampled Block Height Number>]
|
|
15
|
+
}
|
|
16
|
+
*/
|
|
17
|
+
module.exports = ({count, heights, initial}) => {
|
|
18
|
+
// Exit early when there are not more blocks than the sample size
|
|
19
|
+
if (heights.length <= count) {
|
|
20
|
+
return {heights};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A doubled sample takes the start of each stretch to keep the first one
|
|
24
|
+
const isDoubled = !!initial && count > initial;
|
|
25
|
+
|
|
26
|
+
// Positions are counted in half stretches so that every sample of a range
|
|
27
|
+
// maps them to the same heights
|
|
28
|
+
const halves = range(count).map(i => isDoubled ? 2 * i : 2 * i + 1);
|
|
29
|
+
|
|
30
|
+
const indices = halves.map(n => floor(n * heights.length / (2 * count)));
|
|
31
|
+
|
|
32
|
+
return {heights: indices.map(index => heights[index])};
|
|
33
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const {half} = require('./../constants');
|
|
2
|
+
|
|
3
|
+
const {ceil} = Math;
|
|
4
|
+
|
|
5
|
+
/** Split block heights into consecutive windows of blocks
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
count: <Blocks Per Window Count Number>
|
|
9
|
+
heights: [<Block Height Number>]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@returns
|
|
13
|
+
{
|
|
14
|
+
windows: [[<Block Height Number>]]
|
|
15
|
+
}
|
|
16
|
+
*/
|
|
17
|
+
module.exports = ({count, heights}) => {
|
|
18
|
+
const starts = [...Array(ceil(heights.length / count)).keys()];
|
|
19
|
+
|
|
20
|
+
const windows = starts.map(i => heights.slice(i * count, (i + 1) * count));
|
|
21
|
+
|
|
22
|
+
const [last] = windows.slice(-1);
|
|
23
|
+
|
|
24
|
+
// Exit early when the last window is long enough to stand on its own
|
|
25
|
+
if (windows.length < 2 || last.length >= count * half) {
|
|
26
|
+
return {windows};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const [beforeLast] = windows.slice(-2);
|
|
30
|
+
|
|
31
|
+
const joined = beforeLast.concat(last);
|
|
32
|
+
|
|
33
|
+
return {windows: windows.slice(Number(), -2).concat([joined])};
|
|
34
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const asyncQueue = require('async/queue');
|
|
2
|
+
|
|
3
|
+
const {dayOfDate} = require('./../dates');
|
|
4
|
+
const {earliestSupportedDay} = require('./known');
|
|
5
|
+
const {fsMethods} = require('./../storage/constants');
|
|
6
|
+
const {getChainPrice} = require('./estimates');
|
|
7
|
+
const {knownPrices} = require('./known');
|
|
8
|
+
|
|
9
|
+
const supportedCurrency = 'BTC';
|
|
10
|
+
const supportedFiat = 'USD';
|
|
11
|
+
|
|
12
|
+
/** Make a method that gets historic USD prices of BTC from chain data
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
fs: {
|
|
16
|
+
getFile: <Read File Contents Function> (path, cbk) => {}
|
|
17
|
+
makeDirectory: <Make Directory Function> (path, cbk) => {}
|
|
18
|
+
renameFile: <Rename File Function> (from, to, cbk) => {}
|
|
19
|
+
writeFile: <Write File Contents Function> (path, contents, cbk) => {}
|
|
20
|
+
}
|
|
21
|
+
lnd: <Authenticated LND API Object>
|
|
22
|
+
[logger]: <Winston Logger Object>
|
|
23
|
+
path: <Prices File Path String>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@throws
|
|
27
|
+
<Error>
|
|
28
|
+
|
|
29
|
+
@returns
|
|
30
|
+
{
|
|
31
|
+
getChainRate: <Get Chain Rate Method> ({
|
|
32
|
+
currency: <Currency To Price String> // BTC
|
|
33
|
+
date: <Date To Price ISO 8601 Date String> // priced on its day in UTC
|
|
34
|
+
fiat: <Fiat Currency To Price In String> // USD
|
|
35
|
+
}, cbk) => (err, {
|
|
36
|
+
cents: <US Cents Per BTC on the Day of the Date Number>
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
*/
|
|
40
|
+
module.exports = ({fs, lnd, logger, path}) => {
|
|
41
|
+
if (!fs || !fsMethods.every(method => typeof fs[method] === 'function')) {
|
|
42
|
+
throw new Error('ExpectedFileSystemMethodsToMakeChainRates');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!lnd) {
|
|
46
|
+
throw new Error('ExpectedAuthenticatedLndToMakeChainRates');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!path) {
|
|
50
|
+
throw new Error('ExpectedPricesFilePathToMakeChainRates');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const estimates = {provisional: {}, samples: Number()};
|
|
54
|
+
|
|
55
|
+
// A day the data cannot price is not derived again by this method
|
|
56
|
+
const unsupported = new Map();
|
|
57
|
+
|
|
58
|
+
// Lookups are serialized so that each estimate can guide the next one
|
|
59
|
+
const queue = asyncQueue(({date}, cbk) => {
|
|
60
|
+
// The shipped prices are read the first time a price is looked up
|
|
61
|
+
const shipped = (() => {
|
|
62
|
+
try {
|
|
63
|
+
return {rates: knownPrices({}).rates};
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return {err};
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
|
|
69
|
+
// Exit early when the shipped prices cannot be read
|
|
70
|
+
if (!!shipped.err) {
|
|
71
|
+
return cbk([503, 'FailedToReadKnownPricesToGetChainRate', {
|
|
72
|
+
err: shipped.err,
|
|
73
|
+
}]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return getChainPrice({
|
|
77
|
+
date,
|
|
78
|
+
estimates,
|
|
79
|
+
fs,
|
|
80
|
+
lnd,
|
|
81
|
+
logger,
|
|
82
|
+
path,
|
|
83
|
+
unsupported,
|
|
84
|
+
known: shipped.rates,
|
|
85
|
+
},
|
|
86
|
+
cbk);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const getChainRate = ({currency, date, fiat}, cbk) => {
|
|
90
|
+
if (currency !== supportedCurrency) {
|
|
91
|
+
return cbk([400, 'UnsupportedCurrencyToGetChainRate']);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!date || typeof date !== 'string') {
|
|
95
|
+
return cbk([400, 'ExpectedDateToGetChainRate']);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (fiat !== supportedFiat) {
|
|
99
|
+
return cbk([400, 'UnsupportedFiatToGetChainRate']);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A date is priced on its day in UTC
|
|
103
|
+
const {day} = dayOfDate({date});
|
|
104
|
+
|
|
105
|
+
// Exit early when the date is not a readable date on a calendar day
|
|
106
|
+
if (!day) {
|
|
107
|
+
return cbk([400, 'ExpectedValidDateToGetChainRate']);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const earliest = earliestSupportedDay({}).day;
|
|
111
|
+
|
|
112
|
+
// Exit early when the date is before the days prices are derived for
|
|
113
|
+
if (day < earliest) {
|
|
114
|
+
return cbk([400, 'DateBeforeSupportedRangeToGetChainRate', {
|
|
115
|
+
earliest_day: earliest,
|
|
116
|
+
}]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return queue.push({date: day}, (err, res) => {
|
|
120
|
+
if (!!err) {
|
|
121
|
+
return cbk(err);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return cbk(null, {cents: res.cents});
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return {getChainRate};
|
|
129
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"blockHeaderByteLength": 80,
|
|
3
|
+
"blockHeaderHexLength": 160,
|
|
4
|
+
"centsPerDollar": 100,
|
|
5
|
+
"confirmationsAfterDay": 6,
|
|
6
|
+
"defaultMaxPrice": 10000000,
|
|
7
|
+
"defaultMinPrice": 100,
|
|
8
|
+
"fetchLimit": 4,
|
|
9
|
+
"half": 0.5,
|
|
10
|
+
"kernelBins": 2,
|
|
11
|
+
"maxTokens": 10000000000000,
|
|
12
|
+
"minAliasMargin": 1,
|
|
13
|
+
"minTokens": 1000,
|
|
14
|
+
"msPerDay": 86400000,
|
|
15
|
+
"msPerSecond": 1000,
|
|
16
|
+
"noiseRadius": 12,
|
|
17
|
+
"roundTokensMultiple": 1000,
|
|
18
|
+
"sampleLogInterval": 10,
|
|
19
|
+
"spikeKernel": [0.3, 0.7, 1, 0.7, 0.3],
|
|
20
|
+
"tokensPerBigUnit": 100000000,
|
|
21
|
+
"unpricedErrors": [
|
|
22
|
+
"ExpectedPaymentOutputsInBlocksToGetPrice",
|
|
23
|
+
"FailedToDerivePriceFromOutputs",
|
|
24
|
+
"FailedToDerivePriceInWidenedWindow"
|
|
25
|
+
]
|
|
26
|
+
}
|