Compare commits

..

17 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
39 changed files with 10535 additions and 36 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,3 +1,5 @@
status: alpha (not ready for production)
# Hello, Lightning!
Cli lightning network server, based on LDK (rust-lightning).

View File

@ -70,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));
}
@ -90,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);

View File

@ -1,2 +1,2 @@
curl http://127.0.0.1:8310/openchannelstep1/030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f/500000
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

@ -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 helperJsonResponseSuccess("1.0.3")
"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

@ -68,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());
@ -77,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;
@ -212,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)
@ -274,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())
@ -350,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 != "") {
@ -368,6 +399,7 @@ fun start(
channel_manager_constructor = ChannelManagerConstructor(
hexStringToByteArray(serializedChannelManagerHex),
channelMonitors,
uc,
keys_manager?.as_KeysInterface(),
fee_estimator,
chain_monitor,
@ -382,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")
}
}