Compare commits

..

22 Commits

Author SHA1 Message Date
Overtorment
04a42de0e6 REL: 1.2.3 2022-01-12 17:15:43 +00:00
Overtorment
265abdc639 REF 2022-01-02 15:45:54 +00:00
Overtorment
e6dc7f99f5 FIX: double starts; FIX: 'slow' feerate is now always 1 s/vb; REF: no trace logging 2022-01-02 13:58:23 +00:00
Overtorment
80882bf984 REF: 104 2021-12-22 16:12:34 +00:00
Overtorment
53181dabd6 FIX: fees 2021-12-06 18:23:08 +00:00
Overtorment
bce56ba10e FIX: fees 2021-12-06 18:12:07 +00:00
Overtorment
0472f9cdb5 REF: leak-tracking binary; FIX: slow fees 2021-12-06 18:06:44 +00:00
Overtorment
11d58c2350 FIX: usign wrong dir for channel monitors / manager 2021-12-06 12:07:38 +00:00
Overtorment
301ce6e99d FIX: listen interface; FIX: cli (--no-display) 2021-12-04 12:55:30 +00:00
Overtorment
99925c3c2d ADD: cors; FIX: log 2021-12-04 12:20:01 +00:00
Overtorment
aedc4bf61f REF: bump java ldk; fixes 2021-12-04 11:56:10 +00:00
Overtorment
42b6302d23 Merge branch 'master' of github.com:BlueWallet/HelloLightning 2021-11-24 17:14:49 +00:00
Overtorment
5146cf1d02 ADD: basic gui (should be opened in browser) 2021-11-24 17:14:30 +00:00
Overtorment
c2792254a7 ADD: storage prefix (aka fingerprint) 2021-11-24 12:54:01 +00:00
Overtorment
0213a0e9ba REF 2021-11-24 12:53:21 +00:00
Overtorment
6b87b87d33 REF 2021-11-24 12:52:26 +00:00
Overtorment
84d35fe0f9
Update README.md 2021-11-19 18:08:18 +00:00
Overtorment
dab70e6483
FIX 2021-11-17 17:43:47 +00:00
Overtorment
551ee6b305
FIX: broadcast redeem txs 2021-11-17 12:31:18 +00:00
Overtorment
964d045b59
ADD: refund address; maturing balance; version 2021-11-16 18:24:31 +00:00
Overtorment
dd56f6831b
DOC 2021-11-16 18:22:47 +00:00
Overtorment
e2de426a82
FIX: unhardcode wallet entropy 2021-11-15 19:25:00 +00:00
58 changed files with 10869 additions and 80 deletions

2
.idea/compiler.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
<bytecodeTargetLevel target="17" />
</component>
</project>

2
.idea/misc.xml generated
View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -1,15 +1,16 @@
status: alpha (not ready for production)
# Hello, Lightning!
Cli lightning network server, based on LDK (rust-lightning).
Provides DUMB-RPC (telnet friendly) and HTTP interface.
Provides HTTP-RPC interface.
## Example:
* build it
* build it (or download binary from releases)
* run it: `java -jar ./out/artifacts/hello_main_jar/hello.main.jar`
* now DUMB-RPC/HTTP server listens on port 8310
* start the server with `start.sh` script
* ...or run nodejs control process from `./cli/` directory (`npm i && npm start`)
* now HTTP server listens on port 8310
* run nodejs control process from `./cli/` directory (`npm i && npm start`)
## Philosophy
@ -17,16 +18,24 @@ Barebone Java-based server cant do much, out of the box it can only do lightning
All the functionality should be implemented on upper level (like, GUI application, or nodejs cli script, etc), that
includes: providing blockchain data, managing onchain coins to open channels (via PSBT), keeping a list of peers to keep connections etc
So currently repo has ldk-server (compiled from kotlin, considered a lower level), which is supposed to run and provide RPC, and a cli nodejs
process which controls it (considered upper level). Cli process utilizes following apis:
* https://github.com/Blockstream/esplora/blob/master/API.md to fetch blockchain data
* https://1ml.com/api to fetch ip addresses for other ln nodes pubkeys
Whole setup is thus quite lightweight.
## Security
Server is intended to run in a secure environment. Thus, on-disk storage is not encrypted, and RPC server
handles connections without TLS (plain HTTP)
handles connections without TLS (plain HTTP). Also, even though RPC listens on 127.0.0.1, it has no auth.
## TODO
* ~~port methods from https://github.com/BlueWallet/rn-ldk/blob/master/android/src/main/java/com/rnldk/RnLdkModule.kt while adding DUMB-RPC interface for them~~
* create a GUI app (Electron?)
* create a cli controll process
* ~~create a cli control process~~
* ...
* Profit!
@ -34,26 +43,26 @@ handles connections without TLS (plain HTTP)
* [x] start
* [x] stop
* [x] transactionConfirmed
* [x] transactionUnconfirmed
* [x] getRelevantTxids
* [x] updateBestBlock
* [x] connectPeer
* [x] disconnectByNodeId
* [x] sendPayment
* [x] addInvoice
* [x] listPeers
* [x] getNodeId
* [x] closeChannelCooperatively
* [x] closeChannelForce
* [x] openChannelStep1
* [x] openChannelStep2
* [x] listUsableChannels
* [x] listChannels
* [x] setRefundAddressScript
* [x] setFeerate
* [x] getMaturingBalance
* [x] getMaturingHeight
* [x] transactionconfirmed
* [x] transactionunconfirmed
* [x] getrelevanttxids
* [x] updatebestblock
* [x] connectpeer
* [x] disconnectbynodeid
* [x] sendpayment
* [x] addinvoice
* [x] listpeers
* [x] getnodeid
* [x] closechannelcooperatively
* [x] closechannelforce
* [x] openchannelstep1
* [x] openchannelstep2
* [x] listusablechannels
* [x] listcChannels
* [x] setrefundaddressscript
* [x] setfeerate
* [x] getmaturingbalance
* [x] getmaturingheight
* [x] savenetworkgraph
* [x] geteventsfundinggenerationready
* [x] geteventschannelclosed
@ -67,6 +76,15 @@ handles connections without TLS (plain HTTP)
* [x] geteventspaymentreceived
* [x] geteventspaymentforwarded
## Storage
Data is written in `~/.hellolightning` (non-configurable).
There are files with states per each channel, and one for the channel manager.
Upon the first launch of cli control script, given java process is running, HelloLightning will be seeded
with a secure entropy, which is then stored in `seed.txt`.
All events that must be passed from lower level (lightning) to upper level (control script) are served through
their respective RPCs, and also stored as json files.
## License
MIT

175
cli/package-lock.json generated
View File

@ -1278,12 +1278,59 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"base-x": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz",
"integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"bech32": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bip174": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz",
"integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ=="
},
"bip32": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.6.tgz",
"integrity": "sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA==",
"requires": {
"@types/node": "10.12.18",
"bs58check": "^2.1.1",
"create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
"tiny-secp256k1": "^1.1.3",
"typeforce": "^1.11.5",
"wif": "^2.0.6"
},
"dependencies": {
"@types/node": {
"version": "10.12.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz",
"integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ=="
}
}
},
"bip39": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz",
@ -1302,6 +1349,25 @@
}
}
},
"bitcoinjs-lib": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.0.0.tgz",
"integrity": "sha512-KYx81rVE4LDbZcHfE375NCX4CDeZuz7HECZm/KAmqKMY2jpD3ZcUnI7Fm+QX5EMF/xmtzzfrNL/BNxo8o0iOQg==",
"requires": {
"bech32": "^2.0.0",
"bip174": "^2.0.1",
"bs58check": "^2.1.2",
"create-hash": "^1.1.0",
"typeforce": "^1.11.3",
"varuint-bitcoin": "^1.1.2",
"wif": "^2.0.1"
}
},
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"boxen": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
@ -1345,6 +1411,11 @@
"fill-range": "^7.0.1"
}
},
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
},
"browser-process-hrtime": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
@ -1373,6 +1444,24 @@
"fast-json-stable-stringify": "2.x"
}
},
"bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=",
"requires": {
"base-x": "^3.0.2"
}
},
"bs58check": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz",
"integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==",
"requires": {
"bs58": "^4.0.0",
"create-hash": "^1.1.0",
"safe-buffer": "^5.1.2"
}
},
"bser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@ -1836,6 +1925,20 @@
"integrity": "sha512-P/nDMPIYdb2PyqCQwhTXNi5JFjX1AsDVR0y6FrHw752izJIAJ+Pn5lugqyBq4tXeRSZBMBb2ZGvRGB1djtELEQ==",
"dev": true
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"emittery": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz",
@ -2260,6 +2363,11 @@
"flat-cache": "^3.0.4"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -2484,6 +2592,25 @@
}
}
},
"hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"requires": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"requires": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"html-encoding-sniffer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@ -3530,6 +3657,16 @@
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"dev": true
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@ -3551,6 +3688,11 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
},
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -4344,6 +4486,18 @@
"integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==",
"dev": true
},
"tiny-secp256k1": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.6.tgz",
"integrity": "sha512-FmqJZGduTyvsr2cF3375fqGHUovSwDi/QytexX1Se4BPuPZpTE5Ftp5fg+EFSuEf3lhZqgCRjEG3ydUQ/aNiwA==",
"requires": {
"bindings": "^1.3.0",
"bn.js": "^4.11.8",
"create-hmac": "^1.1.7",
"elliptic": "^6.4.0",
"nan": "^2.13.2"
}
},
"tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@ -4496,6 +4650,11 @@
"is-typedarray": "^1.0.0"
}
},
"typeforce": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
},
"typescript": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
@ -4593,6 +4752,14 @@
}
}
},
"varuint-bitcoin": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz",
"integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==",
"requires": {
"safe-buffer": "^5.1.1"
}
},
"w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
@ -4670,6 +4837,14 @@
"string-width": "^4.0.0"
}
},
"wif": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz",
"integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=",
"requires": {
"bs58check": "<3.0.0"
}
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",

