balanceofsatoshis 12.8.2 → 12.8.5

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 CHANGED
@@ -1,6 +1,10 @@
1
1
  # Versions
2
2
 
3
- ## 12.8.2
3
+ ## 12.8.5
4
+
5
+ - `telegram`: Fix error logging on /blocknotify
6
+
7
+ ## 12.8.4
4
8
 
5
9
  - `telegram`: Add safeguards to deal with errors on /graph command
6
10
 
package/README.md CHANGED
@@ -499,7 +499,7 @@ On Umbrel this would be:
499
499
  ## --network="host"
500
500
  ## --add-host=umbrel.local:192.168.1.23
501
501
  ## -v $HOME/umbrel/lnd:/home/node/.lnd:ro
502
- docker run -it --rm --network="host" --add-host=umbrel.local:192.168.1.23 -v $HOME/.bos:/home/node/.bos -v $HOME/umbrel/lnd:/home/node/.lnd:ro alexbosworth/balanceofsatoshis report
502
+ docker run -it --rm --network=umbrel_main_network --add-host=umbrel.local:192.168.1.23 -v $HOME/.bos:/home/node/.bos -v $HOME/umbrel/lnd:/home/node/.lnd:ro alexbosworth/balanceofsatoshis report
503
503
  ```
504
504
 
505
505
  Note: For [umbrel-os](https://github.com/getumbrel/umbrel-os) users, when
package/bos CHANGED
@@ -42,6 +42,7 @@ const services = importLazy('./services');
42
42
  const {swapTypes} = commandConstants;
43
43
  const swaps = importLazy('./swaps');
44
44
  const telegram = importLazy('./telegram');
45
+ const triggers = importLazy('./triggers');
45
46
  const wallets = importLazy('./wallets');
46
47
  const {version} = importLazy('./package');
47
48
 
@@ -1817,6 +1818,25 @@ prog
1817
1818
  });
1818
1819
  })
1819
1820
 
1821
+ // Manage triggers
1822
+ .command('triggers', 'Manage event triggers')
1823
+ .option('--node <name>', 'Node to manage triggers on')
1824
+ .visible(false)
1825
+ .action((args, options, logger) => {
1826
+ return new Promise(async (resolve, reject) => {
1827
+ try {
1828
+ return triggers.manageTriggers({
1829
+ logger,
1830
+ ask: (n, cbk) => inquirer.prompt([n]).then(res => cbk(res)),
1831
+ lnd: (await lndForNode(logger, options.node)).lnd,
1832
+ },
1833
+ responses.returnObject({exit, logger, reject, resolve}));
1834
+ } catch (err) {
1835
+ return logger.error({err}) && reject();
1836
+ }
1837
+ });
1838
+ })
1839
+
1820
1840
  // Unlock wallet
1821
1841
  .command('unlock', 'Unlock wallet if locked')
1822
1842
  .help('Check if the wallet is locked, if so use a password file to unlock')
package/package.json CHANGED
@@ -36,7 +36,7 @@
36
36
  "ini": "3.0.0",
37
37
  "inquirer": "8.2.4",
38
38
  "ln-accounting": "5.0.6",
39
- "ln-service": "53.16.0",
39
+ "ln-service": "53.17.0",
40
40
  "ln-sync": "3.12.0",
41
41
  "ln-telegram": "3.21.5",
42
42
  "moment": "2.29.3",
@@ -55,7 +55,7 @@
55
55
  "devDependencies": {
56
56
  "@alexbosworth/tap": "15.0.11",
57
57
  "invoices": "2.0.6",
58
- "ln-docker-daemons": "2.2.10",
58
+ "ln-docker-daemons": "2.2.11",
59
59
  "mock-lnd": "1.4.1",
60
60
  "tiny-secp256k1": "2.2.1"
61
61
  },
@@ -84,5 +84,5 @@
84
84
  "postpublish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t alexbosworth/balanceofsatoshis --push .",
85
85
  "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/arrays/*.js test/balances/*.js test/chain/*.js test/display/*.js test/encryption/*.js test/lnd/*.js test/network/*.js test/nodes/*.js test/peers/*.js test/responses/*.js test/routing/*.js test/services/*.js test/swaps/*.js test/tags/*.js test/telegram/*.js test/wallets/*.js"
86
86
  },
87
- "version": "12.8.2"
87
+ "version": "12.8.5"
88
88
  }
@@ -211,7 +211,7 @@ module.exports = (args, cbk) => {
211
211
  reply: n => ctx.reply(n, markdown),
212
212
  request: args.request,
213
213
  },
214
- err => !!err ? logger.error({err}) : null);
214
+ err => !!err ? args.logger.error({err}) : null);
215
215
  });
216
216
 
217
217
  // Handle command to get the connect id
@@ -335,7 +335,7 @@ module.exports = (args, cbk) => {
335
335
  });
336
336
  });
337
337
  } catch (err) {
338
- args.logger.error(err);
338
+ args.logger.error({err});
339
339
  }
340
340
  });
341
341
 
@@ -0,0 +1,59 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {createInvoice} = require('ln-service');
3
+ const {returnResult} = require('asyncjs-util');
4
+
5
+ const encodeTrigger = require('./encode_trigger');
6
+
7
+ const daysAsMs = days => Number(days) * 1000 * 60 * 60 * 24;
8
+ const defaultTriggerDays = 365;
9
+ const futureDate = ms => new Date(Date.now() + ms).toISOString();
10
+
11
+ /** Create a connectivity with peer trigger
12
+
13
+ {
14
+ id: <Node Id Public Key Hex String>
15
+ lnd: <Authenticated LND API Object>
16
+ }
17
+
18
+ @returns via cbk or Promise
19
+ */
20
+ module.exports = ({id, lnd}, cbk) => {
21
+ return new Promise((resolve, reject) => {
22
+ return asyncAuto({
23
+ // Check arguments
24
+ validate: cbk => {
25
+ if (!id) {
26
+ return cbk([400, 'ExpectedNodeIdToCreateConnectivityTrigger']);
27
+ }
28
+
29
+ if (!lnd) {
30
+ return cbk([400, 'ExpectedLndToCreateConnectivityTrigger']);
31
+ }
32
+
33
+ return cbk();
34
+ },
35
+
36
+ // Encode the trigger
37
+ description: ['validate', ({}, cbk) => {
38
+ try {
39
+ const {encoded} = encodeTrigger({connectivity: {id}});
40
+
41
+ return cbk(null, encoded);
42
+ } catch (err) {
43
+ return cbk([400, err.message]);
44
+ }
45
+ }],
46
+
47
+ // Add the trigger invoice
48
+ create: ['description', ({description}, cbk) => {
49
+ return createInvoice({
50
+ description,
51
+ lnd,
52
+ expires_at: futureDate(daysAsMs(defaultTriggerDays)),
53
+ },
54
+ cbk);
55
+ }],
56
+ },
57
+ returnResult({reject, resolve}, cbk));
58
+ });
59
+ };
@@ -0,0 +1,59 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {createInvoice} = require('ln-service');
3
+ const {returnResult} = require('asyncjs-util');
4
+
5
+ const encodeTrigger = require('./encode_trigger');
6
+
7
+ const daysAsMs = days => Number(days) * 1000 * 60 * 60 * 24;
8
+ const defaultTriggerDays = 365;
9
+ const futureDate = ms => new Date(Date.now() + ms).toISOString();
10
+
11
+ /** Create a follow node trigger
12
+
13
+ {
14
+ id: <Node Id Public Key Hex String>
15
+ lnd: <Authenticated LND API Object>
16
+ }
17
+
18
+ @returns via cbk or Promise
19
+ */
20
+ module.exports = ({id, lnd}, cbk) => {
21
+ return new Promise((resolve, reject) => {
22
+ return asyncAuto({
23
+ // Check arguments
24
+ validate: cbk => {
25
+ if (!id) {
26
+ return cbk([400, 'ExpectedNodeIdToFollowToCreateFollowNodeTrigger']);
27
+ }
28
+
29
+ if (!lnd) {
30
+ return cbk([400, 'ExpectedLndToCreateFollowNodeTrigger']);
31
+ }
32
+
33
+ return cbk();
34
+ },
35
+
36
+ // Encode the trigger
37
+ description: ['validate', ({}, cbk) => {
38
+ try {
39
+ const {encoded} = encodeTrigger({follow: {id}});
40
+
41
+ return cbk(null, encoded);
42
+ } catch (err) {
43
+ return cbk([400, err.message]);
44
+ }
45
+ }],
46
+
47
+ // Add the trigger invoice
48
+ create: ['description', ({description}, cbk) => {
49
+ return createInvoice({
50
+ description,
51
+ lnd,
52
+ expires_at: futureDate(daysAsMs(defaultTriggerDays)),
53
+ },
54
+ cbk);
55
+ }],
56
+ },
57
+ returnResult({reject, resolve}, cbk));
58
+ });
59
+ };
@@ -0,0 +1,45 @@
1
+ const {decodeTlvStream} = require('bolt01');
2
+
3
+ const findRecord = (records, type) => records.find(n => n.type === type);
4
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
5
+ const typeId = '1';
6
+ const typeVersion = '0';
7
+
8
+ /** Decode connectivity trigger parameters
9
+
10
+ {
11
+ parameters: <Encoded Parameters Hex String>
12
+ }
13
+
14
+ @throws
15
+ <Error>
16
+
17
+ @returns
18
+ {
19
+ id: <Node Id Hex String>
20
+ }
21
+ */
22
+ module.exports = ({parameters}) => {
23
+ if (!parameters) {
24
+ throw new Error('ExpectedEncodedParametersToDecodeConnectivityParameters');
25
+ }
26
+
27
+ const {records} = decodeTlvStream({encoded: parameters});
28
+
29
+ // Check the parameters version
30
+ if (!!findRecord(records, typeVersion)) {
31
+ throw new Error('UnexpectedVersionForEncodedConnectivityTrigger');
32
+ }
33
+
34
+ const idRecord = findRecord(records, typeId);
35
+
36
+ if (!idRecord) {
37
+ throw new Error('ExpectedNodePublicKeyForEncodedConnectivityTrigger');
38
+ }
39
+
40
+ if (!isPublicKey(idRecord.value)) {
41
+ throw new Error('ExpectedValidNodePublicKeyForEncodedConnectivityTrigger');
42
+ }
43
+
44
+ return {id: idRecord.value};
45
+ };
@@ -0,0 +1,45 @@
1
+ const {decodeTlvStream} = require('bolt01');
2
+
3
+ const findRecord = (records, type) => records.find(n => n.type === type);
4
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
5
+ const typeId = '1';
6
+ const typeVersion = '0';
7
+
8
+ /** Decode follow trigger parameters
9
+
10
+ {
11
+ parameters: <Encoded Parameters Hex String>
12
+ }
13
+
14
+ @throws
15
+ <Error>
16
+
17
+ @returns
18
+ {
19
+ id: <Node Id Hex String>
20
+ }
21
+ */
22
+ module.exports = ({parameters}) => {
23
+ if (!parameters) {
24
+ throw new Error('ExpectedEncodedParametersToDecodeFollowParameters');
25
+ }
26
+
27
+ const {records} = decodeTlvStream({encoded: parameters});
28
+
29
+ // Check the parameters version
30
+ if (!!findRecord(records, typeVersion)) {
31
+ throw new Error('UnexpectedVersionForEncodedTrigger');
32
+ }
33
+
34
+ const idRecord = findRecord(records, typeId);
35
+
36
+ if (!idRecord) {
37
+ throw new Error('ExpectedNodePublicKeyForEncodedTrigger');
38
+ }
39
+
40
+ if (!isPublicKey(idRecord.value)) {
41
+ throw new Error('ExpectedValidNodePublicKeyForEncodedTrigger');
42
+ }
43
+
44
+ return {id: idRecord.value};
45
+ };
@@ -0,0 +1,81 @@
1
+ const {decodeTlvStream} = require('bolt01');
2
+
3
+ const decodeConnectivityParams = require('./decode_connectivity_params');
4
+ const decodeFollowParams = require('./decode_follow_params');
5
+
6
+ const base64AsHex = base64 => Buffer.from(base64, 'base64').toString('hex');
7
+ const defaultMethodRecord = {value: '00'};
8
+ const defaultVersionRecord = {value: '00'};
9
+ const findRecord = (records, type) => records.find(n => n.type === type);
10
+ const knownVersions = ['00', '01'];
11
+ const methodConnectivity = '01';
12
+ const methodFollow = '00';
13
+ const triggerPrefix = 'bos-trigger:';
14
+ const typeMethod = '1';
15
+ const typeParams = '2';
16
+ const typeVersion = '0';
17
+
18
+ /** Decode an encoded trigger
19
+
20
+ {
21
+ encoded: <Encoded Trigger String>
22
+ }
23
+
24
+ @throws <Error>
25
+
26
+ @returns
27
+ {
28
+ [connectivity]: {
29
+ id: <Node Id Hex String>
30
+ }
31
+ [follow]: {
32
+ id: <Node Id Hex String>
33
+ }
34
+ }
35
+ */
36
+ module.exports = ({encoded}) => {
37
+ if (!encoded) {
38
+ throw new Error('ExpectedEncodedTriggerToDecode');
39
+ }
40
+
41
+ if (!encoded.startsWith(triggerPrefix)) {
42
+ throw new Error('ExpectedTriggerPrefixForEncodedPrefix');
43
+ }
44
+
45
+ const data = base64AsHex(encoded.slice(triggerPrefix.length));
46
+
47
+ const {records} = decodeTlvStream({encoded: data});
48
+
49
+ const version = findRecord(records, typeVersion) || defaultVersionRecord;
50
+
51
+ // Check the trigger version
52
+ if (!knownVersions.includes(version.value)) {
53
+ throw new Error('UnexpectedVersionForEncodedTrigger');
54
+ }
55
+
56
+ const methodRecord = findRecord(records, typeMethod) || defaultMethodRecord;
57
+
58
+ // Trigger parameters are encoded into a stream record
59
+ const parametersRecord = findRecord(records, typeParams);
60
+
61
+ if (!parametersRecord) {
62
+ throw new Error('ExpectedParametersForTrigger');
63
+ }
64
+
65
+ const parameters = parametersRecord.value;
66
+
67
+ switch (methodRecord.value) {
68
+ case methodConnectivity:
69
+ const connectivity = decodeConnectivityParams({parameters});
70
+
71
+ return {connectivity};
72
+
73
+ case methodFollow:
74
+ const follow = decodeFollowParams({parameters});
75
+
76
+ return {follow};
77
+
78
+ default:
79
+ throw new Error('UnrecognizedMethodTypeForTrigger');
80
+ }
81
+ };
@@ -0,0 +1,29 @@
1
+ const {encodeTlvStream} = require('bolt01');
2
+
3
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
4
+ const typeNodeId = '1';
5
+
6
+ /** Encode the connectivity with node params
7
+
8
+ [0]: <Version>
9
+ 1: <Node Id>
10
+
11
+ {
12
+ id: <Node Identity Public Key Hex String>
13
+ }
14
+
15
+ @throws
16
+ <Error>
17
+
18
+ @returns
19
+ {
20
+ encoded: <Trigger Parameters Hex String>
21
+ }
22
+ */
23
+ module.exports = ({id}) => {
24
+ if (!isPublicKey(id)) {
25
+ throw new Error('ExpectedPublicKeyToEncodeConnectivityParams');
26
+ }
27
+
28
+ return encodeTlvStream({records: [{type: typeNodeId, value: id}]});
29
+ };
@@ -0,0 +1,29 @@
1
+ const {encodeTlvStream} = require('bolt01');
2
+
3
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
4
+ const typeNodeId = '1';
5
+
6
+ /** Encode the follow node params
7
+
8
+ [0]: <Version>
9
+ 1: <Node Id>
10
+
11
+ {
12
+ id: <Node Identity Public Key Hex String>
13
+ }
14
+
15
+ @throws
16
+ <Error>
17
+
18
+ @returns
19
+ {
20
+ encoded: <Trigger Parameters Hex String>
21
+ }
22
+ */
23
+ module.exports = ({id}) => {
24
+ if (!isPublicKey(id)) {
25
+ throw new Error('ExpectedPublicKeyToEncodeFollowParams');
26
+ }
27
+
28
+ return encodeTlvStream({records: [{type: typeNodeId, value: id}]});
29
+ };
@@ -0,0 +1,73 @@
1
+ const {encodeTlvStream} = require('bolt01');
2
+
3
+ const encodeConnectivityParams = require('./encode_connectivity_params');
4
+ const encodeFollowParams = require('./encode_follow_params');
5
+
6
+ const hexAsBase64 = hex => Buffer.from(hex, 'hex').toString('base64');
7
+ const methodConnectivity = '01';
8
+ const triggerPrefix = 'bos-trigger:';
9
+ const typeTriggerMethod = '1';
10
+ const typeTriggerParameters = '2';
11
+ const typeVersion = '0';
12
+ const version = '01';
13
+
14
+ /** Encode a trigger
15
+
16
+ [0]: <Version>
17
+ [1]: <Method>
18
+ [2]: <Parameters>
19
+
20
+ {
21
+ [connectivity]: {
22
+ id: <Node Id Hex String>
23
+ }
24
+ [follow]: {
25
+ id: <Node Id Hex String>
26
+ }
27
+ }
28
+
29
+ @throws
30
+ <Error>
31
+
32
+ @returns
33
+ {
34
+ encoded: <Encoded Trigger String>
35
+ }
36
+ */
37
+ module.exports = ({connectivity, follow}) => {
38
+ if (!connectivity && !follow) {
39
+ throw new Error('ExpectedConnectivityOrFollowDetailsToEncodeTrigger');
40
+ }
41
+
42
+ if (!!connectivity) {
43
+ // Encode the trigger parameters for a connectivity trigger
44
+ const {encoded} = encodeTlvStream({
45
+ records: [
46
+ {
47
+ type: typeTriggerMethod,
48
+ value: methodConnectivity,
49
+ },
50
+ {
51
+ type: typeTriggerParameters,
52
+ value: encodeConnectivityParams({id: connectivity.id}).encoded,
53
+ },
54
+ {
55
+ type: typeVersion,
56
+ value: version,
57
+ },
58
+ ],
59
+ });
60
+
61
+ return {encoded: `${triggerPrefix}${hexAsBase64(encoded)}`};
62
+ }
63
+
64
+ // Encode the trigger parameters for a follow trigger
65
+ const {encoded} = encodeTlvStream({
66
+ records: [{
67
+ type: typeTriggerParameters,
68
+ value: encodeFollowParams({id: follow.id}).encoded,
69
+ }],
70
+ });
71
+
72
+ return {encoded: `${triggerPrefix}${hexAsBase64(encoded)}`};
73
+ };
@@ -0,0 +1,93 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncUntil = require('async/until');
3
+ const {getInvoices} = require('ln-service');
4
+ const {returnResult} = require('asyncjs-util');
5
+
6
+ const decodeTrigger = require('./decode_trigger');
7
+
8
+ const defaultInvoicesLimit = 100;
9
+
10
+ /** Get registered triggers
11
+
12
+ {
13
+ lnd: <Authenticated LND API Object>
14
+ }
15
+
16
+ @returns via cbk or Promise
17
+ {
18
+ triggers: [{
19
+ [connectivity]: {
20
+ id: <Node Identity Public Key Hex String>
21
+ }
22
+ [follow]: {
23
+ id: <Node Identity Public Key Hex String>
24
+ }
25
+ id: <Trigger Id Hex String>
26
+ }]
27
+ }
28
+ */
29
+ module.exports = ({lnd}, cbk) => {
30
+ return new Promise((resolve, reject) => {
31
+ return asyncAuto({
32
+ // Check arguments
33
+ validate: cbk => {
34
+ if (!lnd) {
35
+ return cbk([400, 'ExpectedAuthenticatedLndToGetTriggers']);
36
+ }
37
+
38
+ return cbk();
39
+ },
40
+
41
+ // Get the past triggers
42
+ getTriggers: ['validate', ({}, cbk) => {
43
+ let token;
44
+ const triggers = [];
45
+
46
+ // Register past trigger invoices
47
+ return asyncUntil(
48
+ cbk => cbk(null, token === false),
49
+ cbk => {
50
+ return getInvoices({
51
+ lnd,
52
+ token,
53
+ is_unconfirmed: true,
54
+ limit: !token ? defaultInvoicesLimit : undefined,
55
+ },
56
+ (err, res) => {
57
+ if (!!err) {
58
+ return cbk(err);
59
+ }
60
+
61
+ token = res.next || false;
62
+
63
+ res.invoices.forEach(({description, id}) => {
64
+ try {
65
+ const trigger = decodeTrigger({encoded: description});
66
+
67
+ return triggers.push({
68
+ id,
69
+ connectivity: trigger.connectivity,
70
+ follow: trigger.follow,
71
+ });
72
+ } catch (err) {
73
+ // Ignore invoices that are not triggers
74
+ return;
75
+ }
76
+ });
77
+
78
+ return cbk();
79
+ });
80
+ },
81
+ err => {
82
+ if (!!err) {
83
+ return cbk(err);
84
+ }
85
+
86
+ return cbk(null, triggers);
87
+ },
88
+ );
89
+ }],
90
+ },
91
+ returnResult({reject, resolve, of: 'getTriggers'}, cbk));
92
+ });
93
+ };
@@ -0,0 +1,3 @@
1
+ const manageTriggers = require('./manage_triggers');
2
+
3
+ module.exports = {manageTriggers};
@@ -0,0 +1,243 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {cancelHodlInvoice} = require('ln-service');
3
+ const {returnResult} = require('asyncjs-util');
4
+
5
+ const createConnectivityTrigger = require('./create_connectivity_trigger');
6
+ const createFollowNodeTrigger = require('./create_follow_node_trigger');
7
+ const getTriggers = require('./get_triggers');
8
+ const subscribeToTriggers = require('./subscribe_to_triggers');
9
+
10
+ const actionAddConnectivityTrigger = 'action-add-connectivity-trigger';
11
+ const actionAddFollowTrigger = 'action-add-follow-trigger';
12
+ const actionDeleteTrigger = 'action-delete-trigger';
13
+ const actionListTriggers = 'action-list-triggers';
14
+ const actionSubscribeToTriggers = 'action-subscribe-to-triggers';
15
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
16
+
17
+ /** Manage trigger actions
18
+
19
+ {
20
+ ask: <Ask Function>
21
+ lnd: <Authenticated LND API Object>
22
+ logger: <Winston Logger Object>
23
+ }
24
+
25
+ @returns via cbk or Promise
26
+ */
27
+ module.exports = ({ask, lnd, logger}, cbk) => {
28
+ return new Promise((resolve, reject) => {
29
+ return asyncAuto({
30
+ // Check arguments
31
+ validate: cbk => {
32
+ if (!ask) {
33
+ return cbk([400, 'ExpectedAskFunctionToManageTriggers']);
34
+ }
35
+
36
+ if (!lnd) {
37
+ return cbk([400, 'ExpectedAuthenticatedLndToManageTriggers']);
38
+ }
39
+
40
+ if (!logger) {
41
+ return cbk([400, 'ExpectedWinstonLoggerToManageTriggers']);
42
+ }
43
+
44
+ return cbk();
45
+ },
46
+
47
+ // Select trigger action
48
+ selectAction: ['validate', ({}, cbk) => {
49
+ return ask({
50
+ choices: [
51
+ {
52
+ name: 'Add Node Connectivity Trigger',
53
+ value: actionAddConnectivityTrigger,
54
+ },
55
+ {
56
+ name: 'Add Follow Node Trigger',
57
+ value: actionAddFollowTrigger,
58
+ },
59
+ {
60
+ name: 'View Triggers',
61
+ value: actionListTriggers,
62
+ },
63
+ {
64
+ name: 'Subscribe to Triggers',
65
+ value: actionSubscribeToTriggers,
66
+ },
67
+ ],
68
+ message: 'Trigger action?',
69
+ name: 'action',
70
+ type: 'list',
71
+ },
72
+ ({action}) => cbk(null, action));
73
+ }],
74
+
75
+ // Ask for details about a new connectivity trigger
76
+ askForConnectivityTrigger: ['selectAction', ({selectAction}, cbk) => {
77
+ // Exit early when not adding a trigger
78
+ if (selectAction !== actionAddConnectivityTrigger) {
79
+ return cbk();
80
+ }
81
+
82
+ return ask({
83
+ message: 'Node public key to watch connectivity with?',
84
+ name: 'id',
85
+ type: 'input',
86
+ validate: input => {
87
+ if (!input) {
88
+ return false;
89
+ }
90
+
91
+ if (!isPublicKey(input)) {
92
+ return 'Enter a node identity public key';
93
+ }
94
+
95
+ return true;
96
+ },
97
+ },
98
+ ({id}) => cbk(null, id));
99
+ }],
100
+
101
+ // Ask for details about a new follow trigger
102
+ askForFollowTrigger: ['selectAction', ({selectAction}, cbk) => {
103
+ // Exit early when not adding a trigger
104
+ if (selectAction !== actionAddFollowTrigger) {
105
+ return cbk();
106
+ }
107
+
108
+ return ask({
109
+ message: 'Node public key to follow?',
110
+ name: 'id',
111
+ type: 'input',
112
+ validate: input => {
113
+ if (!input) {
114
+ return false;
115
+ }
116
+
117
+ if (!isPublicKey(input)) {
118
+ return 'Enter a node identity public key to follow';
119
+ }
120
+
121
+ return true;
122
+ },
123
+ },
124
+ ({id}) => cbk(null, id));
125
+ }],
126
+
127
+ // Get the list of triggers
128
+ getTriggers: ['selectAction', ({selectAction}, cbk) => {
129
+ // Exit early when not listing triggers
130
+ if (selectAction !== actionListTriggers) {
131
+ return cbk();
132
+ }
133
+
134
+ logger.info({finding_triggers: true});
135
+
136
+ return getTriggers({lnd}, cbk);
137
+ }],
138
+
139
+ // Subscribe to triggers
140
+ subscribeToTriggers: ['selectAction', ({selectAction}, cbk) => {
141
+ // Exit early when not subscribing
142
+ if (selectAction !== actionSubscribeToTriggers) {
143
+ return cbk();
144
+ }
145
+
146
+ const sub = subscribeToTriggers({lnds: [lnd]});
147
+
148
+ sub.on('channel_opened', opened => logger.info({opened}));
149
+ sub.on('peer_connected', connected => logger.info({connected}));
150
+ sub.on('peer_disconnected', disconnect => logger.info({disconnect}));
151
+ sub.on('error', err => cbk(err));
152
+
153
+ return logger.info({listening_for_trigger_events: true});
154
+ }],
155
+
156
+ // Create a new connectivity trigger
157
+ createConnectivityTrigger: [
158
+ 'askForConnectivityTrigger',
159
+ ({askForConnectivityTrigger}, cbk) =>
160
+ {
161
+ if (!askForConnectivityTrigger) {
162
+ return cbk();
163
+ }
164
+
165
+ return createConnectivityTrigger({
166
+ lnd,
167
+ id: askForConnectivityTrigger,
168
+ },
169
+ cbk);
170
+ }],
171
+
172
+ // Create a new follow trigger
173
+ createFollowTrigger: [
174
+ 'askForFollowTrigger',
175
+ ({askForFollowTrigger}, cbk) =>
176
+ {
177
+ if (!askForFollowTrigger) {
178
+ return cbk();
179
+ }
180
+
181
+ return createFollowNodeTrigger({lnd, id: askForFollowTrigger}, cbk);
182
+ }],
183
+
184
+ // Select a trigger from the list
185
+ selectTrigger: ['getTriggers', ({getTriggers}, cbk) => {
186
+ if (!getTriggers) {
187
+ return cbk();
188
+ }
189
+
190
+ if (!getTriggers.length) {
191
+ return cbk([404, 'NoTriggersFound']);
192
+ }
193
+
194
+ return ask({
195
+ choices: getTriggers.map(({connectivity, follow, id}) => {
196
+ if (!!connectivity) {
197
+ return {
198
+ name: `Connectivity with ${connectivity.id}`,
199
+ value: id,
200
+ };
201
+ } else {
202
+ return {
203
+ name: `Following ${follow.id}`,
204
+ value: id,
205
+ };
206
+ }
207
+ }),
208
+ message: 'Triggers:',
209
+ name: 'view',
210
+ type: 'list',
211
+ },
212
+ ({view}) => cbk(null, view));
213
+ }],
214
+
215
+ // Trigger actions
216
+ triggerAction: ['selectTrigger', ({selectTrigger}, cbk) => {
217
+ // Exit early when no trigger is selected to take actions against
218
+ if (!selectTrigger) {
219
+ return cbk();
220
+ }
221
+
222
+ return ask({
223
+ choices: [{name: 'Delete Trigger', value: actionDeleteTrigger}],
224
+ message: 'Action?',
225
+ name: 'modify',
226
+ type: 'list',
227
+ },
228
+ ({modify}) => cbk(null, selectTrigger));
229
+ }],
230
+
231
+ // Delete a trigger
232
+ deleteTrigger: ['triggerAction', ({triggerAction}, cbk) => {
233
+ // Exit early when not deleting a triger
234
+ if (!triggerAction) {
235
+ return cbk();
236
+ }
237
+
238
+ return cancelHodlInvoice({lnd, id: triggerAction}, cbk);
239
+ }],
240
+ },
241
+ returnResult({reject, resolve}, cbk));
242
+ });
243
+ };
@@ -0,0 +1,232 @@
1
+ const EventEmitter = require('events');
2
+
3
+ const asyncUntil = require('async/until');
4
+ const {decodeChanId} = require('bolt07');
5
+ const {getHeight} = require('ln-service');
6
+ const {getInvoices} = require('ln-service');
7
+ const {subscribeToGraph} = require('ln-service');
8
+ const {subscribeToInvoice} = require('ln-service');
9
+ const {subscribeToInvoices} = require('ln-service');
10
+ const {subscribeToPeers} = require('ln-service');
11
+
12
+ const decodeTrigger = require('./decode_trigger');
13
+
14
+ const defaultInvoicesLimit = 100;
15
+ const {keys} = Object;
16
+
17
+ /** Subscribe to trigger events
18
+
19
+ {
20
+ lnds: <Authenticated LND API Object>
21
+ }
22
+
23
+ @event 'channel_opened'
24
+ {
25
+ [capacity]: <Channel Token Capacity Number>
26
+ id: <Standard Format Channel Id String>
27
+ public_keys: [<Announcing Public Key>, <Target Public Key String>]
28
+ }
29
+
30
+ @event 'peer_connected'
31
+ {
32
+ public_key: <Node Identity Public Key Hex String>
33
+ }
34
+
35
+ @event 'peer_disconnected'
36
+ {
37
+ public_key: <Node Identity Public Key Hex String>
38
+ }
39
+
40
+ @returns
41
+ <Event Emitter Object>
42
+ */
43
+ module.exports = ({lnds}) => {
44
+ const channels = new Set();
45
+ const emitter = new EventEmitter();
46
+ const subs = [];
47
+ const triggers = {};
48
+
49
+ // Stop subscription when listeners are removed
50
+ emitter.on('removeListener', () => {
51
+ if (!!emitter.listenerCount('channel_opened')) {
52
+ return;
53
+ }
54
+
55
+ return cbk([400, 'RemovedAllListeners']);
56
+ });
57
+
58
+ // Clean up when there is an error
59
+ const errored = err => {
60
+ subs.forEach(n => n.removeAllListeners());
61
+
62
+ if (!emitter.listenerCount('error')) {
63
+ return;
64
+ }
65
+
66
+ return emitter.emit('error', err);
67
+ };
68
+
69
+ // Register trigger if present
70
+ const register = ({description, id}, lnd) => {
71
+ try {
72
+ decodeTrigger({encoded: description});
73
+ } catch (err) {
74
+ // Exit early when the invoice is not a trigger invoice
75
+ return;
76
+ }
77
+
78
+ triggers[id] = decodeTrigger({encoded: description});
79
+
80
+ // Listen for the trigger invoice to be canceled to stop it
81
+ const sub = subscribeToInvoice({id, lnd});
82
+
83
+ subs.push(sub);
84
+
85
+ // Listen for an error on the invoice
86
+ sub.on('error', err => errored(err));
87
+
88
+ // Listen for the trigger to get canceled
89
+ sub.on('invoice_updated', invoice => {
90
+ if (!invoice.is_canceled) {
91
+ return;
92
+ }
93
+
94
+ delete triggers[invoice.id];
95
+ });
96
+
97
+ return;
98
+ };
99
+
100
+ lnds.forEach(lnd => {
101
+ const graphSub = subscribeToGraph({lnd});
102
+ const invoicesSub = subscribeToInvoices({lnd});
103
+ const peersSub = subscribeToPeers({lnd});
104
+ let startHeight;
105
+ let token;
106
+
107
+ subs.push(graphSub);
108
+ subs.push(invoicesSub);
109
+ subs.push(peersSub);
110
+
111
+ getHeight({lnd}, (err, res) => {
112
+ if (!!err) {
113
+ return errored(err);
114
+ }
115
+
116
+ return startHeight = res.current_block_height;
117
+ });
118
+
119
+ // Listen for errors on the invoices subscription
120
+ invoicesSub.on('error', err => errored(err));
121
+
122
+ // Listen for new trigger invoices
123
+ invoicesSub.on('invoice_updated', updated => register(updated, lnd));
124
+
125
+ // Listen for errors on the graph subscription
126
+ graphSub.on('error', err => errored(err));
127
+
128
+ // Listen for updates to a channel that may match a trigger
129
+ graphSub.on('channel_updated', (update, cbk) => {
130
+ // Exit early when start height is not known yet
131
+ if (!startHeight) {
132
+ return;
133
+ }
134
+
135
+ // Exit early when the channel exists in the set
136
+ if (channels.has(update.id)) {
137
+ return;
138
+ }
139
+
140
+ // See if the channel matches a relevant trigger
141
+ const follows = keys(triggers)
142
+ .filter(id => !!triggers[id].follow)
143
+ .filter(id => update.public_keys.includes(triggers[id].follow.id));
144
+
145
+ // Exit early when this channel doesn't match any follow trigger
146
+ if (!follows.length) {
147
+ return;
148
+ }
149
+
150
+ const height = decodeChanId({channel: update.id}).block_height;
151
+
152
+ // Exit early when the channel id is less than the start height
153
+ if (height < startHeight) {
154
+ return;
155
+ }
156
+
157
+ // Mark new channel as announced
158
+ channels.add(update.id);
159
+
160
+ // This is a new channel that confirmed after the start height
161
+ return emitter.emit('channel_opened', {
162
+ capacity: update.capacity,
163
+ id: update.id,
164
+ public_keys: update.public_keys,
165
+ });
166
+ });
167
+
168
+ // Listen for errors on the peers subscription
169
+ peersSub.on('error', err => errored(err));
170
+
171
+ // Listen for connected peers subscription
172
+ peersSub.on('connected', (update, cbk) => {
173
+ const id = update.public_key;
174
+
175
+ // See if the peer matches a relevant trigger
176
+ const follows = keys(triggers)
177
+ .filter(id => !!triggers[id].connectivity)
178
+ .filter(id => update.public_key === triggers[id].connectivity.id);
179
+
180
+ // Exit early when this peer doesn't match any connectivity trigger
181
+ if (!follows.length) {
182
+ return;
183
+ }
184
+
185
+ return emitter.emit('peer_connected', update);
186
+ });
187
+
188
+ // Listen for disconnected peers subscription
189
+ peersSub.on('disconnected', (update, cbk) => {
190
+ const id = update.public_key;
191
+
192
+ // See if the peer matches a relevant trigger
193
+ const follows = keys(triggers)
194
+ .filter(id => !!triggers[id].connectivity)
195
+ .filter(id => update.public_key === triggers[id].connectivity.id);
196
+
197
+ // Exit early when this peer doesn't match any connectivity trigger
198
+ if (!follows.length) {
199
+ return;
200
+ }
201
+
202
+ return emitter.emit('peer_disconnected', update);
203
+ });
204
+
205
+ // Register past trigger invoices
206
+ asyncUntil(
207
+ cbk => cbk(null, token === false),
208
+ cbk => {
209
+ return getInvoices({
210
+ lnd,
211
+ token,
212
+ is_unconfirmed: true,
213
+ limit: !token ? defaultInvoicesLimit : undefined,
214
+ },
215
+ (err, res) => {
216
+ if (!!err) {
217
+ return cbk(err);
218
+ }
219
+
220
+ token = res.next || false;
221
+
222
+ res.invoices.forEach(invoice => register(invoice, lnd));
223
+
224
+ return cbk();
225
+ });
226
+ },
227
+ err => !!err ? errored(err) : null,
228
+ );
229
+ });
230
+
231
+ return emitter;
232
+ };