ln-accounting 4.3.1 → 5.0.3

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 (32) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -2
  3. package/esplora/get_esplora_tx.js +88 -0
  4. package/esplora/get_esplora_vout.js +74 -0
  5. package/esplora/get_proxy_tx.js +74 -0
  6. package/esplora/get_proxy_vout.js +62 -0
  7. package/esplora/index.js +4 -0
  8. package/fiat/constants.json +1 -1
  9. package/fiat/get_coingecko_historic_rate.js +80 -0
  10. package/fiat/get_historic_rate.js +3 -1
  11. package/package.json +7 -7
  12. package/records/get_chain_transactions.js +80 -36
  13. package/report/get_accounting_report.js +1 -1
  14. package/test/blockstream/test_get_blockstream_tx.js +109 -0
  15. package/test/fiat/test_get_coincap_historic_rate.js +1 -1
  16. package/test/fiat/test_get_coindesk_historic_rate.js +1 -1
  17. package/test/fiat/test_get_coingecko_historic_rate.js +86 -0
  18. package/test/fiat/test_get_fiat_values.js +1 -1
  19. package/test/fiat/test_get_historic_rate.js +4 -2
  20. package/test/harmony/test_categorize_records.js +1 -1
  21. package/test/harmony/test_chain_fees_as_records.js +1 -1
  22. package/test/harmony/test_chain_receives_as_records.js +1 -1
  23. package/test/harmony/test_chain_sends_as_records.js +1 -1
  24. package/test/harmony/test_formatted_notes.js +1 -1
  25. package/test/harmony/test_forwards_as_records.js +1 -1
  26. package/test/harmony/test_harmonize.js +1 -1
  27. package/test/harmony/test_invoices_as_records.js +1 -1
  28. package/test/harmony/test_notes_for_chain_transaction.js +1 -1
  29. package/test/harmony/test_payments_as_records.js +1 -1
  30. package/test/harmony/test_records_with_fiat.js +1 -1
  31. package/test/records/test_get_all_invoices.js +1 -1
  32. package/test/records/test_get_all_payments.js +3 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Versions
2
2
 
3
+ ## Version 5.0.3
4
+
5
+ - `getChainTransactions`: Add mempool space tx data lookup method
6
+
7
+ ## Version 5.0.2
8
+
9
+ - `getChainTransactions`: Optimize lookup speed when an after date is specified
10
+
11
+ ## Version 5.0.1
12
+
13
+ - `getAccountingReport`: Adjust sweeps to be net zero for transacted amount, plus fees
14
+
15
+ ## Version 5.0.0
16
+
17
+ - `getAccountingReport`: Add support for coingecko historic rate lookup, use as default
18
+
19
+ ### Breaking Changes
20
+
21
+ - Node.js version 12 or higher is now required
22
+
3
23
  ## Version 4.3.1
4
24
 
5
25
  - `getAccountingReport`: Fix reporting for AMP push invoices
package/README.md CHANGED
@@ -18,11 +18,11 @@ Note: Chain fees does not include chain fees paid to close channels
18
18
  [before]: <Records Created Before ISO 8601 Date>
19
19
  [category]: <Category Filter String>
20
20
  currency: <Base Currency Type String>
21
- fiat: <Fiat Currency Type String>
21
+ [fiat]: <Fiat Currency Type String>
22
22
  lnd: <Authenticated LND gRPC API Object>
23
23
  [network]: <Network Name String>
24
24
  [rate]: <Exchange Function> ({currency, date, fiat}, cbk) => (err, {cents})
25
- rate_provider: <Fiat Rate Provider String> coincap || coindesk
25
+ [rate_provider]: <Fiat Rate Provider String> coindesk || coingecko
26
26
  request: <Request Function>
27
27
  }
28
28
 