View File

@ -37,7 +37,9 @@
"author": "Jakub Synowiec <jsynowiec@users.noreply.github.com>",
"license": "Apache-2.0",
"dependencies": {
"bip32": "2.0.6",
"bip39": "^3.0.4",
"bitcoinjs-lib": "^6.0.0",
"cli-table": "^0.3.6",
"cross-fetch": "^3.1.4",
"tslib": "~2.3.1"

View File

@ -1,6 +1,8 @@
/* eslint-disable no-empty,@typescript-eslint/no-inferrable-types,@typescript-eslint/no-var-requires */
import fetch from 'cross-fetch';
import * as bip39 from 'bip39';
const bitcoin = require('bitcoinjs-lib');
const HDNode = require('bip32');
const crypto = require('crypto');
const fs = require('fs');
@ -11,7 +13,8 @@ export default class Ldk {
private _nodeConnectionDetailsCache: any = {};
logToGeneralLog(...args: any[]) {
const str = JSON.stringify(args);
let str = new Date().toUTCString();
args.map(arg => str += ' ' + JSON.stringify(arg));
this.logs.push(str)
}
@ -54,7 +57,7 @@ export default class Ldk {
return this._processResult(text);
}
private async updateFeerate() {
async updateFeerate() {
this.logToGeneralLog('updating feerate');
try {
const response = await fetch('https://blockstream.info/api/fee-estimates');
@ -67,8 +70,9 @@ export default class Ldk {
if (json[blockFast] && json[blockMedium] && json[blockSlow]) {
const feerateFast = Math.round(json[blockFast]);
const feerateMedium = Math.round(json[blockMedium]);
const feerateSlow = Math.round(json[blockSlow]);
await this.setFeerate(Math.max(feerateFast, 2), Math.max(feerateMedium, 2), Math.max(feerateSlow, 2));
// const feerateSlow = Math.round(json[blockSlow]);
const feerateSlow = 1; // less secure but should help us avoid force-closures
await this.setFeerate(Math.max(feerateFast, 2), Math.max(feerateMedium, 1), Math.max(feerateSlow, 1));
} else {
throw new Error('Invalid feerate data:' + JSON.stringify(json));
}
@ -87,9 +91,9 @@ export default class Ldk {
*/
private async setFeerate(newFeerateFast: number, newFeerateMedium: number, newFeerateSlow: number): Promise<boolean> {
this.logToGeneralLog('setting feerate', { newFeerateFast, newFeerateMedium, newFeerateSlow });
const fast = newFeerateFast * 250;
const medium = newFeerateMedium * 250;
const slow = newFeerateSlow * 250;
const fast = Math.max(newFeerateFast * 250, 253);
const medium = Math.max(newFeerateMedium * 250, 253);
const slow = Math.max(newFeerateSlow * 250, 253);
const response = await fetch(`http://127.0.0.1:8310/setfeerate/${fast}/${medium}/${slow}`);
const text = await response.text();
return this._processResult(text);
@ -230,6 +234,24 @@ export default class Ldk {
return this._processResult(text)
}
async broadcastTxsIfNecessary() {
const txs = await this.getTxsBroadcast();
for (const tx of txs) {
this.logToGeneralLog('should broadcast', tx)
const response = await fetch('https://blockstream.info/api/tx', {
method: 'POST',
body: tx.txhex
});
this.logToGeneralLog('broadcast result: ' + await response.text());
}
}
async getTxsBroadcast() {
const response = await fetch(`http://127.0.0.1:8310/geteventstxbroadcast`);
const text = await response.text();
return this._processResult(text)
}
private async getRegisteredTxs() {
const response = await fetch(`http://127.0.0.1:8310/geteventsregistertx`);
const text = await response.text();
@ -317,8 +339,31 @@ export default class Ldk {
return this._processResult(text)
}
unwrapFirstExternalAddressFromMnemonics() {
if (!this.getSecret()) throw new Error('no secret');
const mnemonic = this.getSecret();
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = HDNode.fromSeed(seed);
const path = "m/84'/0'/0'/0/0";
const child = root.derivePath(path);
return bitcoin.payments.p2wpkh({
pubkey: child.publicKey,
}).address;
}
unwrapFirstExternalWifFromMnemonics() {
if (!this.getSecret()) throw new Error('no secret');
const mnemonic = this.getSecret();
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = HDNode.fromSeed(seed);
const path = "m/84'/0'/0'/0/0";
const child = root.derivePath(path);
return child.toWIF();
}
async reconnectPeers(homedir: string) {
this.logToGeneralLog('attempting to reconnect peers if needed...');
const peers2reconnect = {};
const listPeers = await this.listPeers();
@ -361,6 +406,30 @@ export default class Ldk {
return this._processResult(text);
}
public async version() {
const response = await fetch(`http://127.0.0.1:8310/version`);
const text = await response.text();
return this._processResult(text);
}
public async ldkversion() {
const response = await fetch(`http://127.0.0.1:8310/ldkversion`);
const text = await response.text();
return this._processResult(text);
}
public async getMaturingBalance() {
const response = await fetch(`http://127.0.0.1:8310/getmaturingbalance`);
const text = await response.text();
return this._processResult(text);
}
public async getMaturingHeight() {
const response = await fetch(`http://127.0.0.1:8310/getmaturingheight`);
const text = await response.text();
return this._processResult(text);
}
async lookupNodeConnectionDetailsByPubkey(pubkey: string) {
// first, trying cache:
if (this._nodeConnectionDetailsCache[pubkey] && +new Date() - this._nodeConnectionDetailsCache[pubkey].ts < 4 * 7 * 24 * 3600 * 1000) {
@ -387,4 +456,13 @@ export default class Ldk {
}
}
}
async setRefundAddress(address: string) {
const script = bitcoin.address.toOutputScript(address);
const refundAddressScriptHex = script.toString('hex');
const response = await fetch(`http://127.0.0.1:8310/setrefundaddressscript/${refundAddressScriptHex}`);
const text = await response.text();
return this._processResult(text);
}
}

View File

@ -6,6 +6,8 @@ const fs = require('fs');
let lastBlockchainSync = 0;
let lastNetworkGraphSaved = 0;
let lastPeersReconnect = 0;
let maturingBalance = 0;
let maturingHeight = 0;
const homedir = require('os').homedir() + '/.hellolightning';
const seedfile = `${homedir}/seed.txt`;
const ldk = new Ldk();
@ -13,6 +15,7 @@ const ldk = new Ldk();
async function tick() {
console.clear();
console.log('Hello, Lightning!');
console.log(`using ${homedir}`);
if (!fs.existsSync(homedir)) {
@ -28,6 +31,14 @@ async function tick() {
await new Promise(resolve => setTimeout(resolve, 5000)); // sleep
}
if (!ldk.getSecret()) {
const seedFromDisk = fs.readFileSync(seedfile, { encoding: 'utf8' })
ldk.setSecret(seedFromDisk);
}
console.log('seed:', ldk.getSecret());
console.log('refund address:', ldk.unwrapFirstExternalAddressFromMnemonics(), 'refund address WIF:', ldk.unwrapFirstExternalWifFromMnemonics());
let started = true;
let nodeid: string;
try {
@ -41,11 +52,10 @@ async function tick() {
if (!started) {
console.log('attempting to start a node...');
try {
const seedFromDisk = fs.readFileSync(seedfile, { encoding: 'utf8' })
ldk.setSecret(seedFromDisk);
// const hex = ldk.getEntropyHex();
// await ldk.start(hex);
await ldk.start("00000000000000000000000000000000000000000000000000000000000000f6"); // fixme
await ldk.setRefundAddress(ldk.unwrapFirstExternalAddressFromMnemonics());
await ldk.updateFeerate(); // so any refund claim upon startup would use adequate fee
await ldk.start(ldk.getEntropyHex());
// await ldk.start("00000000000000000000000000000000000000000000000000000000000000f6"); // fixme
} catch (error) {
console.error(error.message);
await new Promise(resolve => setTimeout(resolve, 10* 1000)); // sleep
@ -54,10 +64,15 @@ async function tick() {
return;
}
console.log('version:', await ldk.version(), "(ldk binaries version: " + await ldk.ldkversion() + ")");
if (+new Date() - lastBlockchainSync > 5 * 60 * 1000) { // 5 min
lastBlockchainSync = +new Date();
await ldk.setRefundAddress(ldk.unwrapFirstExternalAddressFromMnemonics());
maturingBalance = await ldk.getMaturingBalance();
maturingHeight = await ldk.getMaturingHeight();
ldk.checkBlockchain(); // let it run in the background
await ldk.broadcastTxsIfNecessary();
}
if (+new Date() - lastNetworkGraphSaved > 1 * 60 * 1000) {
@ -65,14 +80,14 @@ async function tick() {
ldk.saveNetworkGraph(); // let it run in the background
}
if (+new Date() - lastPeersReconnect > 1 * 60 * 1000) {
if (+new Date() - lastPeersReconnect > 0.5 * 60 * 1000) {
lastPeersReconnect = +new Date();
ldk.reconnectPeers(homedir); // let it run in the background
}
const peers = await ldk.listPeers();
const channels = await ldk.listChannels();
const activeAhannels = await ldk.listUsableChannels();
const activeChannels = await ldk.listUsableChannels();
let outbound_capacity_msat = 0;
let inbound_capacity_msat = 0;
@ -84,12 +99,16 @@ async function tick() {
const table = new Table()
table.push(
['num peers', 'last sync', 'num channels', 'num active channels', 'channel balance', 'inbound capacity', 'node id']
, [peers.length, Math.floor((+new Date() - lastBlockchainSync)/1000) + ' sec ago', channels.length, activeAhannels.length, msatToBitcoinString(outbound_capacity_msat), msatToBitcoinString(inbound_capacity_msat), nodeid]
['num peers', 'last sync', 'num channels\n(active/total)', 'channel balance', 'inbound capacity', 'node id']
, [peers.length, Math.floor((+new Date() - lastBlockchainSync)/1000) + ' sec ago', activeChannels.length + ' / ' + channels.length, msatToBitcoinString(outbound_capacity_msat), msatToBitcoinString(inbound_capacity_msat), nodeid]
)
if (maturingBalance > 0) {
console.log('maturing balance:', maturingBalance, "sat (awaiting height " + maturingHeight + ")");
}
console.log(table.toString())
console.log(ldk.getLastLogsLines(30).join("\n"));
console.log(ldk.getLastLogsLines(20).join("\n"));
}
async function main() {

2
example/openchannelstep1.sh Executable file
View File

@ -0,0 +1,2 @@
curl http://127.0.0.1:8310/openchannelstep1/03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f/500000/1

2
example/version.sh Executable file
View File

@ -0,0 +1,2 @@
curl http://127.0.0.1:8310/version

31
gui/.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
.idea
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

1
gui/404.html Normal file
View File

@ -0,0 +1 @@
<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width"/><meta charSet="utf-8"/><title>404: This page could not be found</title><meta name="next-head-count" content="3"/><link rel="preload" href="/_next/static/css/f2259aca1f14a10459d9.css" as="style"/><link rel="stylesheet" href="/_next/static/css/f2259aca1f14a10459d9.css" data-n-g=""/><noscript data-n-css=""></noscript><link rel="preload" href="/_next/static/chunks/webpack-189c53927ffd3caf09c3.js" as="script"/><link rel="preload" href="/_next/static/chunks/framework-4ae45ca6d0f28c4504d3.js" as="script"/><link rel="preload" href="/_next/static/chunks/main-899697ea82bdc85e7a94.js" as="script"/><link rel="preload" href="/_next/static/chunks/pages/_app-4be02ffccdd9fceacfca.js" as="script"/><link rel="preload" href="/_next/static/chunks/pages/_error-b30902e13465df7c5366.js" as="script"/></head><body><div id="__next"><div style="color:#000;background:#fff;font-family:-apple-system, BlinkMacSystemFont, Roboto, &quot;Segoe UI&quot;, &quot;Fira Sans&quot;, Avenir, &quot;Helvetica Neue&quot;, &quot;Lucida Grande&quot;, sans-serif;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body { margin: 0 }</style><h1 style="display:inline-block;border-right:1px solid rgba(0, 0, 0,.3);margin:0;margin-right:20px;padding:10px 23px 10px 0;font-size:24px;font-weight:500;vertical-align:top">404</h1><div style="display:inline-block;text-align:left;line-height:49px;height:49px;vertical-align:middle"><h2 style="font-size:14px;font-weight:normal;line-height:inherit;margin:0;padding:0">This page could not be found<!-- -->.</h2></div></div></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"T-qGn4h1yqnREwY2tqUAk","nextExport":true,"isFallback":false,"gip":true}</script><script nomodule="" src="/_next/static/chunks/polyfills-eef578260fd80f8fff94.js"></script><script src="/_next/static/chunks/webpack-189c53927ffd3caf09c3.js" async=""></script><script src="/_next/static/chunks/framework-4ae45ca6d0f28c4504d3.js" async=""></script><script src="/_next/static/chunks/main-899697ea82bdc85e7a94.js" async=""></script><script src="/_next/static/chunks/pages/_app-4be02ffccdd9fceacfca.js" async=""></script><script src="/_next/static/chunks/pages/_error-b30902e13465df7c5366.js" async=""></script><script src="/_next/static/T-qGn4h1yqnREwY2tqUAk/_buildManifest.js" async=""></script><script src="/_next/static/T-qGn4h1yqnREwY2tqUAk/_ssgManifest.js" async=""></script></body></html>

472
gui/classes/ldk.ts Normal file
View File

@ -0,0 +1,472 @@
/* eslint-disable no-empty,@typescript-eslint/no-inferrable-types,@typescript-eslint/no-var-requires */
import fetch from 'cross-fetch';
import * as bip39 from 'bip39';
const bitcoin = require('bitcoinjs-lib');
const HDNode = require('bip32');
const crypto = require('crypto');
export default class Ldk {
private injectedScript2address: ((scriptHex: string) => Promise<string>) | null = null;
private logs: string[] = [];
private secret: string = '';
private _nodeConnectionDetailsCache: any = {};
logToGeneralLog(...args: any[]) {
let str = new Date().toUTCString();
args.map(arg => str += ' ' + JSON.stringify(arg));
this.logs.push(str);
}
getLastLogsLines(num: number) {
return this.logs.slice(num * -1);
}
private async getHeaderHexByHeight(height: number) {
const response2 = await fetch('https://blockstream.info/api/block-height/' + height);
const hash = await response2.text();
const response3 = await fetch('https://blockstream.info/api/block/' + hash + '/header');
return response3.text();
}
private async script2address(scriptHex: string): Promise<string> {
if (this.injectedScript2address) {
return await this.injectedScript2address(scriptHex);
}
const response = await fetch('https://runkit.io/overtorment/output-script-to-address/branches/master/' + scriptHex);
return response.text();
}
private async getCurrentHeight() {
const response = await fetch('https://blockstream.info/api/blocks/tip/height');
return parseInt(await response.text(), 10);
}
private async updateBestBlock() {
this.logToGeneralLog('updating best block');
const height = await this.getCurrentHeight();
const response2 = await fetch('https://blockstream.info/api/block-height/' + height);
const hash = await response2.text();
const response3 = await fetch('https://blockstream.info/api/block/' + hash + '/header');
const headerHex = await response3.text();
this.logToGeneralLog('updateBestBlock():', { headerHex, height });
const response = await fetch(`http://127.0.0.1:8310/updatebestblock/${headerHex}/${height}`);
const text = await response.text();
return this._processResult(text);
}
async updateFeerate() {
this.logToGeneralLog('updating feerate');
try {
const response = await fetch('https://blockstream.info/api/fee-estimates');
const json = await response.json();
const blockFast = '2'; // indexes in json object
const blockMedium = '6';
const blockSlow = '144';
if (json[blockFast] && json[blockMedium] && json[blockSlow]) {
const feerateFast = Math.round(json[blockFast]);
const feerateMedium = Math.round(json[blockMedium]);
// const feerateSlow = Math.round(json[blockSlow]);
const feerateSlow = 1; // less secure but should help us avoid force-closures
await this.setFeerate(Math.max(feerateFast, 2), Math.max(feerateMedium, 1), Math.max(feerateSlow, 1));
} else {
throw new Error('Invalid feerate data:' + JSON.stringify(json));
}
} catch (error) {
console.warn('updateFeerate() failed:', error);
this.logToGeneralLog('updateFeerate() failed:', error);
}
}
/**
* Prodives LKD current feerate to use with all onchain transactions (like sweeps after forse-closures)
*
* @param newFeerateFast {number} Sat/b
* @param newFeerateMedium {number} Sat/b
* @param newFeerateSlow {number} Sat/b
*/
private async setFeerate(newFeerateFast: number, newFeerateMedium: number, newFeerateSlow: number): Promise<boolean> {
this.logToGeneralLog('setting feerate', { newFeerateFast, newFeerateMedium, newFeerateSlow });
const fast = Math.max(newFeerateFast * 250, 253);
const medium = Math.max(newFeerateMedium * 250, 253);
const slow = Math.max(newFeerateSlow * 250, 253);
const response = await fetch(`http://127.0.0.1:8310/setfeerate/${fast}/${medium}/${slow}`);
const text = await response.text();
return this._processResult(text);
}
/**
* Fetches from network registered outputs, registered transactions and block tip
* and feeds this into to native code, if necessary.
* Should be called periodically.
*/
async checkBlockchain(progressCallback?: (progress: number) => void) {
this.logToGeneralLog('checkBlockchain() 1/x');
if (progressCallback) progressCallback(1 / 8);
await this.updateBestBlock();
this.logToGeneralLog('checkBlockchain() 2/x');
if (progressCallback) progressCallback(2 / 8);
await this.updateFeerate();
const confirmedBlocks: any = {};
// iterating all subscriptions for confirmed txid
this.logToGeneralLog('checkBlockchain() 3/x');
if (progressCallback) progressCallback(3 / 8);
for (const regTx of await this.getRegisteredTxs()) {
let json;
try {
const response = await fetch('https://blockstream.info/api/tx/' + regTx.txid);
json = await response.json();
} catch (_) {}
if (json && json.status && json.status.confirmed && json.status.block_height) {
// success! tx confirmed, and we need to notify LDK about it
let jsonPos;
try {
const responsePos = await fetch('https://blockstream.info/api/tx/' + regTx.txid + '/merkle-proof');
jsonPos = await responsePos.json();
} catch (_) {}
if (jsonPos && jsonPos.merkle) {
confirmedBlocks[json.status.block_height + ''] = confirmedBlocks[json.status.block_height + ''] || {};
const responseHex = await fetch('https://blockstream.info/api/tx/' + regTx.txid + '/hex');
confirmedBlocks[json.status.block_height + ''][jsonPos.pos + ''] = await responseHex.text();
}
}
}
// iterating all scripts for spends
this.logToGeneralLog('checkBlockchain() 4/x');
if (progressCallback) progressCallback(4 / 8);
for (const regOut of await this.getRegisteredOutputs()) {
let txs: any[] = [];
try {
const address = await this.script2address(regOut.script_pubkey);
const response = await fetch('https://blockstream.info/api/address/' + address + '/txs');
txs = await response.json();
} catch (_) {}
for (const tx of txs) {
if (tx && tx.status && tx.status.confirmed && tx.status.block_height) {
// got confirmed tx for that output!
let jsonPos;
try {
const responsePos = await fetch('https://blockstream.info/api/tx/' + tx.txid + '/merkle-proof');
jsonPos = await responsePos.json();
} catch (_) {}
if (jsonPos && jsonPos.merkle) {
const responseHex = await fetch('https://blockstream.info/api/tx/' + tx.txid + '/hex');
confirmedBlocks[tx.status.block_height + ''] = confirmedBlocks[tx.status.block_height + ''] || {};
confirmedBlocks[tx.status.block_height + ''][jsonPos.pos + ''] = await responseHex.text();
}
}
}
}
// now, got all data packed in `confirmedBlocks[block_number][tx_position]`
// lets feed it to LDK:
this.logToGeneralLog('confirmedBlocks=', confirmedBlocks);
this.logToGeneralLog('checkBlockchain() 5/x');
if (progressCallback) progressCallback(5 / 8);
for (const height of Object.keys(confirmedBlocks).sort((a, b) => parseInt(a, 10) - parseInt(b, 10))) {
for (const pos of Object.keys(confirmedBlocks[height]).sort((a, b) => parseInt(a, 10) - parseInt(b, 10))) {
await this.transactionConfirmed(await this.getHeaderHexByHeight(parseInt(height, 10)), parseInt(height, 10), parseInt(pos, 10), confirmedBlocks[height][pos]);
}
}
this.logToGeneralLog('checkBlockchain() 6/x');
if (progressCallback) progressCallback(6 / 8);
let txidArr = [];
try {
txidArr = await this.getRelevantTxids();
this.logToGeneralLog('getRelevantTxids:', txidArr);
} catch (error: any) {
this.logToGeneralLog('getRelevantTxids:', error.message);
console.warn('getRelevantTxids:', error.message);
}
// we need to check if any of txidArr got unconfirmed, and then feed it back to LDK if they are unconf
this.logToGeneralLog('checkBlockchain() 7/x');
if (progressCallback) progressCallback(7 / 8);
for (const txid of txidArr) {
let confirmed = false;
try {
const response = await fetch('https://blockstream.info/api/tx/' + txid + '/merkle-proof');
const tx: any = await response.json();
if (tx && tx.block_height) confirmed = true;
} catch (_) {
confirmed = false;
}
if (!confirmed) await this.transactionUnconfirmed(txid);
}
this.logToGeneralLog('checkBlockchain() done');
if (progressCallback) progressCallback(8 / 8);
return true;
}
private async getRelevantTxids() {
const response = await fetch('http://127.0.0.1:8310/getrelevanttxids');
const text = await response.text();
return this._processResult(text);
}
private async transactionConfirmed(headerHex: string, height: number, pos: number, transactionHex: string) {
const response = await fetch(`http://127.0.0.1:8310/transactionconfirmed/${headerHex}/${height}/${pos}/${transactionHex}`);
const text = await response.text();
return this._processResult(text);
}
private async transactionUnconfirmed(txid: string) {
const response = await fetch(`http://127.0.0.1:8310/transactionunconfirmed/${txid}`);
const text = await response.text();
return this._processResult(text);
}
async broadcastTxsIfNecessary() {
const txs = await this.getTxsBroadcast();
for (const tx of txs) {
this.logToGeneralLog('should broadcast', tx);
const response = await fetch('https://blockstream.info/api/tx', {
method: 'POST',
body: tx.txhex,
});
this.logToGeneralLog('broadcast result: ' + await response.text());
}
}
async getTxsBroadcast() {
const response = await fetch('http://127.0.0.1:8310/geteventstxbroadcast');
const text = await response.text();
return this._processResult(text);
}
private async getRegisteredTxs() {
const response = await fetch('http://127.0.0.1:8310/geteventsregistertx');
const text = await response.text();
return this._processResult(text);
}
private async getRegisteredOutputs() {
const response = await fetch('http://127.0.0.1:8310/geteventsregisteroutput');
const text = await response.text();
return this._processResult(text);
}
public async listPeers() {
const response = await fetch('http://127.0.0.1:8310/listpeers');
const text = await response.text();
return this._processResult(text);
}
public async listChannels() {
const response = await fetch('http://127.0.0.1:8310/listchannels');
const text = await response.text();
return this._processResult(text);
}
public async listUsableChannels() {
const response = await fetch('http://127.0.0.1:8310/listusablechannels');
const text = await response.text();
return this._processResult(text);
}
public async getNodeId() {
const response = await fetch('http://127.0.0.1:8310/getnodeid');
const text = await response.text();
return this._processResult(text);
}
public async start(entropy) {
const tip = await this.getCurrentHeight();
const response2 = await fetch('https://blockstream.info/api/block-height/' + tip);
const hash = await response2.text();
const response = await fetch(`http://127.0.0.1:8310/start/${entropy}/${tip}/${hash}`);
const text = await response.text();
return this._processResult(text);
}
private _processResult(text: string) {
const json = JSON.parse(text);
if (json.error) throw new Error(json.result);
return json.result;
}
async generate() {
const buf = await this.randomBytes(16);
this.secret = '' + bip39.entropyToMnemonic(buf.toString('hex'));
}
getEntropyHex() {
let ret = bip39.mnemonicToEntropy(this.secret.replace('', ''));
while (ret.length < 64) ret = '0' + ret;
return ret;
}
getSecret(): string {
return this.secret;
}
setSecret(secret) {
this.secret = secret;
}
async randomBytes(size): Promise<Buffer> {
return new Promise((resolve, reject) => {
crypto.randomBytes(size, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
async saveNetworkGraph() {
this.logToGeneralLog('saving network graph to disk...');
const response = await fetch('http://127.0.0.1:8310/savenetworkgraph');
const text = await response.text();
return this._processResult(text);
}
unwrapFirstExternalAddressFromMnemonics() {
if (!this.getSecret()) throw new Error('no secret');
const mnemonic = this.getSecret();
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = HDNode.fromSeed(seed);
const path = "m/84'/0'/0'/0/0";
const child = root.derivePath(path);
return bitcoin.payments.p2wpkh({
pubkey: child.publicKey,
}).address;
}
unwrapFirstExternalWifFromMnemonics() {
if (!this.getSecret()) throw new Error('no secret');
const mnemonic = this.getSecret();
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = HDNode.fromSeed(seed);
const path = "m/84'/0'/0'/0/0";
const child = root.derivePath(path);
return child.toWIF();
}
async reconnectPeers() {
const peers2reconnect = {};
let listPeers, listChannels;
try {
listPeers = await this.listPeers();
listChannels = await this.listChannels();
} catch (_) {
return;
}
// do we have any channels that need reconnection with peers..?
for (const channel of listChannels) {
if (!listPeers.includes(channel.counterparty_node_id)) peers2reconnect[channel.counterparty_node_id] = channel.counterparty_node_id;
}
// do we have any peers stored in file that must be connected..?
let storedPeers = [];
try {
const storedPeersTxt = localStorage.getItem(`peers.json`)
if (storedPeersTxt) storedPeers = JSON.parse(storedPeersTxt);
} catch (_) {}
for (const storedPeer of storedPeers) {
if (!listPeers.includes(storedPeer)) peers2reconnect[storedPeer] = storedPeer;
}
const peers2save = {};
// dumb dedup:
for (const peer of storedPeers.concat(listPeers).concat(Object.keys(peers2reconnect))) {
peers2save[peer] = peer;
}
localStorage.setItem(`peers.json`, JSON.stringify(Object.keys(peers2save)));
// finally. conencting to the ones that need connection:
for (const peer of Object.keys(peers2reconnect)) {
this.logToGeneralLog(`connecting to ${peer}`);
const details = await this.lookupNodeConnectionDetailsByPubkey(peer);
this.logToGeneralLog(`(${details.pubkey}@${details.host}:${details.port})`);
await this.connectPeer(details.pubkey, details.host , details.port);
}
}
public async connectPeer(pubkey, host, port) {
const response = await fetch(`http://127.0.0.1:8310/connectpeer/${pubkey}/${host}/${port}`);
const text = await response.text();
return this._processResult(text);
}
public async version() {
const response = await fetch('http://127.0.0.1:8310/version');
const text = await response.text();
return this._processResult(text);
}
public async ldkversion() {
const response = await fetch('http://127.0.0.1:8310/ldkversion');
const text = await response.text();
return this._processResult(text);
}
public async getMaturingBalance() {
const response = await fetch('http://127.0.0.1:8310/getmaturingbalance');
const text = await response.text();
return this._processResult(text);
}
public async getMaturingHeight() {
const response = await fetch('http://127.0.0.1:8310/getmaturingheight');
const text = await response.text();
return this._processResult(text);
}
async lookupNodeConnectionDetailsByPubkey(pubkey: string) {
// first, trying cache:
if (this._nodeConnectionDetailsCache[pubkey] && +new Date() - this._nodeConnectionDetailsCache[pubkey].ts < 4 * 7 * 24 * 3600 * 1000) {
// cache hit
return this._nodeConnectionDetailsCache[pubkey];
}
// doing actual fetch and filling cache:
const response = await fetch(`http://127.0.0.1:8310/node/${pubkey}`);
const json = await response.json();
if (json && json.addresses && Array.isArray(json.addresses)) {
for (const address of json.addresses) {
if (address.network === 'tcp') {
const ret = {
pubkey,
host: address.addr.split(':')[0],
port: parseInt(address.addr.split(':')[1]),
};
this._nodeConnectionDetailsCache[pubkey] = Object.assign({}, ret, { ts: +new Date() });
return ret;
}
}
}
}
async setRefundAddress(address: string) {
const script = bitcoin.address.toOutputScript(address);
const refundAddressScriptHex = script.toString('hex');
const response = await fetch(`http://127.0.0.1:8310/setrefundaddressscript/${refundAddressScriptHex}`);
const text = await response.text();
return this._processResult(text);
}
}

52
gui/classes/util.ts Normal file
View File

@ -0,0 +1,52 @@
const SHA256 = require('crypto-js/sha256');
const ENCHEX = require('crypto-js/enc-hex');
const ENCUTF8 = require('crypto-js/enc-utf8');
const AES = require('crypto-js/aes');
const ENCRYPTED_SEED = 'ENCRYPTED_SEED';
export default class Util {
encryptionMarker = 'encrypted://';
encryptionKey: string = '';
constructor(entropy: string) {
if (!entropy) throw new Error('entropy not provided');
this.encryptionKey = this.hashIt(this.hashIt('encryption' + entropy));
}
hashIt(arg: string) {
return ENCHEX.stringify(SHA256(arg));
}
encrypt(clearData: string): string {
return this.encryptionMarker + AES.encrypt(clearData, this.encryptionKey).toString();
}
decrypt(encryptedData: string | null, encryptionKey: string | null = null): string {
if (encryptedData === null) return '';
if (!encryptedData.startsWith(this.encryptionMarker)) return encryptedData;
const bytes = AES.decrypt(encryptedData.replace(this.encryptionMarker, ''), encryptionKey || this.encryptionKey);
return bytes.toString(ENCUTF8);
}
storeEncryptedSeed(encryptedSeed: string) {
return localStorage.setItem(ENCRYPTED_SEED, encryptedSeed); // cold
}
retrieveEncryptedSeed() {
return localStorage.getItem(ENCRYPTED_SEED); // cold
}
storeHotSeed(seed: string) {
window[ENCRYPTED_SEED] = seed;
}
getHotSeed() {
return window[ENCRYPTED_SEED];
}
isSeeded() {
return !!localStorage.getItem(ENCRYPTED_SEED);
}
}

34
gui/components/layout.tsx Normal file
View File

@ -0,0 +1,34 @@
import Head from 'next/head';
const name = 'Hello, Lightning!';
export const siteTitle = 'Hello, Lightning!';
export default function Layout({
children,
}: {
children: React.ReactNode
index?: boolean,
}) {
return (
<div>
<Head>
<link rel="icon" type="image/png" href="/favicon.png" />
<meta
name="description"
content={name}
/>
<meta
property="og:image"
content={`https://og-image.vercel.app/${encodeURI(
siteTitle,
)}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.zeit.co%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
/>
<meta name="og:title" content={siteTitle} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<main>{children}</main>
</div>
);
}

BIN
gui/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

View File

@ -0,0 +1,5 @@
import { HelloScreen } from './helloScreen';
export interface DefaultScreenProps {
changeScreen: (newScreen: HelloScreen) => void;
}

View File

@ -0,0 +1,7 @@
export enum HelloScreen {
Gsom,
NoWalletDetected,
CreateWriteDownSeed,
CreateChooseEncryptPassword,
UnlockWallet,
}

2
gui/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

4
gui/next.config.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
basePath: '/gui',
assetPrefix: '/gui/',
}

45
gui/pack2kotlin.js Normal file
View File

@ -0,0 +1,45 @@
const { resolve } = require('path');
const { readdir } = require('fs').promises;
const fs = require('fs');
async function getFiles(dir) {
const dirents = await readdir(dir, { withFileTypes: true });
const files = await Promise.all(dirents.map((dirent) => {
const res = resolve(dir, dirent.name);
return dirent.isDirectory() ? getFiles(res) : res;
}));
return Array.prototype.concat(...files);
}
fs.writeFileSync('StaticFiles.kt', '/* AUTOGENERATED. DO NOT EDIT. */ \n\n' +
'var files: HashMap<String, String> = HashMap<String, String>()\n' +
'\n' +
'\n' +
'class StaticFiles {\n' +
' fun getHex(key: String): String {\n' +
' return files.getOrDefault(key, "")\n' +
' }\n\n' +
' constructor() {\n');
console.error(__dirname);
getFiles(__dirname + '/gui')
.then(files => {
for (const file of files) {
const key = file.replace(__dirname, '');
if (!(key.endsWith('.js') || key.endsWith('.css') || key.endsWith('.html'))) continue;
console.log(key);
const hex = fs.readFileSync(file).toString('hex');
// console.log(hex);
fs.appendFileSync('StaticFiles.kt', ` files.put("${key}", "${hex}")\n`);
}
fs.appendFileSync('StaticFiles.kt', ' }\n' + '}\n');
process.exit(0);
})
.catch(e => {
console.error(e);
process.exit(1);
});

9233
gui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
gui/package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "hellolightninggui",
"version": "0.0.0",
"scripts": {
"dev": "next dev",
"tslint": "tslint --fix -p . -c tslint.json ",
"build": "next build",
"pack2kotlin": "node pack2kotlin.js && mv StaticFiles.kt ../src/main/kotlin/",
"export": "rm -r -f .next; rm -r -f _next; next build && next export -o gui && npm run pack2kotlin",
"start": "next start"
},
"devDependencies": {
"@types/react": "^17.0.8",
"bootstrap": "^5.0.0-beta3",
"next": "^10.0.0",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-preset-env": "^6.7.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"swr": "^0.5.6",
"tslint": "^6.1.3",
"tslint-config-airbnb": "^5.11.2",
"tslint-react-hooks": "^2.2.2",
"typescript": "^4.2.4"
},
"dependencies": {
"bip32": "2.0.6",
"bip39": "^3.0.4",
"bitcoinjs-lib": "^6.0.1",
"cross-fetch": "^3.1.4",
"crypto-js": "^4.1.1"
}
}

6
gui/pages/_app.tsx Normal file
View File

@ -0,0 +1,6 @@
import 'bootstrap/dist/css/bootstrap.css';
import { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}

53
gui/pages/index.tsx Normal file
View File

@ -0,0 +1,53 @@
import Head from 'next/head';
import Layout, {siteTitle} from '../components/layout';
import Gsom from '../screens/gsom';
import NoWalletDetected from '../screens/noWalletDetected';
import {HelloScreen} from '../models/helloScreen';
import {useEffect, useState} from 'react';
import CreateWriteSeedDown from '../screens/createWriteSeedDown';
import Util from '../classes/util';
import UnlockWallet from '../screens/unlockWallet';
export default function Index() {
console.log('rendering Index');
const [screen, setScreen] = useState<HelloScreen>(HelloScreen.NoWalletDetected);
useEffect(() => {
const ut = new Util('dummy');
if (ut.isSeeded()) {
if (!ut.getHotSeed())
setScreen(HelloScreen.UnlockWallet);
else
setScreen(HelloScreen.Gsom);
} else {
setScreen(HelloScreen.NoWalletDetected);
}
}, []);
const renderScreen = () => {
console.log('currentScreen = ', screen);
switch (screen) {
case HelloScreen.Gsom: return (<Gsom changeScreen={setScreen}/>);
case HelloScreen.NoWalletDetected: return (<NoWalletDetected changeScreen={setScreen}/>);
case HelloScreen.CreateWriteDownSeed: return (<CreateWriteSeedDown changeScreen={setScreen}/>);
case HelloScreen.UnlockWallet: return (<UnlockWallet changeScreen={setScreen}/>);
default:
console.warn('default', screen);
return (<Gsom changeScreen={setScreen}/>);
}
};
return (
<Layout index>
<Head>
<title>{siteTitle}</title>
</Head>
<div className="d-flex flex-column min-vh-100 justify-content-center align-items-center">
<h1>👋 </h1>
{renderScreen()}
</div>
</Layout>
);
}

17
gui/postcss.config.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = {
plugins: [
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009'
},
stage: 3,
features: {
'custom-properties': false
}
}
]
]
}

BIN
gui/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

View File

@ -0,0 +1,60 @@
import {HelloScreen} from '../models/helloScreen';
import {DefaultScreenProps} from '../models/defaultScreenProps';
import Ldk from '../classes/ldk';
import {useEffect, useState} from 'react';
import Util from '../classes/util';
export default function CreateWriteSeedDown(props: DefaultScreenProps) {
const [seed, setSeed] = useState('');
useEffect(() => {
(async () => {
const ldk = new Ldk();
await ldk.generate();
setSeed(ldk.getSecret());
})();
}, []);
return (
<span>
<div className="row">
<div className="col">
<h2>Write down seed words:</h2>
</div>
</div>
<div className="row">
<div className="col">
{seed}
</div>
</div>
<div className="row">
<div className="col">
<button type="button" className="btn btn-primary" onClick={async () => {
let password = '';
for (;;) {
const pass1 = prompt("Please enter your payment password", "");
const pass2 = prompt("Please repeat your password", "");
if (pass1 === pass2 && pass1 != null) {
password = pass1;
break;
}
}
const ut = new Util(password);
const encryptedSeed = ut.encrypt(seed)
alert("Wallet successfully seeded!");
ut.storeHotSeed(seed);
ut.storeEncryptedSeed(encryptedSeed)
props.changeScreen(HelloScreen.Gsom);
}}>next</button>
</div>
</div>
</span>
);
}

160
gui/screens/gsom.tsx Normal file
View File

@ -0,0 +1,160 @@
import useSWR from 'swr';
import {DefaultScreenProps} from '../models/defaultScreenProps';
import Ldk from '../classes/ldk';
import Util from '../classes/util';
let lastBlockchainSync = 0;
let lastNetworkGraphSaved = +new Date();
let lastPeersReconnect = 0;
let maturingBalance = 0;
let maturingHeight = 0;
const fetcher = async (arg1) => {
const ldk = new Ldk();
const util = new Util('dummy');
ldk.setSecret(util.getHotSeed());
if (+new Date() - lastBlockchainSync > 5 * 60 * 1000) { // 5 min
console.log('syncing blockchain...')
lastBlockchainSync = +new Date();
await ldk.setRefundAddress(ldk.unwrapFirstExternalAddressFromMnemonics());
maturingBalance = await ldk.getMaturingBalance();
maturingHeight = await ldk.getMaturingHeight();
ldk.checkBlockchain(); // let it run in the background
await ldk.broadcastTxsIfNecessary();
}
if (+new Date() - lastNetworkGraphSaved > 1 * 60 * 1000) {
lastNetworkGraphSaved = +new Date();
ldk.saveNetworkGraph(); // let it run in the background
}
if (+new Date() - lastPeersReconnect > 0.5 * 60 * 1000) {
lastPeersReconnect = +new Date();
ldk.reconnectPeers(); // let it run in the background
}
const uri = `http://localhost:8310/${arg1}`;
try {
const res = await fetch(uri);
const json = await res.json();
if (json && json.result && !json.error) return json.result;
} catch (_) {
return null;
}
};
export default function Gsom(props: DefaultScreenProps) {
const { data: listpeers }: { data?: any, error?: any } = useSWR('listpeers', fetcher, { refreshInterval: 2 * 1000, refreshWhenHidden: true, refreshWhenOffline: true });
const { data: listchannels }: { data?: any, error?: any } = useSWR('listchannels', fetcher, { refreshInterval: 5 * 1000, refreshWhenHidden: true, refreshWhenOffline: true });
const { data: getnodeid }: { data?: any, error?: any } = useSWR('getnodeid', fetcher, { refreshInterval: 5 * 1000, refreshWhenHidden: true, refreshWhenOffline: true });
if (!getnodeid) {
const ldk = new Ldk();
const util = new Util('dummy');
ldk.setSecret(util.getHotSeed())
console.log();
;(async () => {
try {
await ldk.setRefundAddress(ldk.unwrapFirstExternalAddressFromMnemonics());
await ldk.updateFeerate(); // so any refund claim upon startup would use adequate fee
await ldk.start(ldk.getEntropyHex());
} catch (error) {
console.error(error.message);
}
})();
}
const renderPeersList = () => {
const listItems = (listpeers || []).map((number) =>
<li key={number}>{number}</li>
);
return (
<ul>{listItems}</ul>
);
};
const renderChannelsList = () => {
const listItems = (listchannels || []).map((cha) =>
<li key={cha.channel_id}>{cha?.counterparty_node_id || '?'} {cha?.is_usable ? '[usable]' : ''} {cha?.is_funding_locked ? '' : '[funding not locked]'}</li>
);
return (
<ul>{listItems}</ul>
);
};
return (
<div className="container">
<div className="row">
<div className="col">
<div className="row">
<div className="col">
Total balance<br/>
0 sat<br/>
$0.00<br/>
</div>
<div className="col">
<button type="button" className="btn btn-primary">send</button>
</div>
<div className="col">
<button type="button" className="btn btn-primary">receive</button>
</div>
</div>
</div>
<div className="col">
channels:<br/>
{renderChannelsList()}
Peers:<br/>
{renderPeersList()}
<button type="button" className="btn btn-outline-primary">manage channels</button><br/>
<button type="button" className="btn btn-outline-primary" onClick={async () => {
const uri = prompt('input node uri');
if (!uri) return;
const pubkey = uri.split('@')[0];
const [host, port] = uri.split('@')[1]?.split(':')
if (!pubkey || !host || !port) return;
const ldk = new Ldk();
await ldk.connectPeer(pubkey, host, port);
}}>connect peer</button><br/>
</div>
</div>
</div>
);
}
/*
<div>
{listpeers ? (
<div style={{ fontSize: 20 }}>
<span>Peers: {JSON.stringify(listpeers)}</span>
</div>
) : null}
{listchannels ? (
<div style={{ fontSize: 20 }}>
<span>Channels: {JSON.stringify(listchannels)}</span>
</div>
) : null}
{getnodeid ? (
<div style={{ fontSize: 20 }}>
<span>Node id: {JSON.stringify(getnodeid)}</span>
</div>
) : (<span>not started..?</span>)}
<br/>
</div>
*/

View File

@ -0,0 +1,32 @@
import { HelloScreen } from '../models/helloScreen';
import { DefaultScreenProps } from '../models/defaultScreenProps';
export default function NoWalletDetected(props: DefaultScreenProps) {
return (
<div>
<div className="row">
<div className="col">
<h2>No wallet detected</h2>
</div>
</div>
<div className="row">
<div className="col">
<button type="button" className="btn btn-primary" onClick={async () => {
props.changeScreen(HelloScreen.CreateWriteDownSeed);
}}>Create</button>
</div>
</div>
<div className="row">
<div className="col">
<button type="button" className="btn light" onClick={() => {
alert('todo');
}}>Restore from seed</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,34 @@
import useSWR from 'swr';
import { DefaultScreenProps } from '../models/defaultScreenProps';
import Util from '../classes/util';
import {HelloScreen} from '../models/helloScreen';
export default function UnlockWallet(props: DefaultScreenProps) {
return (
<div>
Wallet is locked, please unlock it
<br/>
<button type="button" className="btn btn-primary" onClick={async () => {
let password = '';
for (;;) {
password = prompt("Please enter your payment password", "");
if (password) {
const ut = new Util(password);
try {
const decrypted = ut.decrypt(ut.retrieveEncryptedSeed())
if (decrypted) {
ut.storeHotSeed(decrypted);
props.changeScreen(HelloScreen.Gsom);
break;
} else {
alert('incorrect password');
}
} catch (error) {
alert("Incorrect password: " + error.message);
}
}
}
}}>next</button>
</div>
);
}

29
gui/tsconfig.json Normal file
View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}

22
gui/tslint.json Normal file
View File

@ -0,0 +1,22 @@
{
"extends": ["tslint-config-airbnb", "tslint-react-hooks"],
"rules": {
"import-name": false,
"ter-arrow-parens": false,
"align": false,
"max-line-length": [true, 240],
"function-name": [
true,
{
"function-regex": "^[a-zA-Z$][\\w\\d]+$",
"method-regex": "^[a-z$][\\w\\d]+$",
"private-method-regex": "^[a-z$][\\w\\d]+$",
"protected-method-regex": "^[a-z$][\\w\\d]+$",
"static-method-regex": "^[a-z$][\\w\\d]+$"
}
],
"variable-name": {
"options": ["ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"]
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -1,2 +0,0 @@
curl http://127.0.0.1:8310/openchannelstep1/030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f/500000

View File

@ -8,6 +8,7 @@ class ClientHandler(client: Socket) {
private val reader: Scanner = Scanner(client.getInputStream())
private val writer: OutputStream = client.getOutputStream()
private val executor: Executor = Executor()
private val server: Server = Server()
private var running: Boolean = false
fun run() {
@ -28,9 +29,20 @@ class ClientHandler(client: Socket) {
continue;
}
if (text.startsWith("GET /gui")) {
val resp = server.serve(text);
write(resp)
shutdown();
continue;
}
var corsHeader = "access-control-allow-origin: http://localhost:8310\n"
if (ARG_DISABLE_CORS) corsHeader = "access-control-allow-origin: *\n"
if (text.startsWith("GET /")) {
println(text);
write("HTTP/1.0 200 OK\n" +
corsHeader +
"Content-type: text/html; charset=UTF-8\n")
val text2 = text.split(' ')
val values = text2[1].split('/')
@ -59,8 +71,7 @@ class ClientHandler(client: Socket) {
// TODO: Implement exception handling
println("Exception handling '" + text + "': ")
println(ex)
write("Exception handling '" + text + "': ")
write(ex.toString())
write(helperJsonResponseFailure("Exception handling '" + text + "': " + ex.toString()))
shutdown()
} finally {
@ -76,7 +87,7 @@ class ClientHandler(client: Socket) {
private fun shutdown() {
running = false
client.close()
println("${client.inetAddress.hostAddress} closed the connection")
// println("${client.inetAddress.hostAddress} closed the connection")
}
}

View File

@ -1,4 +1,5 @@
import java.io.File
import java.net.URL
import kotlin.system.exitProcess
class Executor {
@ -9,6 +10,17 @@ class Executor {
"start" -> {
if (arg1 == null || arg2 == null || arg3 == null) return helperJsonResponseFailure("incorrect arguments")
println("starting LDK... using " + arg1 + " " + arg2 + " " + arg3)
if (started) return helperJsonResponseFailure("already started")
started = true;
homedir += "/" + sha256(sha256(arg1)).substring(0, 8);
println("using " + homedir)
val directory = File(homedir)
if (!directory.exists()) {
directory.mkdir()
}
var serializedChannelManager = ""
var serializedMonitors = ""
var monitors = arrayOf<String>()
@ -32,7 +44,7 @@ class Executor {
return helperJsonResponseSuccess("ok")
}
"ldkversion" -> return helperJsonResponseSuccess((org.ldk.impl.version.get_ldk_java_bindings_version() + ", " + org.ldk.impl.bindings.get_ldk_c_bindings_version() + ", " + org.ldk.impl.bindings.get_ldk_version()))
"version" -> return "1.0.0"
"version" -> return helperJsonResponseSuccess("1.2.3")
"connectpeer" -> {
if (arg1 == null || arg2 == null || arg3 == null) return helperJsonResponseFailure("incorrect arguments")
var retValue = "";
@ -166,6 +178,11 @@ class Executor {
eventsPaymentPathFailed = arrayOf<String>()
return helperJsonResponseSuccess(ret)
}
"geteventspaymentfailed" -> {
val ret = eventsPaymentFailed.joinToString(separator = ",", prefix = "[", postfix = "]")
eventsPaymentFailed = arrayOf<String>()
return helperJsonResponseSuccess(ret)
}
"geteventspaymentreceived" -> {
val ret = eventsPaymentReceived.joinToString(separator = ",", prefix = "[", postfix = "]")
eventsPaymentReceived = arrayOf<String>()
@ -267,6 +284,11 @@ class Executor {
})
return retValue
}
"node" -> {
if (arg1 == null) return helperJsonResponseFailure("incorrect arguments")
val resp = URL("https://1ml.com/node/" + arg1 + "/json").readText()
return resp;
}
"transactionconfirmed" -> {
if (arg1 == null || arg2 == null || arg3 == null || arg4 == null) return helperJsonResponseFailure("incorrect arguments")
var retValue = ""

View File

@ -2,9 +2,15 @@ import org.ldk.batteries.ChannelManagerConstructor
import org.ldk.batteries.NioPeerHandler
import org.ldk.structs.*
import java.io.File
import java.net.InetAddress
import java.net.ServerSocket
import kotlin.concurrent.thread
// Globals. Ugly, but ok
var ARG_DISABLE_CORS = false;
var ARG_NO_DISPLAY = false;
var started = false;
var homedir = ""
val prefix_channel_monitor = "channel_monitor_"
val prefix_channel_manager = "channel_manager"
@ -34,11 +40,22 @@ var eventsRegisterOutput: Array<String> = arrayOf<String>()
var eventsTxBroadcast: Array<String> = arrayOf<String>()
var eventsPaymentSent: Array<String> = arrayOf<String>()
var eventsPaymentPathFailed: Array<String> = arrayOf<String>()
var eventsPaymentFailed: Array<String> = arrayOf<String>()
var eventsPaymentReceived: Array<String> = arrayOf<String>()
var eventsPaymentForwarded: Array<String> = arrayOf<String>()
fun main(args: Array<String>) {
println("Hello Lightning!")
args.iterator().forEach {
if (it == "--disable-cors" || it == "--no-cors") {
ARG_DISABLE_CORS = true
println("CORS disabled")
}
if (it == "--no-display") {
ARG_NO_DISPLAY = true
println("no display")
}
}
homedir = System.getProperty("user.home") + "/.hellolightning";
println("using " + homedir)
@ -47,12 +64,13 @@ fun main(args: Array<String>) {
directory.mkdir()
}
val server = ServerSocket(8310)
val server = ServerSocket(8310, 0, InetAddress.getLoopbackAddress())
println("Server is running on port ${server.localPort}")
if (!ARG_NO_DISPLAY) openInBrowser("http://localhost:8310/gui/");
while (true) {
val client = server.accept()
println("Client connected: ${client.inetAddress.hostAddress}")
// println("Client connected: ${client.inetAddress.hostAddress}")
// Run client in it's own thread.
thread { ClientHandler(client).run() }
}

View File

@ -47,9 +47,9 @@ fun getNodeId(promise: Promise) {
}
fun setFeerate(newFeerateFast: Int, newFeerateMedium: Int, newFeerateSlow: Int, promise: Promise) {
if (newFeerateFast < 300) return promise.reject("newFeerateFast is too small");
if (newFeerateMedium < 300) return promise.reject("newFeerateMedium is too small");
if (newFeerateSlow < 300) return promise.reject("newFeerateSlow is too small");
if (newFeerateFast < 253) return promise.reject("newFeerateFast is too small");
if (newFeerateMedium < 253) return promise.reject("newFeerateMedium is too small");
if (newFeerateSlow < 253) return promise.reject("newFeerateSlow is too small");
feerate_fast = newFeerateFast;
feerate_medium = newFeerateMedium;
feerate_slow = newFeerateSlow;
@ -89,7 +89,7 @@ fun channel2channelObject(it: ChannelDetails): String {
channelObject += "\"is_public\":" + it._is_public + ",";
val fundingTxoTxid = it._funding_txo?._txid;
if (fundingTxoTxid is ByteArray) {
channelObject += "\"funding_txo_txid\":" + "\"" + byteArrayToHex(fundingTxoTxid) + "\",";
channelObject += "\"funding_txo_txid\":" + "\"" + byteArrayToHex(fundingTxoTxid.reversedArray()) + "\",";
}
val fundingTxoIndex = it._funding_txo?._index;
if (fundingTxoIndex != null) {

View File

@ -53,9 +53,10 @@ fun handleEvent(event: Event) {
if (txResult is Result_TransactionNoneZ.Result_TransactionNoneZ_OK) {
// success building the transaction, passing it to outer code to broadcast
// val params = Arguments.createMap();
// params.putString("txhex", byteArrayToHex(txResult.res))
// this.sendEvent(MARKER_BROADCAST, params)
val params = WritableMap();
params.putString("txhex", byteArrayToHex(txResult.res))
storeEvent("$homedir/events_tx_broadcast", params)
eventsTxBroadcast = eventsTxBroadcast.plus(params.toString())
}
}
@ -67,8 +68,12 @@ fun handleEvent(event: Event) {
eventsPaymentSent = eventsPaymentSent.plus(params.toString())
}
if (event is Event.PaymentPathSuccessful) {
println("ReactNativeLDK: " + "payment path successful");
}
if (event is Event.PaymentPathFailed) {
println("ReactNativeLDK: " + "payment failed, payment_hash: " + byteArrayToHex(event.payment_hash));
println("ReactNativeLDK: " + "payment path failed, payment_hash: " + byteArrayToHex(event.payment_hash));
val params = WritableMap()
params.putString("payment_hash", byteArrayToHex(event.payment_hash));
params.putString("rejected_by_dest", event.rejected_by_dest.toString());
@ -76,6 +81,15 @@ fun handleEvent(event: Event) {
eventsPaymentPathFailed = eventsPaymentPathFailed.plus(params.toString())
}
if (event is Event.PaymentFailed) {
println("ReactNativeLDK: " + "payment failed, payment_hash: " + byteArrayToHex(event.payment_hash));
val params = WritableMap()
params.putString("payment_hash", byteArrayToHex(event.payment_hash));
params.putString("payment_id", byteArrayToHex(event.payment_id));
storeEvent("$homedir/events_payment_failed", params)
eventsPaymentFailed = eventsPaymentFailed.plus(params.toString())
}
if (event is Event.PaymentReceived) {
println("ReactNativeLDK: " + "payment received, payment_hash: " + byteArrayToHex(event.payment_hash));
var paymentPreimage: ByteArray? = null;
@ -211,8 +225,10 @@ fun start(
// INITIALIZE THE LOGGER #######################################################################
// What it's used for: LDK logging
val logger = Logger.new_impl { arg: String? ->
println("ReactNativeLDK: " + arg)
val logger = Logger.new_impl { arg: Record ->
if (arg._level == org.ldk.enums.Level.LDKLevel_Gossip) return@new_impl;
if (arg._level == org.ldk.enums.Level.LDKLevel_Trace) return@new_impl;
println("ReactNativeLDK: " + arg._args)
// val params = Arguments.createMap()
// params.putString("line", arg)
// sendEvent(MARKER_LOG, params)
@ -273,7 +289,7 @@ fun start(
override fun register_tx(txid: ByteArray, script_pubkey: ByteArray) {
println("ReactNativeLDK: register_tx");
val params = WritableMap()
params.putString("txid", byteArrayToHex(txid))
params.putString("txid", byteArrayToHex(txid.reversedArray()))
params.putString("script_pubkey", byteArrayToHex(script_pubkey))
storeEvent("$homedir/events_register_tx", params)
eventsRegisterTx = eventsRegisterTx.plus(params.toString())
@ -349,17 +365,33 @@ fun start(
router = NetworkGraph.of(hexStringToByteArray("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f").reversedArray())
}
val route_handler = NetGraphMsgHandler.of(
/*val route_handler = NetGraphMsgHandler.of(
router,
Option_AccessZ.none(),
logger
)
)*/
// INITIALIZE THE CHANNELMANAGER ###############################################################
// What it's used for: managing channel state
val scorer = LockableScore.of(Scorer.with_default().as_Score())
val scorer = MultiThreadedLockableScore.of(Scorer.with_default().as_Score())
// this is gona be fee policy for __incoming__ channels. they are set upfront globally:
val uc = UserConfig.with_default()
val newChannelConfig = ChannelConfig.with_default()
newChannelConfig.set_forwarding_fee_proportional_millionths(10000);
newChannelConfig.set_forwarding_fee_base_msat(1000);
val handshake = ChannelHandshakeConfig.with_default();
handshake.set_minimum_depth(1);
uc.set_own_channel_config(handshake);
uc.set_channel_options(newChannelConfig);
val newLim = ChannelHandshakeLimits.with_default()
newLim.set_force_announced_channel_preference(false)
uc.set_peer_channel_config_limits(newLim)
//
try {
if (serializedChannelManagerHex != "") {
@ -367,6 +399,7 @@ fun start(
channel_manager_constructor = ChannelManagerConstructor(
hexStringToByteArray(serializedChannelManagerHex),
channelMonitors,
uc,
keys_manager?.as_KeysInterface(),
fee_estimator,
chain_monitor,
@ -381,17 +414,6 @@ fun start(
nio_peer_handler = channel_manager_constructor!!.nio_peer_handler;
} else {
// fresh start
// this is gona be fee policy for __incoming__ channels. they are set upfront globally:
val uc = UserConfig.with_default()
val newChannelConfig = ChannelConfig.with_default()
newChannelConfig.set_forwarding_fee_proportional_millionths(10000);
newChannelConfig.set_forwarding_fee_base_msat(1000);
uc.set_channel_options(newChannelConfig);
val newLim = ChannelHandshakeLimits.with_default()
newLim.set_force_announced_channel_preference(false)
uc.set_peer_channel_config_limits(newLim)
//
channel_manager_constructor = ChannelManagerConstructor(
Network.LDKNetwork_Bitcoin,
uc,

43
src/main/kotlin/Server.kt Normal file
View File

@ -0,0 +1,43 @@
val staticFiles = StaticFiles();
class Server {
fun serve(text: String): String {
println(text);
var ret = "";
val text2 = text.split(' ')
val values = text2[1].split('/')
var file2serve = "";
if (values.elementAtOrNull(2) == null || values.elementAtOrNull(2) == "") {
file2serve = "/index.html"; // default
} else {
for (i in 2..6) {
if (values.elementAtOrNull(i) == null) break;
file2serve += "/" + values[i] // adding directories to path
if (file2serve.endsWith(".js") || file2serve.endsWith(".css") || file2serve.endsWith(".html") || file2serve.endsWith(".png")) break;
}
}
// now, got a filename to look for. lets look in bundled files:
val hex = staticFiles.getHex("/gui" + file2serve);
if (hex != "") {
val bar = hexStringToByteArray(hex).toString(Charsets.UTF_8)
if (file2serve.endsWith(".css")) {
ret += "HTTP/1.0 200 OK\n" + "Content-type: text/css; charset=UTF-8\n\n";
} else if (file2serve.endsWith(".js")) {
ret += "HTTP/1.0 200 OK\n" + "Content-type: application/javascript; charset=UTF-8\n\n";
} else {
ret += "HTTP/1.0 200 OK\n" + "Content-type: text/html; charset=UTF-8\n\n";
}
// TODO: png and other binaries
ret += bar;
return ret;
}
// not in bundled files
return "HTTP/1.0 404 OK\n" + "Content-type: text/html; charset=UTF-8\n\n" + "does not exist: " + file2serve;
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,9 @@
import java.awt.Desktop
import java.io.File
import java.math.BigInteger
import java.net.URI
import java.security.MessageDigest
import java.util.*
fun hexStringToByteArray(strArg: String): ByteArray {
val HEX_CHARS = "0123456789ABCDEF"
@ -42,4 +47,20 @@ fun storeEvent(eventsPath: String, params: WritableMap) {
}
File(eventsPath + "/" + System.currentTimeMillis() + ".json").writeText(params.toString())
}
fun sha256(input:String): String {
val md = MessageDigest.getInstance("SHA-256")
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
}
fun openInBrowser(uri: String) {
val osName by lazy(LazyThreadSafetyMode.NONE) { System.getProperty("os.name").lowercase(Locale.getDefault()) }
val desktop = Desktop.getDesktop()
when {
Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.BROWSE) -> desktop.browse(URI(uri))
"mac" in osName -> Runtime.getRuntime().exec("open $uri")
"nix" in osName || "nux" in osName -> Runtime.getRuntime().exec("xdg-open $uri")
else -> throw RuntimeException("cannot open $uri")
}
}