ADD: basic cli control (nodejs)
This commit is contained in:
parent
e112c943ab
commit
d059a18299
15
cli/.editorconfig
Normal file
15
cli/.editorconfig
Normal 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
1
cli/.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
/**/*.js
|
||||
21
cli/.eslintrc.json
Normal file
21
cli/.eslintrc.json
Normal 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
29
cli/.gitignore
vendored
Normal 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
12
cli/.prettierrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.ts",
|
||||
"options": {
|
||||
"parser": "typescript"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
42
cli/__tests__/main.test.ts
Normal file
42
cli/__tests__/main.test.ts
Normal 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
20
cli/jest.config.js
Normal 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
3967
cli/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
cli/package.json
Normal file
44
cli/package.json
Normal 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
240
cli/src/class/ldk.ts
Normal 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
15
cli/src/main.ts
Normal 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
25
cli/tsconfig.json
Normal 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
11
cli/tsconfig.release.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"removeComments": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user