@@ -0,0 +1,88 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const dateFromEpoch = epoch => new Date(epoch * 1e3).toISOString();
5
+ const {isArray} = Array;
6
+ const url = (api, id) => `${api}tx/${id}`;
7
+
8
+ /** Get transaction details from Esplora-compatible API
9
+
10
+ {
11
+ api: <Esplora API Base String>
12
+ id: <Transaction Id Hex String>
13
+ request: <Request Function>
14
+ }
15
+
16
+ @returns via cbk or Promise
17
+ {
18
+ [confirmation_height]: <Confirmed In Block At Height Number>
19
+ [created_at]: <Transaction Confirmation Date ISO 8601 Date String>
20
+ [block_id]: <Confirmed In Block With Hash Hex String>
21
+ fee: <Transaction Fee Tokens Number>
22
+ output_addresses: [<Output Address String>]
23
+ }
24
+ */
25
+ module.exports = ({api, id, request}, cbk) => {
26
+ return new Promise((resolve, reject) => {
27
+ return asyncAuto({
28
+ // Check argument
29
+ validate: cbk => {
30
+ if (!api) {
31
+ return cbk([400, 'ExpectedBaseEsploraApiToGetEsploraTx']);
32
+ }
33
+
34
+ if (!id) {
35
+ return cbk([400, 'ExpectedTransactionIdToGetEsploraTx']);
36
+ }
37
+
38
+ if (!request) {
39
+ return cbk([400, 'ExpectedRequestFunctionToGetEsploraTx']);
40
+ }
41
+
42
+ return cbk();
43
+ },
44
+
45
+ // Get tx details
46
+ getDetails: ['validate', ({}, cbk) => {
47
+ return request({json: true, url: url(api, id)}, (err, r, body) => {
48
+ if (!!err) {
49
+ return cbk([503, 'UnexpectedErrorGettingEsploraTx', {err}]);
50
+ }
51
+
52
+ if (!body) {
53
+ return cbk([503, 'ExpectedTxLookupResultForEsploraTx']);
54
+ }
55
+
56
+ if (body.fee === undefined) {
57
+ return cbk([503, 'ExpectedTransactionFeeInResultFromEsplora']);
58
+ }
59
+
60
+ if (!body.status) {
61
+ return cbk([503, 'ExpectedStatusOfEsploraTransaction']);
62
+ }
63
+
64
+ if (!isArray(body.vout)) {
65
+ return cbk([503, 'ExpectedOutputsInEsploraTransaction']);
66
+ }
67
+
68
+ // Exit early when transaction is not confirmed
69
+ if (!body.status.confirmed) {
70
+ return cbk(null, {
71
+ fee: body.fee,
72
+ output_addresses: body.vout.map(n => n.scriptpubkey_address),
73
+ });
74
+ }
75
+
76
+ return cbk(null, {
77
+ block_id: body.status.block_hash,
78
+ confirmation_height: body.status.block_height,
79
+ created_at: dateFromEpoch(body.status.block_time),
80
+ fee: body.fee,
81
+ output_addresses: body.vout.map(n => n.scriptpubkey_address),
82
+ });
83
+ });
84
+ }],
85
+ },
86
+ returnResult({reject, resolve, of: 'getDetails'}, cbk));
87
+ });
88
+ };
@@ -0,0 +1,74 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const dateFromEpoch = epoch => new Date(epoch * 1e3).toISOString();
5
+ const {isArray} = Array;
6
+ const url = (api, id) => `${api}tx/${id}`;
7
+
8
+ /** Get transaction details from Esplora-compatible API
9
+
10
+ {
11
+ api: <Esplora API Base String>
12
+ id: <Transaction Id Hex String>
13
+ request: <Request Function>
14
+ }
15
+
16
+ @returns via cbk or Promise
17
+ {
18
+ tokens: <Transaction Output Tokens Number>
19
+ }
20
+ */
21
+ module.exports = ({api, id, request, vout}, cbk) => {
22
+ return new Promise((resolve, reject) => {
23
+ return asyncAuto({
24
+ // Check argument
25
+ validate: cbk => {
26
+ if (!api) {
27
+ return cbk([400, 'ExpectedApiPathToGetEsploraVout']);
28
+ }
29
+
30
+ if (!id) {
31
+ return cbk([400, 'ExpectedTransactionIdToGetEsploraVout']);
32
+ }
33
+
34
+ if (!request) {
35
+ return cbk([400, 'ExpectedRequestFunctionToGetEsploraVout']);
36
+ }
37
+
38
+ if (vout === undefined) {
39
+ return cbk([400, 'ExpectedTransactionOutputIndexToGetEsploraVout']);
40
+ }
41
+
42
+ return cbk();
43
+ },
44
+
45
+ // Get tx details
46
+ getDetails: ['validate', ({}, cbk) => {
47
+ return request({json: true, url: url(api, id)}, (err, r, body) => {
48
+ if (!!err) {
49
+ return cbk([503, 'UnexpectedErrorGettingExploraTx', {err}]);
50
+ }
51
+
52
+ if (!body) {
53
+ return cbk([503, 'ExpectedTxLookupResultForEsploraTx']);
54
+ }
55
+
56
+ if (!isArray(body.vout)) {
57
+ return cbk([503, 'ExpectedOutputsArrayForExploraTx']);
58
+ }
59
+
60
+ if (!body.vout[vout]) {
61
+ return cbk([503, 'ExpectedOutputInEsploraTxDetails']);
62
+ }
63
+
64
+ if (!body.vout[vout].value) {
65
+ return cbk([503, 'ExpectedOutputValueInEsploraTxDetails']);
66
+ }
67
+
68
+ return cbk(null, {tokens: body.vout[vout].value});
69
+ });
70
+ }],
71
+ },
72
+ returnResult({reject, resolve, of: 'getDetails'}, cbk));
73
+ });
74
+ };
@@ -0,0 +1,74 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const getEsploraTx = require('./get_esplora_tx');
5
+
6
+ const apiBlockstreamBtc = 'https://blockstream.info/api/';
7
+ const apiBlockstreamBtcTestnet = 'https://blockstream.info/testnet/api/';
8
+ const apiMempoolSpaceBtc = 'https://mempool.space/api/';
9
+ const btcTestnet = 'btctestnet';
10
+ const random = arr => arr[Math.floor(Math.random() * arr.length)];
11
+
12
+ /** Get a transaction as a proxy for a local transaction
13
+
14
+ {
15
+ id: <Transaction Id Hex String>
16
+ [network]: <Network Name String>
17
+ request: <Request Function>
18
+ }
19
+
20
+ @returns via cbk or Promise
21
+ {
22
+ [block_id]: <Block Hash Hex String>
23
+ [confirmation_height]: <Transaction Confirmed At Height Number>
24
+ created_at: <Transaction Created At ISO 8601 Date String>>
25
+ fee: <Transaction Fee Tokens Number>
26
+ id: <Transaction Id Hex String>
27
+ is_confirmed: <Transaction Confirmed Bool>
28
+ output_addresses: [<Transaction Output Address String>]
29
+ }
30
+ */
31
+ module.exports = ({id, network, request}, cbk) => {
32
+ return new Promise((resolve, reject) => {
33
+ return asyncAuto({
34
+ // Check arguments
35
+ validate: cbk => {
36
+ if (!id) {
37
+ return cbk([400, 'ExpectedPaymentIdToGetProxyTransaction']);
38
+ }
39
+
40
+ if (!request) {
41
+ return cbk([400, 'ExpectedRequestFunctionToGetProxyTransaction']);
42
+ }
43
+
44
+ return cbk();
45
+ },
46
+
47
+ // Determine the API to use
48
+ api: ['validate', ({}, cbk) => {
49
+ if (network === btcTestnet) {
50
+ return cbk(null, apiBlockstreamBtcTestnet);
51
+ }
52
+
53
+ return cbk(null, random([apiBlockstreamBtc, apiMempoolSpaceBtc]));
54
+ }],
55
+
56
+ // Get transaction
57
+ getTx: ['api', ({api}, cbk) => getEsploraTx({api, id, request}, cbk)],
58
+
59
+ // Transaction details
60
+ tx: ['getTx', ({api, getTx}, cbk) => {
61
+ return cbk(null, {
62
+ id,
63
+ block_id: getTx.block_id,
64
+ confirmation_height: getTx.confirmation_height,
65
+ created_at: getTx.created_at || new Date().toISOString(),
66
+ fee: getTx.fee,
67
+ is_confirmed: !!getTx.block_id,
68
+ output_addresses: getTx.output_addresses,
69
+ });
70
+ }],
71
+ },
72
+ returnResult({reject, resolve, of: 'tx'}, cbk));
73
+ });
74
+ };
@@ -0,0 +1,62 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const getEsploraVout = require('./get_esplora_vout');
5
+
6
+ const apiBlockstreamBtc = 'https://blockstream.info/api/';
7
+ const apiBlockstreamBtcTestnet = 'https://blockstream.info/testnet/api/';
8
+ const apiMempoolSpaceBtc = 'https://mempool.space/api/';
9
+ const btcTestnet = 'btctestnet';
10
+ const random = arr => arr[Math.floor(Math.random() * arr.length)];
11
+
12
+ /** Get a vout from an esplora endpoint
13
+
14
+ {
15
+ id: <Transaction Id Hex String>
16
+ [network]: <Network Name String>
17
+ request: <Request Function>
18
+ vout: <Transaction Output Index Number>
19
+ }
20
+
21
+ @returns via cbk or Promise
22
+ {
23
+ tokens: <Transaction Output Tokens Number>
24
+ }
25
+ */
26
+ module.exports = ({id, network, request, vout}, cbk) => {
27
+ return new Promise((resolve, reject) => {
28
+ return asyncAuto({
29
+ // Check arguments
30
+ validate: cbk => {
31
+ if (!id) {
32
+ return cbk([400, 'ExpectedPaymentIdToGetProxyVout']);
33
+ }
34
+
35
+ if (!request) {
36
+ return cbk([400, 'ExpectedRequestFunctionToGetProxyVout']);
37
+ }
38
+
39
+ if (vout === undefined) {
40
+ return cbk([400, 'ExpectedTransactionOutputIndexToGetProxyVout']);
41
+ }
42
+
43
+ return cbk();
44
+ },
45
+
46
+ // Determine the API to use
47
+ api: ['validate', ({}, cbk) => {
48
+ if (network === btcTestnet) {
49
+ return cbk(null, apiBlockstreamBtcTestnet);
50
+ }
51
+
52
+ return cbk(null, random([apiBlockstreamBtc, apiMempoolSpaceBtc]));
53
+ }],
54
+
55
+ // Get transaction vout
56
+ getVout: ['api', ({api}, cbk) => {
57
+ return getEsploraVout({api, id, request, vout}, cbk);
58
+ }],
59
+ },
60
+ returnResult({reject, resolve, of: 'getVout'}, cbk));
61
+ });
62
+ };
@@ -0,0 +1,4 @@
1
+ const getProxyTx = require('./get_proxy_tx');
2
+ const getProxyVout = require('./get_proxy_vout');
3
+
4
+ module.exports = {getProxyTx, getProxyVout};
@@ -1,3 +1,3 @@
1
1
  {
2
- "rateProviders": ["coincap", "coindesk"]
2
+ "rateProviders": ["coincap", "coindesk", "coingecko"]
3
3
  }
