ADD: basic cli control (nodejs)

This commit is contained in:
Overtorment 2021-11-12 19:51:02 +00:00
parent e112c943ab
commit d059a18299
No known key found for this signature in database
GPG Key ID: AB15F43F78CCBC06
13 changed files with 4442 additions and 0 deletions

15
cli/.editorconfig Normal file
View File

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
[*.{js,jsx,json,ts,tsx,yml}]
indent_size = 2
indent_style = space

1
cli/.eslintignore Normal file
View File

@ -0,0 +1 @@
/**/*.js

21
cli/.eslintrc.json Normal file
View File

@ -0,0 +1,21 @@
{
"env": {
"browser": false,
"es6": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "jest"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:jest/recommended",
"prettier"
],
"rules": {}
}

29
cli/.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
# Logs
logs
*.log
npm-debug.log*
# Dependencies
node_modules/
# Coverage
coverage
# Transpiled files
build/
# VS Code
.vscode
!.vscode/tasks.js
# JetBrains IDEs
.idea/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Misc
.DS_Store

12
cli/.prettierrc Normal file
View File

@ -0,0 +1,12 @@
{
"singleQuote": true,
"trailingComma": "all",
"overrides": [
{
"files": "*.ts",
"options": {
"parser": "typescript"
}
}
]
}

View File

@ -0,0 +1,42 @@
import { Delays, greeter } from '../src/main';
describe('greeter function', () => {
const name = 'John';
let hello: string;
let timeoutSpy: jest.SpyInstance;
// Act before assertions
beforeAll(async () => {
// Read more about fake timers
// http://facebook.github.io/jest/docs/en/timer-mocks.html#content
// Jest 27 now uses "modern" implementation of fake timers
// https://jestjs.io/blog/2021/05/25/jest-27#flipping-defaults
// https://github.com/facebook/jest/pull/5171
jest.useFakeTimers();
timeoutSpy = jest.spyOn(global, 'setTimeout');
const p: Promise<string> = greeter(name);
jest.runOnlyPendingTimers();
hello = await p;
});
// Teardown (cleanup) after assertions
afterAll(() => {
timeoutSpy.mockRestore();
});
// Assert if setTimeout was called properly
it('delays the greeting by 2 seconds', () => {
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(
expect.any(Function),
Delays.Long,
);
});
// Assert greeter result
it('greets a user with `Hello, {name}` message', () => {
expect(hello).toBe(`Hello, ${name}`);
});
});

20
cli/jest.config.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
testEnvironment: 'node',
transform: {
"^.+\\.tsx?$": "ts-jest"
},
moduleFileExtensions: [
"ts",
"tsx",
"js",
"jsx",
"json",
"node",
],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(ts|js)x?$',
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.{ts,tsx,js,jsx}',
'!src/**/*.d.ts',
],
};

3967
cli/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

44
cli/package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "cli",
"version": "0.0.0",
"description": "",
"engines": {
"node": ">= 16.13 <17"
},
"devDependencies": {
"@types/jest": "~27.0.2",
"@types/node": "~16.11.6",
"@typescript-eslint/eslint-plugin": "~5.3.0",
"@typescript-eslint/parser": "~5.3.0",
"eslint": "~8.1.0",
"eslint-config-prettier": "~8.3.0",
"eslint-plugin-jest": "~25.2.2",
"jest": "~27.3.1",
"prettier": "~2.4.1",
"rimraf": "~3.0.2",
"ts-jest": "~27.0.7",
"ts-node": "^10.4.0",
"tsutils": "~3.21.0",
"typescript": "~4.4.4"
},
"scripts": {
"start": "ts-node src/main.ts",
"runbuild": "node build/src/main.js",
"clean": "rimraf coverage build tmp",
"prebuild": "npm run lint",
"build": "tsc -p tsconfig.release.json",
"build:watch": "tsc -w -p tsconfig.release.json",
"lint": "eslint . --ext .ts,.tsx",
"test": "jest --coverage",
"test:watch": "jest --watch"
},
"author": "Jakub Synowiec <jsynowiec@users.noreply.github.com>",
"license": "Apache-2.0",
"dependencies": {
"cross-fetch": "^3.1.4",
"tslib": "~2.3.1"
},
"volta": {
"node": "16.13.0"
}
}

240
cli/src/class/ldk.ts Normal file
View File

@ -0,0 +1,240 @@
/* eslint-disable no-empty */
import fetch from 'cross-fetch';
export default class Ldk {
private injectedScript2address: ((scriptHex: string) => Promise<string>) | null = null;
logToGeneralLog(...args: any[]) {
const str = JSON.stringify(args);
console.log('js log:', str);
}
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 JSON.parse(text);
}
private 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, 2), Math.max(feerateSlow, 2));
} 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 = newFeerateFast * 250;
const medium = newFeerateMedium * 250;
const slow = newFeerateSlow * 250;
const response = await fetch(`http://127.0.0.1:8310/setfeerate/${fast}/${medium}/${slow}`);
const text = await response.text();
return JSON.parse(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 JSON.parse(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 JSON.parse(text)
}
private async transactionUnconfirmed(txid: string) {
const response = await fetch(`http://127.0.0.1:8310/transactionunconfirmed/${txid}`);
const text = await response.text();
return JSON.parse(text)
}
private async getRegisteredTxs() {
const response = await fetch(`http://127.0.0.1:8310/geteventsregistertx`);
const text = await response.text();
return JSON.parse(text)
}
private async getRegisteredOutputs() {
const response = await fetch(`http://127.0.0.1:8310/geteventsregisteroutput`);
const text = await response.text();
return JSON.parse(text)
}
public async listPeers() {
const response = await fetch(`http://127.0.0.1:8310/listpeers`);
const text = await response.text();
return JSON.parse(text)
}
}

15
cli/src/main.ts Normal file
View File

@ -0,0 +1,15 @@
import Ldk from './class/ldk';
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function main() {
console.log('start');
const ldk = new Ldk();
await ldk.checkBlockchain();
// console.warn(await ldk.listPeers());
}
main()

25
cli/tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"allowJs": true,
"importHelpers": true,
"jsx": "react",
"alwaysStrict": true,
"sourceMap": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": false,
"noImplicitThis": false,
"strictNullChecks": false
},
"include": [
"src/**/*",
"__tests__/**/*"
]
}

11
cli/tsconfig.release.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "build",
"removeComments": true
},
"include": [
"src/**/*"
]
}