REF: 104
This commit is contained in:
parent
53181dabd6
commit
80882bf984
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -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
2
.idea/misc.xml
generated
@ -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>
|
||||
465
gui/classes/ldk.ts
Normal file
465
gui/classes/ldk.ts
Normal file
@ -0,0 +1,465 @@
|
||||
/* 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]);
|
||||
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 = {};
|
||||
|
||||
const listPeers = await this.listPeers();
|
||||
const listChannels = await this.listChannels();
|
||||
|
||||
// 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
52
gui/classes/util.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
5
gui/models/defaultScreenProps.ts
Normal file
5
gui/models/defaultScreenProps.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { HelloScreen } from './helloScreen';
|
||||
|
||||
export interface DefaultScreenProps {
|
||||
changeScreen: (newScreen: HelloScreen) => void;
|
||||
}
|
||||
7
gui/models/helloScreen.ts
Normal file
7
gui/models/helloScreen.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export enum HelloScreen {
|
||||
Gsom,
|
||||
NoWalletDetected,
|
||||
CreateWriteDownSeed,
|
||||
CreateChooseEncryptPassword,
|
||||
UnlockWallet,
|
||||
}
|
||||
5409
gui/package-lock.json
generated
5409
gui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,8 +5,8 @@
|
||||
"dev": "next dev",
|
||||
"tslint": "tslint --fix -p . -c tslint.json ",
|
||||
"build": "next build",
|
||||
"export": "rm -r -f .next; rm -r -f _next; next build && next export -o gui; npm run pack2kotlin",
|
||||
"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": {
|
||||
@ -23,5 +23,11 @@
|
||||
"tslint-react-hooks": "^2.2.2",
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
"dependencies": {}
|
||||
"dependencies": {
|
||||
"bip32": "2.0.6",
|
||||
"bip39": "^3.0.4",
|
||||
"bitcoinjs-lib": "^6.0.1",
|
||||
"cross-fetch": "^3.1.4",
|
||||
"crypto-js": "^4.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +1,41 @@
|
||||
import Head from 'next/head';
|
||||
import Layout, { siteTitle } from '../components/layout';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const fetcher = async (arg1) => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
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() {
|
||||
const { data: listpeers }: { data?: any, error?: any } = useSWR('listpeers', fetcher, { refreshInterval: 5 * 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 });
|
||||
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>
|
||||
@ -25,23 +44,10 @@ export default function Index() {
|
||||
</Head>
|
||||
|
||||
<div className="d-flex flex-column min-vh-100 justify-content-center align-items-center">
|
||||
{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/>
|
||||
<h1>👋 ⚡️</h1>
|
||||
{renderScreen()}
|
||||
</div>
|
||||
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
60
gui/screens/createWriteSeedDown.tsx
Normal file
60
gui/screens/createWriteSeedDown.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
146
gui/screens/gsom.tsx
Normal file
146
gui/screens/gsom.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
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
|
||||
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 () => {
|
||||
await ldk.setRefundAddress(ldk.unwrapFirstExternalAddressFromMnemonics());
|
||||
await ldk.updateFeerate(); // so any refund claim upon startup would use adequate fee
|
||||
await ldk.start(ldk.getEntropyHex());
|
||||
})();
|
||||
}
|
||||
|
||||
const renderPeersList = () => {
|
||||
const listItems = (listpeers || []).map((number) =>
|
||||
<li key={number}>{number}</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/>
|
||||
todo<br/>
|
||||
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>
|
||||
|
||||
*/
|
||||
32
gui/screens/noWalletDetected.tsx
Normal file
32
gui/screens/noWalletDetected.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
34
gui/screens/unlockWallet.tsx
Normal file
34
gui/screens/unlockWallet.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@ -1,4 +1,5 @@
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class Executor {
|
||||
@ -40,7 +41,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.1.3")
|
||||
"version" -> return helperJsonResponseSuccess("1.2.0")
|
||||
"connectpeer" -> {
|
||||
if (arg1 == null || arg2 == null || arg3 == null) return helperJsonResponseFailure("incorrect arguments")
|
||||
var retValue = "";
|
||||
@ -174,6 +175,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>()
|
||||
@ -275,6 +281,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 = ""
|
||||
|
||||
@ -39,13 +39,14 @@ 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") {
|
||||
if (it == "--disable-cors" || it == "--no-cors") {
|
||||
ARG_DISABLE_CORS = true
|
||||
println("CORS disabled")
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -213,6 +226,7 @@ fun start(
|
||||
// INITIALIZE THE LOGGER #######################################################################
|
||||
// What it's used for: LDK logging
|
||||
val logger = Logger.new_impl { arg: Record ->
|
||||
if (arg._level == org.ldk.enums.Level.LDKLevel_Gossip) return@new_impl;
|
||||
println("ReactNativeLDK: " + arg._args)
|
||||
// val params = Arguments.createMap()
|
||||
// params.putString("line", arg)
|
||||
@ -360,13 +374,18 @@ fun start(
|
||||
// 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)
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user