@@ -0,0 +1,80 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const asCoingeckoDate = yyyymmdd => yyyymmdd.split('-').reverse().join('-');
5
+ const centsPerDollar = 100;
6
+ const dateComponents = date => date.substring(0, 'yyyy-mm-dd'.length);
7
+ const remoteServiceTimeoutMs = 30 * 1000;
8
+ const url = 'https://api.coingecko.com/api/v3/coins/bitcoin/history';
9
+
10
+ /** Get the number of cents for a big unit token from coingecko
11
+
12
+ {
13
+ currency: <Currency Type String>
14
+ date: <ISO 8601 Date String>
15
+ fiat: <Fiat Type String>
16
+ request: <Request Function>
17
+ }
18
+
19
+ @returns via cbk or Promise
20
+ {
21
+ cents: <Cents Per Token Number>
22
+ }
23
+ */
24
+ module.exports = ({currency, date, fiat, request}, cbk) => {
25
+ return new Promise((resolve, reject) => {
26
+ return asyncAuto({
27
+ // Check arguments
28
+ validate: cbk => {
29
+ if (currency !== 'BTC') {
30
+ return cbk([400, 'UnsupportedCurrencyForCoingeckoFiatRateLookup']);
31
+ }
32
+
33
+ if (!date) {
34
+ return cbk([400, 'ExpectedDateForCoingeckoRateLookup']);
35
+ }
36
+
37
+ if (fiat !== 'USD') {
38
+ return cbk([400, 'UnsupportedFiatTypeForCoingeckoFiatRateLookup']);
39
+ }
40
+
41
+ if (!request) {
42
+ return cbk([400, 'ExpectedRequestMethodForCoingeckoFiatRateLookup']);
43
+ }
44
+
45
+ return cbk();
46
+ },
47
+
48
+ // Get rate
49
+ getRate: ['validate', ({}, cbk) => {
50
+ return request({
51
+ url,
52
+ json: true,
53
+ qs: {
54
+ date: asCoingeckoDate(dateComponents(date)),
55
+ localization: false,
56
+ },
57
+ timeout: remoteServiceTimeoutMs,
58
+ },
59
+ (err, r, body) => {
60
+ if (!!err) {
61
+ return cbk([503, 'UnexpectedErrGettingCoingeckoPastRate', {err}]);
62
+ }
63
+
64
+ if (!body || !body.market_data || !body.market_data.current_price) {
65
+ return cbk([503, 'UnexpectedResponseInCoingeckoPastRateResponse']);
66
+ }
67
+
68
+ if (!body.market_data.current_price.usd) {
69
+ return cbk([503, 'ExpectedCoingeckoCurrentPriceForFiat']);
70
+ }
71
+
72
+ const cents = body.market_data.current_price.usd * centsPerDollar;
73
+
74
+ return cbk(null, {cents});
75
+ });
76
+ }],
77
+ },
78
+ returnResult({reject, resolve, of: 'getRate'}, cbk));
79
+ });
80
+ };
@@ -4,8 +4,9 @@ const {returnResult} = require('asyncjs-util');
4
4
 
