balanceofsatoshis 12.0.2 → 12.3.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 +12 -0
- package/bos +24 -1
- package/commands/constants.json +3 -1
- package/lnurl/auth.js +178 -0
- package/lnurl/der_encode_signature.js +37 -0
- package/lnurl/index.js +2 -2
- package/lnurl/manage_lnurl.js +127 -0
- package/lnurl/pay.js +5 -5
- package/lnurl/sign_auth_challenge.js +46 -0
- package/lnurl/withdraw.js +226 -0
- package/network/probe_destination.js +1 -1
- package/package.json +8 -6
- package/services/get_balanced_opens.js +1 -1
- package/services/respond_to_key_send_ping.js +1 -1
- package/swaps/get_paid_service.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## 12.3.0
|
|
4
|
+
|
|
5
|
+
- `lnurl`: Add support for the `auth` function to authenticate with a node key
|
|
6
|
+
|
|
7
|
+
## 12.2.0
|
|
8
|
+
|
|
9
|
+
- `swap`: Add utility command for testing submarine swaps
|
|
10
|
+
|
|
11
|
+
## 12.1.0
|
|
12
|
+
|
|
13
|
+
- `lnurl`: Add support for the `withdraw` function to send a payment request
|
|
14
|
+
|
|
3
15
|
## 12.0.2
|
|
4
16
|
|
|
5
17
|
- `open-balanced-channel`: Fix display issue where `undefined` was printed
|
package/bos
CHANGED
|
@@ -1023,6 +1023,9 @@ prog
|
|
|
1023
1023
|
.command('lnurl', 'Collection of lnurl features')
|
|
1024
1024
|
.argument('<function>', 'lnurl function', keys(lnurlFunctions))
|
|
1025
1025
|
.help(`Functions: ${keys(lnurlFunctions).join(', ')}`)
|
|
1026
|
+
.help('lnurl auth will request authorization')
|
|
1027
|
+
.help('lnurl pay will request a payment request from a service')
|
|
1028
|
+
.help('lnurl withdraw will create an invoice and send it to a service')
|
|
1026
1029
|
.option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
|
|
1027
1030
|
.option('--max-fee <max_fee>', 'Maximum fee to pay', INT, 1337)
|
|
1028
1031
|
.option('--max-paths <max_paths>', 'Maximum paths to use', INT, 1)
|
|
@@ -1032,7 +1035,7 @@ prog
|
|
|
1032
1035
|
.action((args, options, logger) => {
|
|
1033
1036
|
return new Promise(async (resolve, reject) => {
|
|
1034
1037
|
try {
|
|
1035
|
-
return lnurl.
|
|
1038
|
+
return lnurl.manageLnurl({
|
|
1036
1039
|
logger,
|
|
1037
1040
|
ask: (n, cbk) => inquirer.prompt(n).then(n => cbk(n)),
|
|
1038
1041
|
avoid: flatten([options.avoid].filter(n => !!n)),
|
|
@@ -1624,6 +1627,26 @@ prog
|
|
|
1624
1627
|
});
|
|
1625
1628
|
})
|
|
1626
1629
|
|
|
1630
|
+
// Submarine swap
|
|
1631
|
+
.command('swap', 'Trade off-chain coins for on-chain via submarine swap')
|
|
1632
|
+
.visible(false)
|
|
1633
|
+
.option('--node <node_name>', 'Node to use for swap')
|
|
1634
|
+
.action((args, options, logger) => {
|
|
1635
|
+
return new Promise(async (resolve, reject) => {
|
|
1636
|
+
try {
|
|
1637
|
+
return paidServices.manageSwap({
|
|
1638
|
+
logger,
|
|
1639
|
+
ask: (n, cbk) => inquirer.prompt([n]).then(res => cbk(res)),
|
|
1640
|
+
lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
|
|
1641
|
+
request: commands.simpleRequest,
|
|
1642
|
+
},
|
|
1643
|
+
responses.returnObject({exit, logger, reject, resolve}));
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
return logger.error({err}) && reject();
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
})
|
|
1649
|
+
|
|
1627
1650
|
// Create submarine swap to convert on-chain funds to off-chain
|
|
1628
1651
|
.command('swap-in', 'Trade on-chain coins for off-chain via submarine swap')
|
|
1629
1652
|
.argument('[amount]', 'Amount to receive', INT, 1e6)
|
package/commands/constants.json
CHANGED
package/lnurl/auth.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {bech32} = require('bech32');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
const {signMessage} = require('ln-service');
|
|
5
|
+
const tinysecp = require('tiny-secp256k1');
|
|
6
|
+
|
|
7
|
+
const signAuthChallenge = require('./sign_auth_challenge');
|
|
8
|
+
|
|
9
|
+
const actionKey = 'action';
|
|
10
|
+
const {decode} = bech32;
|
|
11
|
+
const defaultAction = 'authenticate';
|
|
12
|
+
const asLnurl = n => n.substring(n.startsWith('lightning:') ? 10 : 0);
|
|
13
|
+
const bech32CharLimit = 2000;
|
|
14
|
+
const challengeKey = 'k1';
|
|
15
|
+
const errorStatus = 'ERROR';
|
|
16
|
+
const knownActions = ['auth', 'link', 'login', 'register'];
|
|
17
|
+
const okStatus = 'OK';
|
|
18
|
+
const prefix = 'lnurl';
|
|
19
|
+
const tlsProtocol = 'https:';
|
|
20
|
+
const lud13AuthPhrase = 'DO NOT EVER SIGN THIS TEXT WITH YOUR PRIVATE KEYS! IT IS ONLY USED FOR DERIVATION OF LNURL-AUTH HASHING-KEY, DISCLOSING ITS SIGNATURE WILL COMPROMISE YOUR LNURL-AUTH IDENTITY AND MAY LEAD TO LOSS OF FUNDS!';
|
|
21
|
+
const wordsAsUtf8 = n => Buffer.from(bech32.fromWords(n)).toString('utf8');
|
|
22
|
+
|
|
23
|
+
/** Authenticate using lnurl
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
ask: <Ask Function>
|
|
27
|
+
request: <Request Function>
|
|
28
|
+
lnd: <Authenticated LND API Object>
|
|
29
|
+
lnurl: <Lnurl String>
|
|
30
|
+
logger: <Winston Logger Object>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@returns via cbk or Promise
|
|
34
|
+
*/
|
|
35
|
+
module.exports = (args, cbk) => {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
return asyncAuto({
|
|
38
|
+
// Import the ECPair library
|
|
39
|
+
ecp: async () => (await import('ecpair')).ECPairFactory(tinysecp),
|
|
40
|
+
|
|
41
|
+
// Check arguments
|
|
42
|
+
validate: cbk => {
|
|
43
|
+
if (!args.ask) {
|
|
44
|
+
return cbk([400, 'ExpectedAskFunctionToAuthenticateUsingLnurl']);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!args.lnurl) {
|
|
48
|
+
return cbk([400, 'ExpectedUrlToAuthenticateToLnurl']);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
decode(asLnurl(args.lnurl), bech32CharLimit);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return cbk([400, 'FailedToDecodeLnurlToAuthenticate', {err}]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (decode(asLnurl(args.lnurl), bech32CharLimit).prefix !== prefix) {
|
|
58
|
+
return cbk([400, 'ExpectedLnUrlPrefixToAuthenticate']);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!args.lnd) {
|
|
62
|
+
return cbk([400, 'ExpectedLndToAuthenticateUsingLnurl']);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!args.logger) {
|
|
66
|
+
return cbk([400, 'ExpectedLoggerToAuthenticateUsingLnurl']);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!args.request) {
|
|
70
|
+
return cbk([400, 'ExpectedRequestFunctionToGetLnurlAuthentication']);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return cbk();
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// Parse the encoded Lnurl
|
|
77
|
+
parse: ['validate', ({}, cbk) => {
|
|
78
|
+
const {words} = decode(asLnurl(args.lnurl), bech32CharLimit);
|
|
79
|
+
|
|
80
|
+
const url = wordsAsUtf8(words);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
new URL(url);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return cbk([400, 'ExpectedValidCallbackUrlInDecodedLnurlForAuth']);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const {hostname, protocol, searchParams} = new URL(url);
|
|
89
|
+
|
|
90
|
+
if (protocol !== tlsProtocol) {
|
|
91
|
+
return cbk([501, 'UnsupportedUrlProtocolForLnurlAuthentication']);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const action = searchParams.get(actionKey);
|
|
95
|
+
|
|
96
|
+
if (!!action && !knownActions.includes(action)) {
|
|
97
|
+
return cbk([503, 'UnknownAuthenticationActionForLnurlAuth']);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const k1 = searchParams.get(challengeKey);
|
|
101
|
+
|
|
102
|
+
if (!k1) {
|
|
103
|
+
return cbk([503, 'ExpectedChallengeK1ValueInDecodedLnurlForAuth']);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return cbk(null, {hostname, k1, url, action: action || defaultAction});
|
|
107
|
+
}],
|
|
108
|
+
|
|
109
|
+
// Sign the canonical phrase for LUD-13 signMessage based seed generation
|
|
110
|
+
seed: ['parse', ({}, cbk) => {
|
|
111
|
+
return signMessage({lnd: args.lnd, message: lud13AuthPhrase}, cbk);
|
|
112
|
+
}],
|
|
113
|
+
|
|
114
|
+
// Derive keys and get signatures
|
|
115
|
+
sign: ['ecp', 'parse', 'seed', ({ecp, parse, seed}, cbk) => {
|
|
116
|
+
const sign = signAuthChallenge({
|
|
117
|
+
ecp,
|
|
118
|
+
hostname: parse.hostname,
|
|
119
|
+
k1: parse.k1,
|
|
120
|
+
seed: seed.signature,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return cbk(null, {
|
|
124
|
+
public_key: sign.public_key,
|
|
125
|
+
signature: sign.signature,
|
|
126
|
+
});
|
|
127
|
+
}],
|
|
128
|
+
|
|
129
|
+
// Display confirmation dialog with domain name and action
|
|
130
|
+
ok: ['parse', 'sign', ({parse, sign}, cbk) => {
|
|
131
|
+
return args.ask({
|
|
132
|
+
default: true,
|
|
133
|
+
message: `Do you want to ${parse.action} with ${parse.hostname}?`,
|
|
134
|
+
name: 'ok',
|
|
135
|
+
type: 'confirm',
|
|
136
|
+
},
|
|
137
|
+
({ok}) => cbk(null, ok));
|
|
138
|
+
}],
|
|
139
|
+
|
|
140
|
+
// Transmit authenticating signature and key to the host
|
|
141
|
+
send: ['ok', 'parse', 'sign', ({ok, parse, sign}, cbk) => {
|
|
142
|
+
if (!ok) {
|
|
143
|
+
return cbk([400, 'AuthenticationCanceled']);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
args.logger.info({sending_authentication: sign.public_key});
|
|
147
|
+
|
|
148
|
+
return args.request({
|
|
149
|
+
json: true,
|
|
150
|
+
qs: {key: sign.public_key, sig: sign.signature},
|
|
151
|
+
url: parse.url,
|
|
152
|
+
},
|
|
153
|
+
(err, r, json) => {
|
|
154
|
+
if (!!err) {
|
|
155
|
+
return cbk([503, 'FailedToGetLnurlAuthenticationData', {err}]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!json) {
|
|
159
|
+
return cbk([503, 'ExpectedJsonReturnedInLnurlResponseForAuth']);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (json.status === errorStatus) {
|
|
163
|
+
return cbk([503, 'LnurlAuthenticationFail', {err: json.reason}]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (json.status !== okStatus) {
|
|
167
|
+
return cbk([503, 'ExpectedOkStatusInLnurlResponseJsonForAuth']);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
args.logger.info({is_authenticated: true});
|
|
171
|
+
|
|
172
|
+
return cbk();
|
|
173
|
+
});
|
|
174
|
+
}],
|
|
175
|
+
},
|
|
176
|
+
returnResult({reject, resolve}, cbk));
|
|
177
|
+
});
|
|
178
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const asDer = n => (n[0]&128)?Buffer.concat([Buffer.alloc(1),n],1+n.length):n;
|
|
2
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
3
|
+
const {concat} = Buffer;
|
|
4
|
+
const decomposeSignature = sig => [sig.slice(0, 32), sig.slice(32, 64)];
|
|
5
|
+
const {from} = Buffer;
|
|
6
|
+
const header = 0x30;
|
|
7
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
8
|
+
const int = 0x02;
|
|
9
|
+
|
|
10
|
+
/** DER encode a signature given r and s values
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
signature: <Signature Buffer Object>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@returns
|
|
17
|
+
{
|
|
18
|
+
encoded: <DER Encoded Signature Buffer Object>
|
|
19
|
+
}
|
|
20
|
+
*/
|
|
21
|
+
module.exports = ({signature}) => {
|
|
22
|
+
// Split the signature for DER encoding
|
|
23
|
+
const [r, s] = decomposeSignature(hexAsBuffer(signature)).map(asDer);
|
|
24
|
+
|
|
25
|
+
const encoded = bufferAsHex(concat([
|
|
26
|
+
from([header]), // Header byte indicating compound structure
|
|
27
|
+
from([r.length + s.length + [int, int, r.length, s.length].length]), // Len
|
|
28
|
+
from([int]), // Integer indicator
|
|
29
|
+
from([r.length]), // Length of data
|
|
30
|
+
r,
|
|
31
|
+
from([int]), // Integer indicator
|
|
32
|
+
from([s.length]), // Length of data
|
|
33
|
+
s,
|
|
34
|
+
]));
|
|
35
|
+
|
|
36
|
+
return {encoded};
|
|
37
|
+
};
|
package/lnurl/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
const
|
|
1
|
+
const manageLnurl = require('./manage_lnurl');
|
|
2
2
|
|
|
3
|
-
module.exports = {
|
|
3
|
+
module.exports = {manageLnurl};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {returnResult} = require('asyncjs-util');
|
|
3
|
+
|
|
4
|
+
const auth = require('./auth');
|
|
5
|
+
const pay = require('./pay');
|
|
6
|
+
const withdraw = require('./withdraw');
|
|
7
|
+
|
|
8
|
+
const functionAuth = 'auth';
|
|
9
|
+
const functionPay = 'pay';
|
|
10
|
+
const functionWithdraw = 'withdraw';
|
|
11
|
+
const {isArray} = Array;
|
|
12
|
+
|
|
13
|
+
/** Manage Lnurl functions
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
ask: <Ask Function>
|
|
17
|
+
avoid: [<Avoid Forwarding Through String>]
|
|
18
|
+
function: <Lnurl Function String>
|
|
19
|
+
request: <Request Function>
|
|
20
|
+
lnd: <Authenticated LND API Object>
|
|
21
|
+
lnurl: <Lnurl String>
|
|
22
|
+
logger: <Winston Logger Object>
|
|
23
|
+
[max_fee]: <Max Fee Tokens Number>
|
|
24
|
+
[max_paths]: <Maximum Paths Number>
|
|
25
|
+
out: [<Out Through Peer With Public Key Hex String>]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@returns via cbk or Promise
|
|
29
|
+
*/
|
|
30
|
+
module.exports = (args, cbk) => {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
return asyncAuto({
|
|
33
|
+
// Check arguments
|
|
34
|
+
validate: cbk => {
|
|
35
|
+
if (!args.ask) {
|
|
36
|
+
return cbk([400, 'ExpectedAskFunctionToManageLnurl']);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!isArray(args.avoid)) {
|
|
40
|
+
return cbk([400, 'ExpectedArrayOfAvoidsToManageLnurl']);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (![functionAuth, functionPay, functionWithdraw].includes(args.function)) {
|
|
44
|
+
return cbk([400, 'ExpectedLnurlFunctionToManageLnurl']);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!args.lnd) {
|
|
48
|
+
return cbk([400, 'ExpectedLndToManageLnurl']);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!args.lnurl) {
|
|
52
|
+
return cbk([400, 'ExpectedUrlStringToManageLnurl']);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!args.logger) {
|
|
56
|
+
return cbk([400, 'ExpectedLoggerToManageLnurl']);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!isArray(args.out)) {
|
|
60
|
+
return cbk([400, 'ExpectedArrayOfOutPeersToManageLnurl']);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!args.request) {
|
|
64
|
+
return cbk([400, 'ExpectedRequestFunctionToManageLnurl']);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return cbk();
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Authenticate using lnurl
|
|
71
|
+
auth: ['validate', ({}, cbk) => {
|
|
72
|
+
// Exit early if not lnurl auth
|
|
73
|
+
if (args.function !== functionAuth) {
|
|
74
|
+
return cbk();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return auth({
|
|
78
|
+
ask: args.ask,
|
|
79
|
+
lnd: args.lnd,
|
|
80
|
+
lnurl: args.lnurl,
|
|
81
|
+
logger: args.logger,
|
|
82
|
+
request: args.request,
|
|
83
|
+
},
|
|
84
|
+
cbk);
|
|
85
|
+
}],
|
|
86
|
+
|
|
87
|
+
// Pay to lnurl
|
|
88
|
+
pay: ['validate', ({}, cbk) => {
|
|
89
|
+
// Exit early if not lnurl pay
|
|
90
|
+
if (args.function !== functionPay) {
|
|
91
|
+
return cbk();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return pay({
|
|
95
|
+
ask: args.ask,
|
|
96
|
+
avoid: args.avoid,
|
|
97
|
+
lnd: args.lnd,
|
|
98
|
+
lnurl: args.lnurl,
|
|
99
|
+
logger: args.logger,
|
|
100
|
+
max_fee: args.max_fee,
|
|
101
|
+
max_paths: args.max_paths,
|
|
102
|
+
out: args.out,
|
|
103
|
+
request: args.request,
|
|
104
|
+
},
|
|
105
|
+
cbk);
|
|
106
|
+
}],
|
|
107
|
+
|
|
108
|
+
// Withdraw from lnurl
|
|
109
|
+
withdraw: ['validate', ({}, cbk) => {
|
|
110
|
+
// Exit early if not lnurl withdraw
|
|
111
|
+
if (args.function !== functionWithdraw) {
|
|
112
|
+
return cbk();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return withdraw({
|
|
116
|
+
ask: args.ask,
|
|
117
|
+
lnd: args.lnd,
|
|
118
|
+
lnurl: args.lnurl,
|
|
119
|
+
logger: args.logger,
|
|
120
|
+
request: args.request,
|
|
121
|
+
},
|
|
122
|
+
cbk);
|
|
123
|
+
}],
|
|
124
|
+
},
|
|
125
|
+
returnResult({reject, resolve}, cbk));
|
|
126
|
+
});
|
|
127
|
+
};
|
package/lnurl/pay.js
CHANGED
|
@@ -36,13 +36,13 @@ const wordsAsUtf8 = n => Buffer.from(bech32.fromWords(n)).toString('utf8');
|
|
|
36
36
|
{
|
|
37
37
|
ask: <Ask Function>
|
|
38
38
|
avoid: [<Avoid Forwarding Through String>]
|
|
39
|
-
fetch: <Fetch Function>
|
|
40
39
|
lnd: <Authenticated LND API Object>
|
|
41
40
|
lnurl: <Lnurl String>
|
|
42
41
|
logger: <Winston Logger Object>
|
|
43
42
|
max_fee: <Max Fee Tokens Number>
|
|
44
43
|
max_paths: <Maximum Paths Number>
|
|
45
44
|
out: [<Out Through Peer With Public Key Hex String>]
|
|
45
|
+
request: <Request Function>
|
|
46
46
|
}
|
|
47
47
|
*/
|
|
48
48
|
module.exports = (args, cbk) => {
|
|
@@ -58,10 +58,6 @@ module.exports = (args, cbk) => {
|
|
|
58
58
|
return cbk([400, 'ExpectedAvoidArrayToGetPaymentRequestFromLnurl']);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
if (!args.request) {
|
|
62
|
-
return cbk([400, 'ExpectedRequestFunctionToGetLnurlData']);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
61
|
if (!args.lnurl) {
|
|
66
62
|
return cbk([400, 'ExpectedUrlToGetPaymentRequestFromLnurl']);
|
|
67
63
|
}
|
|
@@ -96,6 +92,10 @@ module.exports = (args, cbk) => {
|
|
|
96
92
|
return cbk([400, 'ExpectedArrayOfOutPeersToPayViaLnurl']);
|
|
97
93
|
}
|
|
98
94
|
|
|
95
|
+
if (!args.request) {
|
|
96
|
+
return cbk([400, 'ExpectedRequestFunctionToGetLnurlData']);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
99
|
return cbk();
|
|
100
100
|
},
|
|
101
101
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const {createHash} = require('crypto');
|
|
2
|
+
const {createHmac} = require('crypto');
|
|
3
|
+
|
|
4
|
+
const derEncodeSignature = require('./der_encode_signature');
|
|
5
|
+
|
|
6
|
+
const asDer = n => (n[0]&128)?Buffer.concat([Buffer.alloc(1),n],1+n.length):n;
|
|
7
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
8
|
+
const {from} = Buffer;
|
|
9
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
10
|
+
const hmacSha256 = (pk, url) => createHmac('sha256', pk).update(url).digest();
|
|
11
|
+
const sha256 = n => createHash('sha256').update(n).digest();
|
|
12
|
+
const utf8AsBuffer = utf8 => Buffer.from(utf8, 'utf8');
|
|
13
|
+
|
|
14
|
+
/** Sign an authentication challenge for LNURL Auth
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
ecp: <ECPair Object>
|
|
18
|
+
hostname: <Domain for Authentication Challenge String>
|
|
19
|
+
k1: <Challenge Nonce String>
|
|
20
|
+
seed: <Seed Signature String>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@returns
|
|
24
|
+
{
|
|
25
|
+
public_key: <Signing Identity Public Key Hex String>
|
|
26
|
+
signature: <Signature For Authentication Challenge Hex String>
|
|
27
|
+
}
|
|
28
|
+
*/
|
|
29
|
+
module.exports = ({ecp, hostname, k1, seed}) => {
|
|
30
|
+
// LUD-13: LN wallet defines hashingKey as sha256(signature)
|
|
31
|
+
const hashingKey = sha256(utf8AsBuffer(seed));
|
|
32
|
+
|
|
33
|
+
// LUD-13: linkingPrivKey is defined as hmacSha256(hashingKey, domain)
|
|
34
|
+
const linkingPrivKey = hmacSha256(hashingKey, utf8AsBuffer(hostname));
|
|
35
|
+
|
|
36
|
+
// Instantiate the key pair from this derived private key
|
|
37
|
+
const linkingKey = ecp.fromPrivateKey(linkingPrivKey);
|
|
38
|
+
|
|
39
|
+
// Using the host-specific linking key, sign the challenge k1 value
|
|
40
|
+
const signature = bufferAsHex(from(linkingKey.sign(hexAsBuffer(k1))));
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
public_key: bufferAsHex(linkingKey.publicKey),
|
|
44
|
+
signature: derEncodeSignature({signature}).encoded,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {bech32} = require('bech32');
|
|
3
|
+
const {createInvoice} = require('ln-service');
|
|
4
|
+
const {returnResult} = require('asyncjs-util');
|
|
5
|
+
|
|
6
|
+
const {decode} = bech32;
|
|
7
|
+
const asLnurl = n => n.substring(n.startsWith('lightning:') ? 10 : 0);
|
|
8
|
+
const bech32CharLimit = 2000;
|
|
9
|
+
const errorStatus = 'ERROR';
|
|
10
|
+
const isNumber = n => !isNaN(n);
|
|
11
|
+
const minWithdrawable = 1;
|
|
12
|
+
const mtokensAsTokens = n => Math.floor(n / 1000);
|
|
13
|
+
const prefix = 'lnurl';
|
|
14
|
+
const {round} = Math;
|
|
15
|
+
const sslProtocol = 'https:';
|
|
16
|
+
const tag = 'withdrawRequest';
|
|
17
|
+
const tokensAsMillitokens = n => n * 1000;
|
|
18
|
+
const wordsAsUtf8 = n => Buffer.from(bech32.fromWords(n)).toString('utf8');
|
|
19
|
+
|
|
20
|
+
/** Withdraw from lnurl
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
ask: <Ask Function>
|
|
24
|
+
request: <Request Function>
|
|
25
|
+
lnd: <Authenticated LND API Object>
|
|
26
|
+
lnurl: <Lnurl String>
|
|
27
|
+
logger: <Winston Logger Object>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@returns via cbk or Promise
|
|
31
|
+
*/
|
|
32
|
+
module.exports = (args, cbk) => {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
return asyncAuto({
|
|
35
|
+
// Check arguments
|
|
36
|
+
validate: cbk => {
|
|
37
|
+
if (!args.ask) {
|
|
38
|
+
return cbk([400, 'ExpectedAskFunctionToWithdrawFromLnurl']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!args.lnurl) {
|
|
42
|
+
return cbk([400, 'ExpectedUrlToWithdrawFromLnurl']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
decode(asLnurl(args.lnurl), bech32CharLimit);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return cbk([400, 'FailedToDecodeLnurlToWithdraw', {err}]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (decode(asLnurl(args.lnurl), bech32CharLimit).prefix !== prefix) {
|
|
52
|
+
return cbk([400, 'ExpectedLnUrlPrefixToWithdraw']);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!args.lnd) {
|
|
56
|
+
return cbk([400, 'ExpectedLndToWithdrawFromLnurl']);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!args.logger) {
|
|
60
|
+
return cbk([400, 'ExpectedLoggerToWithdrawFromLnurl']);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!args.request) {
|
|
64
|
+
return cbk([400, 'ExpectedRequestFunctionToGetLnurlWithdrawData']);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return cbk();
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Get accepted terms from the encoded url
|
|
71
|
+
getTerms: ['validate', ({}, cbk) => {
|
|
72
|
+
const {words} = decode(asLnurl(args.lnurl), bech32CharLimit);
|
|
73
|
+
|
|
74
|
+
const url = wordsAsUtf8(words);
|
|
75
|
+
|
|
76
|
+
return args.request({url, json: true}, (err, r, json) => {
|
|
77
|
+
if (!!err) {
|
|
78
|
+
return cbk([503, 'FailureGettingLnUrlDataFromUrl', {err}]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!json) {
|
|
82
|
+
return cbk([503, 'ExpectedJsonObjectReturnedInLnurlResponse']);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (json.status === errorStatus) {
|
|
86
|
+
return cbk([503, 'LnurlWithdrawReturnedErr', {err: json.reason}]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!json.callback) {
|
|
90
|
+
return cbk([503, 'ExpectedCallbackInLnurlResponseJson']);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
new URL(json.callback);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return cbk([503, 'ExpectedValidCallbackUrlInLnurlResponseJson']);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if ((new URL(json.callback)).protocol !== sslProtocol) {
|
|
100
|
+
return cbk([400, 'LnurlsThatSpecifyNonSslUrlsAreUnsupported']);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!json.k1) {
|
|
104
|
+
return cbk([503, 'ExpectedK1InLnurlResponseJson']);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!json.tag) {
|
|
108
|
+
return cbk([503, 'ExpectedTagInLnurlResponseJson']);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (json.tag !== tag) {
|
|
112
|
+
return cbk([503, 'ExpectedTagToBeWithdrawRequestInLnurlResponse']);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!isNumber(json.minWithdrawable)) {
|
|
116
|
+
return cbk([503, 'ExpectedNumericValueForMinWithdrawable']);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!isNumber(json.maxWithdrawable)) {
|
|
120
|
+
return cbk([503, 'ExpectedNumericValueForMaxWithdrawable']);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (json.minWithdrawable < minWithdrawable) {
|
|
124
|
+
return cbk([400, 'MinWithdrawableIsLowerThanSupportedValue']);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (json.minWithdrawable > json.maxWithdrawable) {
|
|
128
|
+
return cbk([400, 'MinWithdrawableIsHigherThanMaxWithdrawable']);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return cbk(null, {
|
|
132
|
+
description: json.defaultDescription,
|
|
133
|
+
k1: json.k1,
|
|
134
|
+
max: mtokensAsTokens(json.maxWithdrawable),
|
|
135
|
+
min: mtokensAsTokens(json.minWithdrawable),
|
|
136
|
+
url: json.callback,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}],
|
|
140
|
+
|
|
141
|
+
// Ask amount for invoice
|
|
142
|
+
askAmount: ['getTerms', ({getTerms}, cbk) => {
|
|
143
|
+
const {max} = getTerms;
|
|
144
|
+
const {min} = getTerms;
|
|
145
|
+
|
|
146
|
+
return args.ask({
|
|
147
|
+
default: min,
|
|
148
|
+
message: `Amount to withdraw? (min: ${min}, max: ${max})`,
|
|
149
|
+
name: 'amount',
|
|
150
|
+
type: 'input',
|
|
151
|
+
validate: input => {
|
|
152
|
+
if (!input) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The amount should be numeric
|
|
157
|
+
if (!isNumber(input)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (round(input) !== Number(input)) {
|
|
162
|
+
return 'Fractional amounts are not supported';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (Number(input) > max) {
|
|
166
|
+
return `Service max withdrawable is ${max}, try a lower amount?`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (Number(input) < min) {
|
|
170
|
+
return `Service min withdrawable is ${min}, try higher amount?`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return true;
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
({amount}) => cbk(null, tokensAsMillitokens(Number(amount))));
|
|
177
|
+
}],
|
|
178
|
+
|
|
179
|
+
// Create a new payment request for withdrawl
|
|
180
|
+
createInvoice: ['askAmount', ({askAmount}, cbk) => {
|
|
181
|
+
return createInvoice({lnd: args.lnd, mtokens: askAmount}, cbk);
|
|
182
|
+
}],
|
|
183
|
+
|
|
184
|
+
// Send the withdraw request
|
|
185
|
+
withdraw: [
|
|
186
|
+
'createInvoice',
|
|
187
|
+
'getTerms',
|
|
188
|
+
({createInvoice, getTerms}, cbk) =>
|
|
189
|
+
{
|
|
190
|
+
args.logger.info({invoice: createInvoice.request});
|
|
191
|
+
|
|
192
|
+
const {url} = getTerms;
|
|
193
|
+
const {k1} = getTerms;
|
|
194
|
+
|
|
195
|
+
const qs = {k1, pr: createInvoice.request};
|
|
196
|
+
|
|
197
|
+
return args.request({url, qs, json: true}, (err, r, json) => {
|
|
198
|
+
if (!!err) {
|
|
199
|
+
return cbk([503, 'UnexpectedErrorRequestingLnurlWithdraw', {err}]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!json) {
|
|
203
|
+
return cbk([503, 'ExpectedJsonObjectReturnedInWithdrawResponse']);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!json.status) {
|
|
207
|
+
return cbk([503, 'ExpectedStatusInLnurlWithdrawResponseJson']);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (json.status === errorStatus) {
|
|
211
|
+
return cbk([503, 'LnurlWithdrawReqFailed', {err: json.reason}]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (json.status !== 'OK') {
|
|
215
|
+
return cbk([503, 'ExpectedStatusToBeOkInLnurlResponseJson']);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
args.logger.info({withdrawal_request_sent: true});
|
|
219
|
+
|
|
220
|
+
return cbk();
|
|
221
|
+
});
|
|
222
|
+
}],
|
|
223
|
+
},
|
|
224
|
+
returnResult({reject, resolve}, cbk));
|
|
225
|
+
});
|
|
226
|
+
};
|
|
@@ -7,7 +7,7 @@ const {getChannels} = require('ln-service');
|
|
|
7
7
|
const {getIdentity} = require('ln-service');
|
|
8
8
|
const {getNode} = require('ln-service');
|
|
9
9
|
const moment = require('moment');
|
|
10
|
-
const {parsePaymentRequest} = require('
|
|
10
|
+
const {parsePaymentRequest} = require('ln-service');
|
|
11
11
|
const {payViaRoutes} = require('ln-service');
|
|
12
12
|
const {returnResult} = require('asyncjs-util');
|
|
13
13
|
const {signBytes} = require('ln-service');
|
package/package.json
CHANGED
|
@@ -28,31 +28,33 @@
|
|
|
28
28
|
"colorette": "2.0.16",
|
|
29
29
|
"crypto-js": "4.1.1",
|
|
30
30
|
"csv-parse": "5.0.4",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
31
|
+
"ecpair": "2.0.1",
|
|
32
|
+
"goldengate": "11.2.1",
|
|
33
|
+
"grammy": "1.8.0",
|
|
33
34
|
"hot-formula-parser": "4.0.0",
|
|
34
35
|
"import-lazy": "4.0.0",
|
|
35
36
|
"ini": "3.0.0",
|
|
36
37
|
"inquirer": "8.2.2",
|
|
37
|
-
"invoices": "2.0.5",
|
|
38
38
|
"ln-accounting": "5.0.6",
|
|
39
39
|
"ln-service": "53.11.0",
|
|
40
40
|
"ln-sync": "3.12.0",
|
|
41
41
|
"ln-telegram": "3.21.1",
|
|
42
|
-
"moment": "2.29.
|
|
43
|
-
"paid-services": "3.
|
|
42
|
+
"moment": "2.29.3",
|
|
43
|
+
"paid-services": "3.15.1",
|
|
44
44
|
"probing": "2.0.5",
|
|
45
45
|
"psbt": "2.0.1",
|
|
46
46
|
"qrcode-terminal": "0.12.0",
|
|
47
47
|
"sanitize-filename": "1.6.3",
|
|
48
48
|
"socks-proxy-agent": "6.2.0-beta.0",
|
|
49
49
|
"table": "6.8.0",
|
|
50
|
+
"tiny-secp256k1": "2.2.1",
|
|
50
51
|
"update-notifier": "5.1.0",
|
|
51
52
|
"window-size": "1.1.1"
|
|
52
53
|
},
|
|
53
54
|
"description": "Lightning balance CLI",
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@alexbosworth/tap": "15.0.11",
|
|
57
|
+
"invoices": "2.0.6",
|
|
56
58
|
"ln-docker-daemons": "2.2.7",
|
|
57
59
|
"mock-lnd": "1.4.1",
|
|
58
60
|
"tiny-secp256k1": "2.2.1"
|
|
@@ -82,5 +84,5 @@
|
|
|
82
84
|
"postpublish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t alexbosworth/balanceofsatoshis --push .",
|
|
83
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"
|
|
84
86
|
},
|
|
85
|
-
"version": "12.0
|
|
87
|
+
"version": "12.3.0"
|
|
86
88
|
}
|
|
@@ -3,7 +3,7 @@ const asyncFilter = require('async/filter');
|
|
|
3
3
|
const {balancedOpenRequest} = require('paid-services');
|
|
4
4
|
const {getInvoices} = require('ln-service');
|
|
5
5
|
const {getPayment} = require('ln-service');
|
|
6
|
-
const {parsePaymentRequest} = require('
|
|
6
|
+
const {parsePaymentRequest} = require('ln-service');
|
|
7
7
|
const {returnResult} = require('asyncjs-util');
|
|
8
8
|
|
|
9
9
|
const {balancedChannelKeyTypes} = require('./service_key_types');
|
|
@@ -2,7 +2,7 @@ const asyncAuto = require('async/auto');
|
|
|
2
2
|
const {formatTokens} = require('ln-sync');
|
|
3
3
|
const {getNodeAlias} = require('ln-sync');
|
|
4
4
|
const {getPayment} = require('ln-service');
|
|
5
|
-
const {parsePaymentRequest} = require('
|
|
5
|
+
const {parsePaymentRequest} = require('ln-service');
|
|
6
6
|
const {payViaPaymentRequest} = require('ln-service');
|
|
7
7
|
const {returnResult} = require('asyncjs-util');
|
|
8
8
|
|
|
@@ -8,7 +8,7 @@ const {getSwapOutTerms} = require('goldengate');
|
|
|
8
8
|
const {lightningLabsSwapAuth} = require('goldengate');
|
|
9
9
|
const {lightningLabsSwapService} = require('goldengate');
|
|
10
10
|
const {paidMacaroon} = require('goldengate');
|
|
11
|
-
const {parsePaymentRequest} = require('
|
|
11
|
+
const {parsePaymentRequest} = require('ln-service');
|
|
12
12
|
const {payViaPaymentRequest} = require('ln-service');
|
|
13
13
|
const {returnResult} = require('asyncjs-util');
|
|
14
14
|
const {swapUserId} = require('goldengate');
|