5
5
  const getCoincapHistoricRate = require('./get_coincap_historic_rate');
6
6
  const getCoindeskHistoricRate = require('./get_coindesk_historic_rate');
7
+ const getCoingeckoHistoricRate = require('./get_coingecko_historic_rate');
7
8
 
8
- const defaultRateProvider = 'coincap';
9
+ const defaultRateProvider = 'coingecko';
9
10
  const interval = retryCount => Math.random() * 5000 * Math.pow(2, retryCount);
10
11
  const times = 10;
11
12
 
@@ -55,6 +56,7 @@ module.exports = ({currency, date, fiat, provider, request}, cbk) => {
55
56
  const providers = {
56
57
  coincap: getCoincapHistoricRate,
57
58
  coindesk: getCoindeskHistoricRate,
59
+ coingecko: getCoingeckoHistoricRate,
58
60
  };
59
61
 
60
62
  const source = providers[provider || defaultRateProvider];
package/package.json CHANGED
@@ -7,19 +7,19 @@
7
7
  "url": "https://github.com/alexbosworth/ln-accounting/issues"
8
8
  },
9
9
  "dependencies": {
10
- "async": "3.2.0",
10
+ "async": "3.2.1",
11
11
  "asyncjs-util": "1.2.6",
12
12
  "bitcoinjs-lib": "5.2.0",
13
- "goldengate": "10.1.0",
13
+ "goldengate": "10.4.0",
14
14
  "json2csv": "5.0.6",
15
- "ln-service": "51.8.5"
15
+ "ln-service": "52.4.0"
16
16
  },
17
17
  "description": "lnd accounting reports",
18
18
  "devDependencies": {
19
- "tap": "15.0.9"
19
+ "@alexbosworth/tap": "15.0.10"
20
20
  },
21
21
  "engines": {
22
- "node": ">=10.10.0"
22
+ "node": ">=12"
23
23
  },
24
24
  "keywords": [
25
25
  "accounting",
@@ -33,7 +33,7 @@
33
33
  "url": "https://github.com/alexbosworth/ln-accounting.git"
34
34
  },
35
35
  "scripts": {
36
- "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 test/fiat/*.js test/harmony/*.js test/records/*.js"
36
+ "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 test/blockstream/*.js test/fiat/*.js test/harmony/*.js test/records/*.js"
37
37
  },
38
- "version": "4.3.1"
38
+ "version": "5.0.3"
39
39
  }
@@ -3,25 +3,29 @@ const asyncMapSeries = require('async/mapSeries');
3
3
  const asyncRetry = require('async/retry');
4
4
  const {getChainTransactions} = require('ln-service');
5
5
  const {getClosedChannels} = require('ln-service');
6
+ const {getHeight} = require('ln-service');
6
7
  const {getSweepTransactions} = require('ln-service');
7
8
  const {returnResult} = require('asyncjs-util');
8
9
  const {Transaction} = require('bitcoinjs-lib');
9
10
 
10
- const {getBlockstreamTx} = require('./../blockstream');
11
- const {getBlockstreamVout} = require('./../blockstream');
11
+ const {getProxyTx} = require('./../esplora');
12
+ const {getProxyVout} = require('./../esplora');
12
13
 
14
+ const dateAsMs = date => new Date(date).getTime();
13
15
  const {fromHex} = Transaction;
14
- const interval = retryCount => 100 * Math.pow(2, retryCount);
16
+ const interval = 200;
15
17
  const {isArray} = Array;
18
+ const msAsBlocks = ms => Math.ceil(ms / 1000 / 60 / 2.5);
19
+ const {now} = Date;
16
20
  const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
17
- const times = 10;
21
+ const times = 15;
18
22
 
19
23
  /** Get chain transactions, including sweep fees
20
24
 
21
25
  {
22
26
  [after]: <Records Created After ISO 8601 Date>
23
27
  [before]: <Records Created Before ISO 8601 Date>
24
- lnd: <Authenticated LND Object>
28
+ lnd: <Authenticated LND API Object>
25
29
  [network]: <Network Name String>
26
30
  request: <Request Function>
27
31
  }
@@ -63,11 +67,27 @@ module.exports = ({after, before, lnd, network, request}, cbk) => {
63
67
  // Get the closed channels
64
68
  getClosed: ['validate', ({}, cbk) => getClosedChannels({lnd}, cbk)],
65
69
 
70
+ // Get the chain height
71
+ getHeight: ['validate', ({}, cbk) => getHeight({lnd}, cbk)],
72
+
66
73
  // Get the sweep transactions
67
74
  getSweeps: ['validate', ({}, cbk) => getSweepTransactions({lnd}, cbk)],
68
75
 
69
76
  // Get the regular set of chain transactions
70
- getTx: ['validate', ({}, cbk) => getChainTransactions({lnd}, cbk)],
77
+ getTx: ['getHeight', ({getHeight}, cbk) => {
78
+ // Exit early when there is no after constraint
79
+ if (!after) {
80
+ return getChainTransactions({lnd}, cbk);
81
+ }
82
+
83
+ const height = getHeight.current_block_height;
84
+
85
+ return getChainTransactions({
86
+ lnd,
87
+ after: height - msAsBlocks(now() - dateAsMs(after)),
88
+ },
89
+ cbk);
90
+ }],
71
91
 
72
92
  // Time-relevant sweep transactions
73
93
  sweepTransactions: ['getSweeps', ({getSweeps}, cbk) => {
@@ -96,7 +116,7 @@ module.exports = ({after, before, lnd, network, request}, cbk) => {
96
116
  }
97
117
 
98
118
  return asyncRetry({interval, times}, cbk => {
99
- return getBlockstreamVout({
119
+ return getProxyVout({
100
120
  network,
101
121
  request,
102
122
  id: spend.transaction_id,
@@ -131,12 +151,13 @@ module.exports = ({after, before, lnd, network, request}, cbk) => {
131
151
  confirmation_count: tx.confirmation_count,
132
152
  confirmation_height: tx.confirmation_height,
133
153
  created_at: tx.created_at,
154
+ description: 'Sweep',
134
155
  fee: tx.fee || (sumOf(spends.map(n => n.tokens)) - totalOut),
135
156
  id: tx.id,
136
157
  is_confirmed: tx.is_confirmed,
137
158
  is_outgoing: true,
138
159
  output_addresses: tx.output_addresses,
139
- tokens: tx.tokens,
160
+ tokens: tx.fee || (sumOf(spends.map(n => n.tokens)) - totalOut),
140
161
  transaction: tx.transaction,
141
162
  });
142
163
  });
@@ -145,14 +166,32 @@ module.exports = ({after, before, lnd, network, request}, cbk) => {
145
166
  }],
146
167
 
147
168
  // Calculate closing fees for channels that were locally initiated
148
- getClosingFees: ['getClosed', 'getTx', ({getClosed, getTx}, cbk) => {
169
+ getClosingFees: [
170
+ 'getClosed',
171
+ 'getHeight',
172
+ 'getTx',
173
+ ({getClosed, getHeight, getTx}, cbk) =>
174
+ {
175
+ const height = getHeight.current_block_height;
176
+
149
177
  const channels = getClosed.channels
150
178
  .filter(n => !!n.close_transaction_id)
151
- .filter(n => n.is_partner_initiated === false);
179
+ .filter(n => n.is_partner_initiated === false)
180
+ .filter(channel => {
181
+ // Exit early when not checking channel close height vs after
182
+ if (!after || !channel.close_confirm_height) {
183
+ return true;
184
+ }
185
+
186
+ const afterHeight = height - msAsBlocks(now() - dateAsMs(after));
187
+
188
+ // Do not consider channels that closed before the after constraint
189
+ return channel.close_confirm_height > afterHeight;
190
+ });
152
191
 
153
192
  return asyncMapSeries(channels, (channel, cbk) => {
154
193
  const tx = getTx.transactions.find(tx => {
155
- return tx.id === channel.close_transaction_id
194
+ return tx.id === channel.close_transaction_id;
156
195
  });
157
196
 
158
197
  const hasMissingLocalData = (() => {
@@ -172,28 +211,32 @@ module.exports = ({after, before, lnd, network, request}, cbk) => {
172
211
 
173
212
  // Exit early when the close transaction is missing
174
213
  if (hasMissingLocalData) {
175
- return getBlockstreamTx({
176
- network,
177
- request,
178
- id: channel.close_transaction_id,
179
- },
180
- (err, res) => {
181
- if (!!err) {
182
- return cbk(err);
183
- }
184
-
185
- return cbk(null, {
186
- block_id: res.block_id,
187
- confirmation_height: res.confirmation_height,
188
- created_at: res.created_at || new Date().toISOString(),
189
- fee: res.fee,
214
+ return asyncRetry({interval, times}, cbk => {
215
+ return getProxyTx({
216
+ network,
217
+ request,
190
218
  id: channel.close_transaction_id,
191
- is_confirmed: !!res.block_id,
192
- is_outgoing: true,
193
- output_addresses: res.output_addresses,
194
- tokens: res.fee,
219
+ },
220
+ (err, res) => {
221
+ if (!!err) {
222
+ return cbk(err);
223
+ }
224
+
225
+ return cbk(null, {
226
+ block_id: res.block_id,
227
+ confirmation_height: res.confirmation_height,
228
+ created_at: res.created_at,
229
+ description: 'Channel close',
230
+ fee: res.fee,
231
+ id: res.id,
232
+ is_confirmed: res.is_confirmed,
233
+ is_outgoing: true,
234
+ output_addresses: res.output_addresses,
235
+ tokens: res.fee,
236
+ });
195
237
  });
196
- });
238
+ },
239
+ cbk);
197
240
  }
198
241
 
199
242
  const inputs = fromHex(tx.transaction).ins.map(({hash, index}) => {
@@ -216,13 +259,13 @@ module.exports = ({after, before, lnd, network, request}, cbk) => {
216
259
  confirmation_count: tx.confirmation_count,
217
260
  confirmation_height: tx.confirmation_height,
218
261
  created_at: tx.created_at,
219
- description: tx.description,
262
+ description: `Channel close: ${tx.description}`,
220
263
  fee: inputsValue - sumOf(outputsValue),
221
264
  id: tx.id,
222
265
  is_confirmed: tx.is_confirmed,
223
266
  is_outgoing: true,
224
267
  output_addresses: tx.output_addresses,
225
- tokens: tx.tokens,
268
+ tokens: inputsValue - sumOf(outputsValue),
226
269
  transaction: tx.transaction,
227
270
  });
228
271
  },
@@ -231,16 +274,17 @@ module.exports = ({after, before, lnd, network, request}, cbk) => {
231
274
 
232
275
  // Consolidate transactions, including missing fees
233
276
  transactions: [
277
+ 'getClosed',
234
278
  'getClosingFees',
235
279
  'getSweepFees',
236
280
  'getTx',
237
- ({getClosingFees, getSweepFees, getTx}, cbk) =>
281
+ ({getClosed, getClosingFees, getSweepFees, getTx}, cbk) =>
238
282
  {
239
- const closes = getClosingFees.map(({id}) => id);
283
+ const closeIds = getClosed.channels.map(n => n.close_transaction_id);
240
284
  const sweeps = getSweepFees.map(({id}) => id);
241
285
 
242
286
  const normalTx = getTx.transactions.filter(({id}) => {
243
- return !closes.includes(id) && !sweeps.includes(id);
287
+ return !closeIds.includes(id) && !sweeps.includes(id);
244
288
  });
245
289
 
246
290
  const transactions = []
@@ -42,7 +42,7 @@ const times = 10;
42
42
  lnd: <LND gRPC Object>
43
43
  [network]: <Network Name String>
44
44
  [rate]: <Exchange Function> ({currency, date, fiat}, cbk) => (err, {cents})
45
- [rate_provider]: <Fiat Rate Provider String> // coincap || coindesk
45
+ [rate_provider]: <Fiat Rate Provider String> // coindesk || coingecko
46
46
  request: <Request Function>
47
47
  }
48
48
 
@@ -0,0 +1,109 @@
1
+ const {test} = require('@alexbosworth/tap');
2
+
3
+ const method = require('./../../blockstream/get_blockstream_tx');
4
+
5
+ const makeArgs = overrides => {
6
+ const args = {
7
+ id: Buffer.alloc(32).toString('hex'),
8
+ network: 'btc',
9
+ request: ({}, cbk) => cbk(null, null, {
10
+ fee: 1,
11
+ status: {
12
+ block_hash: Buffer.alloc(32).toString('hex'),
13
+ block_height: 1,
14
+ block_time: 1,
15
+ confirmed: true,
16
+ },
17
+ vout: [{scriptpubkey_address: 'bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'}],
18
+ }),
19
+ };
20
+
21
+ Object.keys(overrides).forEach(k => args[k] = overrides[k]);
22
+
23
+ return args;
24
+ };
25
+
26
+ const tests = [
27
+ {
28
+ args: makeArgs({id: undefined}),
29
+ description: 'An id is required',
30
+ error: [400, 'ExpectedTransactionIdToGetBlockstreamTxFee'],
31
+ },
32
+ {
33
+ args: makeArgs({network: 'network'}),
34
+ description: 'A known network is required',
35
+ error: [400, 'UnsupportedNetworkToGetBlockstreamTxFee'],
36
+ },
37
+ {
38
+ args: makeArgs({request: undefined}),
39
+ description: 'A request method is required',
40
+ error: [400, 'ExpectedRequestFunctionToGetBlockstreamTxFee'],
41
+ },
42
+ {
43
+ args: makeArgs({request: ({}, cbk) => cbk('err')}),
44
+ description: 'Request errors are passed back',
45
+ error: [503, 'UnexpectedErrorGettingBlockstreamTxFee', {err: 'err'}],
46
+ },
47
+ {
48
+ args: makeArgs({request: ({}, cbk) => cbk()}),
49
+ description: 'A response body is expected',
50
+ error: [503, 'ExpectedTxLookupResultForBlockstreamTxFee'],
51
+ },
52
+ {
53
+ args: makeArgs({request: ({}, cbk) => cbk(null, null, {})}),
54
+ description: 'A transaction fee is required',
55
+ error: [503, 'ExpectedTransactionFeeInResultFromBlockstream'],
56
+ },
57
+ {
58
+ args: makeArgs({request: ({}, cbk) => cbk(null, null, {fee: 1})}),
59
+ description: 'Transaction status is required',
60
+ error: [503, 'ExpectedStatusOfBlockstreamTransaction'],
61
+ },
62
+ {
63
+ args: makeArgs({
64
+ request: ({}, cbk) => cbk(null, null, {fee: 1, status: {}}),
65
+ }),
66
+ description: 'A vout array is required',
67
+ error: [503, 'ExpectedOutputsInBlockstreamTransaction'],
68
+ },
69
+ {
70
+ args: makeArgs({
71
+ network: 'btctestnet',
72
+ request: ({}, cbk) => cbk(null, null, {
73
+ fee: 1,
74
+ status: {},
75
+ vout: [{scriptpubkey_address: 'bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'}],
76
+ }),
77
+ }),
78
+ description: 'An unconfirmed tx is returned',
79
+ expected: {
80
+ fee: 1,
81
+ output_addresses: ['bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'],
82
+ },
83
+ },
84
+ {
85
+ args: makeArgs({}),
86
+ description: 'Blockstream tx info is returned',
87
+ expected: {
88
+ confirmation_height: 1,
89
+ created_at: '1970-01-01T00:00:01.000Z',
90
+ block_id: Buffer.alloc(32).toString('hex'),
91
+ fee: 1,
92
+ output_addresses: ['bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj']
93
+ },
94
+ },
95
+ ];
96
+
97
+ tests.forEach(({args, description, error, expected}) => {
98
+ return test(description, async ({end, rejects, strictSame}) => {
99
+ if (!!error) {
100
+ await rejects(method(args), error, 'Got expected error');
101
+ } else {
102
+ const res = await method(args);
103
+
104
+ strictSame(res, expected, 'Got expected result');
105
+ }
106
+
107
+ return end();
108
+ });
109
+ });
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const getCoincapHistoricRate = require('./../../fiat/get_coincap_historic_rate');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const getCoindeskHistoricRate = require('./../../fiat/get_coindesk_historic_rate');
4
4
 
@@ -0,0 +1,86 @@
1
+ const {test} = require('@alexbosworth/tap');
2
+
3
+ const method = require('./../../fiat/get_coingecko_historic_rate');
4
+
5
+ const api = ({qs}, cbk) => {
6
+ return cbk(null, null, {market_data: {current_price: {usd: 12.34}}});
7
+ };
8
+
9
+ const tests = [
10
+ {
11
+ args: {},
12
+ description: 'A currency is required',
13
+ error: [400, 'UnsupportedCurrencyForCoingeckoFiatRateLookup'],
14
+ },
15
+ {
16
+ args: {currency: 'BTC'},
17
+ description: 'A date is required',
18
+ error: [400, 'ExpectedDateForCoingeckoRateLookup'],
19
+ },
20
+ {
21
+ args: {currency: 'BTC', date: new Date().toISOString()},
22
+ description: 'A currency type is required',
23
+ error: [400, 'UnsupportedFiatTypeForCoingeckoFiatRateLookup'],
24
+ },
25
+ {
26
+ args: {currency: 'BTC', date: new Date().toISOString(), fiat: 'USD'},
27
+ description: 'A request method is required',
28
+ error: [400, 'ExpectedRequestMethodForCoingeckoFiatRateLookup'],
29
+ },
30
+ {
31
+ args: {
32
+ date: new Date().toISOString(),
33
+ currency: 'BTC',
34
+ fiat: 'USD',
35
+ request: ({}, cbk) => cbk('err'),
36
+ },
37
+ description: 'Errors returned from request',
38
+ error: [503, 'UnexpectedErrGettingCoingeckoPastRate', {err: 'err'}],
39
+ },
40
+ {
41
+ args: {
42
+ date: new Date().toISOString(),
43
+ currency: 'BTC',
44
+ fiat: 'USD',
45
+ request: ({}, cbk) => cbk(),
46
+ },
47
+ description: 'A body is expected in response',
48
+ error: [503, 'UnexpectedResponseInCoingeckoPastRateResponse'],
49
+ },
50
+ {
51
+ args: {
52
+ date: new Date().toISOString(),
53
+ currency: 'BTC',
54
+ fiat: 'USD',
55
+ request: ({}, cbk) => cbk(null, null, {
56
+ market_data: {current_price: {}},
57
+ }),
58
+ },
59
+ description: 'A price is expected in response',
60
+ error: [503, 'ExpectedCoingeckoCurrentPriceForFiat'],
61
+ },
62
+ {
63
+ args: {
64
+ date: new Date().toISOString(),
65
+ currency: 'BTC',
66
+ fiat: 'USD',
67
+ request: ({qs}, cbk) => api({qs}, cbk),
68
+ },
69
+ description: 'Get coindesk historic rate',
70
+ expected: {cents: 1234},
71
+ },
72
+ ];
73
+
74
+ tests.forEach(({args, description, error, expected}) => {
75
+ return test(description, async ({end, rejects, strictSame}) => {
76
+ if (!!error) {
77
+ await rejects(method(args), error, 'Got expected error');
78
+ } else {
79
+ const res = await method(args);
80
+
81
+ strictSame(res, expected, 'Got expected return value');
82
+ }
83
+
84
+ return end();
85
+ });
86
+ });
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {getFiatValues} = require('./../../fiat/');
4
4
 
@@ -1,10 +1,12 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const getHistoricRate = require('./../../fiat/get_historic_rate');
4
4
 
5
5
  const date = new Date().toISOString();
6
6
 
7
- const api = ({}, cbk) => cbk(null, null, {data: [{date, priceUsd: 12.3401}]});
7
+ const api = ({}, cbk) => {
8
+ return cbk(null, null, {market_data: {current_price: {usd: 12.34}}});
9
+ };
8
10
 
9
11
  const tests = [
10
12
  {
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {categorizeRecords} = require('./../../harmony');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {chainFeesAsRecords} = require('./../../harmony');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {chainReceivesAsRecords} = require('./../../harmony');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {chainSendsAsRecords} = require('./../../harmony');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const formattedNotes = require('./../../harmony/formatted_notes');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {forwardsAsRecords} = require('./../../harmony');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {harmonize} = require('./../../harmony');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {invoicesAsRecords} = require('./../../harmony');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const notesForChainTx = require('./../../harmony/notes_for_chain_transaction');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {paymentsAsRecords} = require('./../../harmony');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {recordsWithFiat} = require('./../../harmony');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {getAllInvoices} = require('./../../records');
4
4
 
@@ -1,4 +1,4 @@
1
- const {test} = require('tap');
1
+ const {test} = require('@alexbosworth/tap');
2
2
 
3
3
  const {getAllPayments} = require('./../../records');
4
4
 
@@ -99,6 +99,7 @@ const tests = [
99
99
  expected: {
100
100
  payments: [{
101
101
  attempts: [{
102
+ confirmed_at: '1970-01-01T00:00:00.001Z',
102
103
  is_confirmed: false,
103
104
  is_failed: true,
104
105
  is_pending: false,
@@ -122,6 +123,7 @@ const tests = [
122
123
  total_mtokens: '1000',
123
124
  },
124
125
  }],
126
+ confirmed_at: undefined,
125
127
  created_at: '1970-01-01T00:00:01.000Z',
126
128
  destination: Buffer.alloc(33).toString('hex'),
127
129
  fee: 1,