From 991f53e27178cf462afcb20063155585239037ab Mon Sep 17 00:00:00 2001 From: NullSoldier Date: Tue, 1 Feb 2022 17:21:12 -0800 Subject: [PATCH 01/24] Bump version to 0.1.21 --- ironfish-cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index df7821ce94..b5979c99df 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish-cli", - "version": "0.1.20", + "version": "0.1.21", "description": "Command line Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "engines": { From 9a3851625ed745fe61481c4646c27225c222abb7 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Fri, 4 Feb 2022 13:48:01 -0800 Subject: [PATCH 02/24] Convert native node code from neon to napi-rs (#947) --- .gitignore | 6 - ironfish-cli/tsconfig.json | 1 - ironfish-rust-nodejs/Cargo.lock | 174 +++++--- ironfish-rust-nodejs/Cargo.toml | 14 +- ironfish-rust-nodejs/build.rs | 5 + ironfish-rust-nodejs/index.d.ts | 194 +++++---- ironfish-rust-nodejs/index.js | 249 +++++++++++ ironfish-rust-nodejs/index.node.d.ts | 1 - ironfish-rust-nodejs/index.ts | 253 ----------- ironfish-rust-nodejs/package.json | 17 +- ironfish-rust-nodejs/src/lib.rs | 237 ++-------- ironfish-rust-nodejs/src/structs/note.rs | 106 ++--- .../src/structs/note_encrypted.rs | 178 +++----- .../src/structs/spend_proof.rs | 59 +-- .../src/structs/transaction.rs | 411 +++++++----------- ironfish-rust-nodejs/src/structs/witness.rs | 178 +++----- ironfish-rust-nodejs/tsconfig.json | 9 - ironfish-rust/src/keys/public_address.rs | 2 +- ironfish-rust/src/keys/view_keys.rs | 8 +- ironfish-rust/src/merkle_note.rs | 4 +- ironfish-rust/src/merkle_note_hash.rs | 2 +- ironfish-rust/src/mining.rs | 2 +- ironfish-rust/src/serializing.rs | 4 +- ironfish-rust/src/spending.rs | 4 +- ironfish-rust/src/transaction/mod.rs | 6 +- ironfish/package.json | 5 +- ironfish/src/mining/miner.test.ts | 4 +- ironfish/src/primitives/note.ts | 7 +- ironfish/src/primitives/noteEncrypted.ts | 3 - ironfish/src/primitives/transaction.ts | 18 +- ironfish/src/strategy.test.slow.ts | 10 +- .../src/workerPool/tasks/createMinersFee.ts | 4 - .../src/workerPool/tasks/createTransaction.ts | 5 - .../src/workerPool/tasks/getUnspentNotes.ts | 2 +- .../src/workerPool/tasks/transactionFee.ts | 6 +- .../src/workerPool/tasks/verifyTransaction.ts | 4 +- ironfish/tsconfig.test.json | 4 +- rust-toolchain | 2 +- yarn.lock | 10 +- 39 files changed, 947 insertions(+), 1261 deletions(-) create mode 100644 ironfish-rust-nodejs/build.rs create mode 100644 ironfish-rust-nodejs/index.js delete mode 100644 ironfish-rust-nodejs/index.node.d.ts delete mode 100644 ironfish-rust-nodejs/index.ts delete mode 100644 ironfish-rust-nodejs/tsconfig.json diff --git a/.gitignore b/.gitignore index 5dd068420a..32abfd0853 100644 --- a/.gitignore +++ b/.gitignore @@ -41,12 +41,6 @@ lerna-debug.log* **/*/target/* *.node -# ironfish-rust-nodejs -ironfish-rust-nodejs/*.map -ironfish-rust-nodejs/*.tsbuildinfo -ironfish-rust-nodejs/index.d.ts -ironfish-rust-nodejs/index.js - # ironfish-cli bin/ironfish-cli/databases .dockerignore diff --git a/ironfish-cli/tsconfig.json b/ironfish-cli/tsconfig.json index e995b866c1..142cf4a4f0 100644 --- a/ironfish-cli/tsconfig.json +++ b/ironfish-cli/tsconfig.json @@ -8,6 +8,5 @@ "include": ["src", "./package.json"], "references": [ { "path": "../ironfish" }, - { "path": "../ironfish-rust-nodejs" }, ] } diff --git a/ironfish-rust-nodejs/Cargo.lock b/ironfish-rust-nodejs/Cargo.lock index 6ffba3ace5..6912bf9ba8 100644 --- a/ironfish-rust-nodejs/Cargo.lock +++ b/ironfish-rust-nodejs/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "addchain" version = "0.1.0" @@ -43,6 +45,15 @@ dependencies = [ "opaque-debug 0.2.3", ] +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.40" @@ -248,6 +259,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "convert_case" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" + [[package]] name = "cpuid-bool" version = "0.1.2" @@ -372,10 +389,14 @@ dependencies = [ ] [[package]] -name = "cslice" -version = "0.2.0" +name = "ctor" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697c714f50560202b1f4e2e09cd50a421881c83e9025db75d15f276616f04f40" +checksum = "7fbaabec2c953050352311293be5c6aba8e141ba19d6811862b232d6fd020484" +dependencies = [ + "quote", + "syn", +] [[package]] name = "digest" @@ -601,7 +622,9 @@ name = "ironfish-rust-nodejs" version = "0.1.0" dependencies = [ "ironfish_rust", - "neon", + "napi", + "napi-build", + "napi-derive", ] [[package]] @@ -645,16 +668,6 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" -[[package]] -name = "libloading" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" -dependencies = [ - "cfg-if 1.0.0", - "winapi", -] - [[package]] name = "lock_api" version = "0.4.2" @@ -679,6 +692,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + [[package]] name = "memoffset" version = "0.5.6" @@ -689,46 +708,56 @@ dependencies = [ ] [[package]] -name = "neon" -version = "0.9.1" +name = "napi" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e85820b585bf3360bf158ac87a75764c48e361c91bbeb69873e6613cc78c023" +checksum = "5cbca4762c2865296cf6c9b2252fc2d875f005d2903a148b4cd6206bb6f9a686" dependencies = [ - "cslice", - "neon-build", - "neon-macros", - "neon-runtime", - "semver", - "smallvec", + "ctor", + "lazy_static", + "napi-sys", + "windows", ] [[package]] -name = "neon-build" -version = "0.9.1" +name = "napi-build" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9febc63f515156d4311a0c43899d3ace46352ecdd591c21b98ca3974f2a0d0" +checksum = "ebd4419172727423cf30351406c54f6cc1b354a2cfb4f1dba3e6cd07f6d5522b" [[package]] -name = "neon-macros" -version = "0.9.1" +name = "napi-derive" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "987f12c91eb6ce0b67819f7c5fb4d391de64cf411c605ed027f03507a33943b2" +checksum = "c6978824526532976fe146f71fef50395840d64a6c1af9085a358bdfdd300c7d" dependencies = [ + "convert_case", + "napi-derive-backend", + "proc-macro2", "quote", "syn", ] [[package]] -name = "neon-runtime" -version = "0.9.1" +name = "napi-derive-backend" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02662cd2e62b131937bdef85d0918b05bc3c204daf4c64af62845403eccb60f3" +checksum = "30623da375dd5a5cae5609b0f0b9915177c03b3dc23a8450cc7dcc14027eee25" dependencies = [ - "cfg-if 1.0.0", - "libloading", - "smallvec", + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "syn", ] +[[package]] +name = "napi-sys" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a385494dac3c52cbcacb393bb3b42669e7db8ab240c7ad5115f549eb061f2cc" + [[package]] name = "num-bigint" version = "0.2.6" @@ -975,6 +1004,23 @@ dependencies = [ "rust-argon2", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "rust-argon2" version = "0.8.3" @@ -1013,21 +1059,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "sha2" version = "0.8.2" @@ -1226,6 +1257,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac7fef12f4b59cd0a29339406cc9203ab44e440ddff6b3f5a41455349fa9cf3" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d027175d00b01e0cbeb97d6ab6ebe03b12330a35786cbaca5252b1c4bf5d9b" + +[[package]] +name = "windows_i686_gnu" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8793f59f7b8e8b01eda1a652b2697d87b93097198ae85f823b969ca5b89bba58" + +[[package]] +name = "windows_i686_msvc" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8602f6c418b67024be2996c512f5f995de3ba417f4c75af68401ab8756796ae4" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d615f419543e0bd7d2b3323af0d86ff19cbc4f816e6453f36a2c2ce889c354" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d95421d9ed3672c280884da53201a5c46b7b2765ca6faf34b0d71cf34a3561" + [[package]] name = "zcash_primitives" version = "0.2.0" diff --git a/ironfish-rust-nodejs/Cargo.toml b/ironfish-rust-nodejs/Cargo.toml index ffc1de143c..3ff2dde3e6 100644 --- a/ironfish-rust-nodejs/Cargo.toml +++ b/ironfish-rust-nodejs/Cargo.toml @@ -2,8 +2,7 @@ name = "ironfish-rust-nodejs" version = "0.1.0" license = "ISC" -edition = "2018" -exclude = ["index.node"] +edition = "2021" [lib] crate-type = ["cdylib"] @@ -11,9 +10,12 @@ crate-type = ["cdylib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +napi-derive = "2" ironfish_rust= { path = "../ironfish-rust", features = ["native"] } -[dependencies.neon] -version = "0.9.1" -default-features = false -features = ["napi-6"] +[dependencies.napi] +version = "2" +features = ["napi6"] + +[build-dependencies] +napi-build = "1" diff --git a/ironfish-rust-nodejs/build.rs b/ironfish-rust-nodejs/build.rs new file mode 100644 index 0000000000..9fc2367889 --- /dev/null +++ b/ironfish-rust-nodejs/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 34f8f8c1d3..6616993c31 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -1,93 +1,119 @@ -/// -export interface Key { - free(): void; - readonly incoming_view_key: string; - readonly outgoing_view_key: string; - readonly public_address: string; - readonly spending_key: string; -} -interface IWitnessNode { - side(): 'Left' | 'Right'; - hashOfSibling(): Uint8Array; +/* tslint:disable */ +/* eslint-disable */ + +/* auto-generated by NAPI-RS */ + +export class ExternalObject { + readonly '': { + readonly '': unique symbol + [K: symbol]: T + } } -interface IWitness { - verify(myHash: Uint8Array): boolean; - authPath(): IWitnessNode[]; - treeSize(): number; - serializeRootHash(): Uint8Array; +export interface Key { + spending_key: string + incoming_view_key: string + outgoing_view_key: string + public_address: string } -export interface mineHeaderResult { - readonly randomness: number; - readonly foundMatch: boolean; +export function generateKey(): Key +export function generateNewPublicAddress(privateKey: string): Key +export interface MineHeaderNapiResult { + randomness: number + foundMatch: boolean } -export declare class Note { - boxedData: unknown; - constructor(); - constructor(owner: string, value: bigint, memo: string); - free(): void; - static fromBoxedData(boxedData: unknown): Note; - static deserialize(data: Buffer): Note; - serialize(): Buffer; - get value(): bigint; - get memo(): string; - nullifier(ownerPrivateKey: string, position: bigint): Buffer; +export function mineHeaderBatch(headerBytes: Buffer, initialRandomness: number, targetBuffer: Buffer, batchSize: number): MineHeaderNapiResult +export type NativeNoteEncrypted = NoteEncrypted +export class NoteEncrypted { + static deserialize(bytes: Buffer): NativeNoteEncrypted + serialize(): Buffer + equals(other: NoteEncrypted): boolean + merkleHash(): Buffer + /** + * Hash two child hashes together to calculate the hash of the + * new parent + */ + static combineHash(depth: number, left: Buffer, right: Buffer): Buffer + /** Returns undefined if the note was unable to be decrypted with the given key. */ + decryptNoteForOwner(incomingHexKey: string): NativeNote | undefined | null + /** Returns undefined if the note was unable to be decrypted with the given key. */ + decryptNoteForSpender(outgoingHexKey: string): NativeNote | undefined | null } -export declare class NoteEncrypted { - boxedData: unknown; - constructor(boxedData: unknown); - free(): void; - static combineHash(depth: number, left: Buffer, right: Buffer): any; - static deserialize(data: Buffer): NoteEncrypted; - serialize(): Buffer; - equals(noteEncrypted: NoteEncrypted): boolean; - merkleHash(): Buffer; - decryptNoteForOwner(owner_hex_key: string): Note | undefined; - decryptNoteForSpender(spender_hex_key: string): Note | undefined; +export type NativeNote = Note +export class Note { + constructor(owner: string, value: bigint, memo: string) + static deserialize(bytes: Buffer): NativeNote + serialize(): Buffer + /** Value this note represents. */ + value(): bigint + /** + * Arbitrary note the spender can supply when constructing a spend so the + * receiver has some record from whence it came. + * Note: While this is encrypted with the output, it is not encoded into + * the proof in any way. + */ + memo(): string + /** + * Compute the nullifier for this note, given the private key of its owner. + * + * The nullifier is a series of bytes that is published by the note owner + * only at the time the note is spent. This key is collected in a massive + * 'nullifier set', preventing double-spend. + */ + nullifier(ownerPrivateKey: string, position: bigint): Buffer } -export declare class SimpleTransaction { - boxedData: unknown; - constructor(spenderHexKey: string, intendedTransactionFee: bigint); - free(): void; - spend(note: Note, witness: IWitness): string; - receive(note: Note): string; - post(): TransactionPosted; +export class NativeSpendProof { + treeSize(): number + rootHash(): Buffer + nullifier(): Buffer } -export declare class Transaction { - boxedData: unknown; - constructor(); - free(): void; - receive(spenderHexKey: string, note: Note): string; - spend(spenderHexKey: string, note: Note, witness: IWitness): string; - post_miners_fee(): TransactionPosted; - post(spenderHexKey: string, changeGoesTo: string | undefined, intendedTransactionFee: bigint): TransactionPosted; - setExpirationSequence(expirationSequence: number): undefined; +export type NativeTransactionPosted = TransactionPosted +export class TransactionPosted { + static deserialize(bytes: Buffer): NativeTransactionPosted + serialize(): Buffer + verify(): boolean + notesLength(): number + getNote(index: number): Buffer + spendsLength(): number + getSpend(index: number): NativeSpendProof + fee(): bigint + transactionSignature(): Buffer + hash(): Buffer + expirationSequence(): number } -declare class SpendProof { - boxedData: unknown; - constructor(boxedData: unknown); - free(): void; - get nullifier(): Buffer; - get rootHash(): Buffer; - get treeSize(): number; +export type NativeTransaction = Transaction +export class Transaction { + constructor() + /** Create a proof of a new note owned by the recipient in this transaction. */ + receive(spenderHexKey: string, note: Note): string + /** Spend the note owned by spender_hex_key at the given witness location. */ + spend(spenderHexKey: string, note: Note, witness: object): string + /** + * Special case for posting a miners fee transaction. Miner fee transactions + * are unique in that they generate currency. They do not have any spends + * or change and therefore have a negative transaction fee. In normal use, + * a miner would not accept such a transaction unless it was explicitly set + * as the miners fee. + */ + post_miners_fee(): TransactionPosted + /** + * Post the transaction. This performs a bit of validation, and signs + * the spends with a signature that proves the spends are part of this + * transaction. + * + * Transaction fee is the amount the spender wants to send to the miner + * for mining this transaction. This has to be non-negative; sane miners + * wouldn't accept a transaction that takes money away from them. + * + * sum(spends) - sum(outputs) - intended_transaction_fee - change = 0 + * aka: self.transaction_fee - intended_transaction_fee - change = 0 + */ + post(spenderHexKey: string, changeGoesTo?: string | undefined | null, intendedTransactionFee: bigint): TransactionPosted + setExpirationSequence(expirationSequence: number): void } -export declare class TransactionPosted { - boxedData: unknown; - constructor(boxedData: unknown); - free(): void; - static deserialize(bytes: Buffer): TransactionPosted; - serialize(): Buffer; - verify(): boolean; - getNote(index: number): Buffer; - getSpend(index: number): SpendProof; - get notesLength(): number; - get spendsLength(): number; - get fee(): bigint; - get hash(): Buffer; - get transactionSignature(): Buffer; - get expirationSequence(): number; +export type NativeSimpleTransaction = SimpleTransaction +export class SimpleTransaction { + constructor(spenderHexKey: string, intendedTransactionFee: bigint) + spend(note: Note, witness: object): string + receive(note: Note): string + post(): TransactionPosted } -export declare const generateKey: () => Key; -export declare const generateNewPublicAddress: (privateKey: string) => Key; -export declare const mineHeaderBatch: (headerBytes: Buffer, initialRandomness: number, target: Buffer, batchSize: number) => mineHeaderResult; -export {}; -//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js new file mode 100644 index 0000000000..b08a5ab897 --- /dev/null +++ b/ironfish-rust-nodejs/index.js @@ -0,0 +1,249 @@ +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') + +const { platform, arch } = process + +let nativeBinding = null +let localFileExisted = false +let loadError = null + +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + return readFileSync('/usr/bin/ldd', 'utf8').includes('musl') + } catch (e) { + return true + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header + return !glibcVersionRuntime + } +} + +switch (platform) { + case 'android': + switch (arch) { + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'ironfish-rust-nodejs.android-arm64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.android-arm64.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-android-arm64') + } + } catch (e) { + loadError = e + } + break + case 'arm': + localFileExisted = existsSync(join(__dirname, 'ironfish-rust-nodejs.android-arm-eabi.node')) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.android-arm-eabi.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-android-arm-eabi') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Android ${arch}`) + } + break + case 'win32': + switch (arch) { + case 'x64': + localFileExisted = existsSync( + join(__dirname, 'ironfish-rust-nodejs.win32-x64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.win32-x64-msvc.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-win32-x64-msvc') + } + } catch (e) { + loadError = e + } + break + case 'ia32': + localFileExisted = existsSync( + join(__dirname, 'ironfish-rust-nodejs.win32-ia32-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.win32-ia32-msvc.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-win32-ia32-msvc') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'ironfish-rust-nodejs.win32-arm64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.win32-arm64-msvc.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-win32-arm64-msvc') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`) + } + break + case 'darwin': + switch (arch) { + case 'x64': + localFileExisted = existsSync(join(__dirname, 'ironfish-rust-nodejs.darwin-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.darwin-x64.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-darwin-x64') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'ironfish-rust-nodejs.darwin-arm64.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.darwin-arm64.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-darwin-arm64') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`) + } + break + case 'freebsd': + if (arch !== 'x64') { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + } + localFileExisted = existsSync(join(__dirname, 'ironfish-rust-nodejs.freebsd-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.freebsd-x64.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-freebsd-x64') + } + } catch (e) { + loadError = e + } + break + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'ironfish-rust-nodejs.linux-x64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.linux-x64-musl.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-linux-x64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'ironfish-rust-nodejs.linux-x64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.linux-x64-gnu.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-linux-x64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'ironfish-rust-nodejs.linux-arm64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.linux-arm64-musl.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-linux-arm64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'ironfish-rust-nodejs.linux-arm64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.linux-arm64-gnu.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-linux-arm64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm': + localFileExisted = existsSync( + join(__dirname, 'ironfish-rust-nodejs.linux-arm-gnueabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./ironfish-rust-nodejs.linux-arm-gnueabihf.node') + } else { + nativeBinding = require('ironfish-rust-nodejs-linux-arm-gnueabihf') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`) + } + break + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) +} + +if (!nativeBinding) { + if (loadError) { + throw loadError + } + throw new Error(`Failed to load native binding`) +} + +const { NoteEncrypted, Note, NativeSpendProof, TransactionPosted, Transaction, SimpleTransaction, generateKey, generateNewPublicAddress, mineHeaderBatch } = nativeBinding + +module.exports.NoteEncrypted = NoteEncrypted +module.exports.Note = Note +module.exports.NativeSpendProof = NativeSpendProof +module.exports.TransactionPosted = TransactionPosted +module.exports.Transaction = Transaction +module.exports.SimpleTransaction = SimpleTransaction +module.exports.generateKey = generateKey +module.exports.generateNewPublicAddress = generateNewPublicAddress +module.exports.mineHeaderBatch = mineHeaderBatch diff --git a/ironfish-rust-nodejs/index.node.d.ts b/ironfish-rust-nodejs/index.node.d.ts deleted file mode 100644 index b8552e70ba..0000000000 --- a/ironfish-rust-nodejs/index.node.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default any; \ No newline at end of file diff --git a/ironfish-rust-nodejs/index.ts b/ironfish-rust-nodejs/index.ts deleted file mode 100644 index e3a688699e..0000000000 --- a/ironfish-rust-nodejs/index.ts +++ /dev/null @@ -1,253 +0,0 @@ -import native from './index.node'; - -export interface Key { - free(): void; - - readonly incoming_view_key: string; - - readonly outgoing_view_key: string; - - readonly public_address: string; - - readonly spending_key: string; -} - -interface IWitnessNode { - side(): 'Left' | 'Right'; - hashOfSibling(): Uint8Array; -} - -interface IWitness { - verify(myHash: Uint8Array): boolean; - authPath(): IWitnessNode[]; - treeSize(): number; - serializeRootHash(): Uint8Array; -} - -export interface mineHeaderResult { - readonly randomness: number; - readonly foundMatch: boolean; -} - -export class Note { - boxedData: unknown - - constructor() - constructor(owner: string, value: bigint, memo: string) - constructor(owner?: string, value?: bigint, memo?: string) { - if (arguments.length === 0) { - return; - } - - this.boxedData = native.noteNew(owner, value?.toString(), memo); - } - - free() {} - - static fromBoxedData(boxedData: unknown) { - const note = new Note(); - note.boxedData = boxedData; - return note; - } - - static deserialize(data: Buffer): Note { - const result = native.noteDeserialize(data); - return Note.fromBoxedData(result); - } - - serialize(): Buffer { - return native.noteSerialize.call(this.boxedData); - } - - get value(): bigint { - return BigInt(native.noteValue.call(this.boxedData)); - } - - get memo(): string { - return native.noteMemo.call(this.boxedData); - } - - nullifier(ownerPrivateKey: string, position: bigint): Buffer { - return native.noteNullifier.call(this.boxedData, ownerPrivateKey, position.toString()); - } -} - -export class NoteEncrypted { - boxedData: unknown - - constructor(boxedData: unknown) { - this.boxedData = boxedData; - } - - free() {} - - static combineHash(depth: number, left: Buffer, right: Buffer) { - return native.combineHash(depth, left, right) - } - - static deserialize(data: Buffer): NoteEncrypted { - const result = native.noteEncryptedDeserialize(data); - return new NoteEncrypted(result); - } - - serialize(): Buffer { - return native.noteEncryptedSerialize.call(this.boxedData); - } - - equals(noteEncrypted: NoteEncrypted): boolean { - return native.noteEncryptedEquals.call(this.boxedData, noteEncrypted.boxedData); - } - - merkleHash(): Buffer { - return native.noteEncryptedMerkleHash.call(this.boxedData); - } - - decryptNoteForOwner(owner_hex_key: string): Note | undefined { - const boxedData = native.noteEncryptedDecryptNoteForOwner.call(this.boxedData, owner_hex_key); - - return boxedData ? Note.fromBoxedData(boxedData) : undefined; - } - - decryptNoteForSpender(spender_hex_key: string): Note | undefined { - const boxedData = native.noteEncryptedDecryptNoteForSpender.call(this.boxedData, spender_hex_key); - - return boxedData ? Note.fromBoxedData(boxedData) : undefined; - } -} - -export class SimpleTransaction { - boxedData: unknown - - constructor(spenderHexKey: string, intendedTransactionFee: bigint) { - this.boxedData = native.simpleTransactionNew(spenderHexKey, intendedTransactionFee.toString()); - } - - free() {} - - spend(note: Note, witness: IWitness): string { - return native.simpleTransactionSpend.call(this.boxedData, note.boxedData, witness); - } - - receive(note: Note): string { - return native.simpleTransactionReceive.call(this.boxedData, note.boxedData); - } - - post(): TransactionPosted { - return new TransactionPosted(native.simpleTransactionPost.call(this.boxedData)); - } -} - -export class Transaction { - boxedData: unknown - - constructor() { - this.boxedData = native.transactionNew(); - } - - free() {} - - receive(spenderHexKey: string, note: Note): string { - return native.transactionReceive.call(this.boxedData, spenderHexKey, note.boxedData); - } - - spend(spenderHexKey: string, note: Note, witness: IWitness): string { - return native.transactionSpend.call(this.boxedData, spenderHexKey, note.boxedData, witness); - } - - post_miners_fee(): TransactionPosted { - return new TransactionPosted(native.transactionPostMinersFee.call(this.boxedData)); - } - - post(spenderHexKey: string, changeGoesTo: string | undefined, intendedTransactionFee: bigint): TransactionPosted { - changeGoesTo = changeGoesTo ?? ''; - - return new TransactionPosted(native.transactionPost.call(this.boxedData, spenderHexKey, changeGoesTo, intendedTransactionFee.toString())); - } - - setExpirationSequence(expirationSequence: number): undefined { - return native.transactionSetExpirationSequence.call(this.boxedData, expirationSequence) - } -} - -class SpendProof { - boxedData: unknown - - constructor(boxedData: unknown) { - this.boxedData = boxedData; - } - - free() {} - - get nullifier(): Buffer { - return native.spendProofNullifier.call(this.boxedData); - } - - get rootHash(): Buffer { - return native.spendProofRootHash.call(this.boxedData); - } - - get treeSize(): number { - return native.spendProofTreeSize.call(this.boxedData); - } -} - -export class TransactionPosted { - boxedData: unknown - - constructor(boxedData: unknown) { - this.boxedData = boxedData; - } - - free() {} - - static deserialize(bytes: Buffer): TransactionPosted { - const result = native.transactionPostedDeserialize(bytes); - return new TransactionPosted(result); - } - - serialize(): Buffer { - return native.transactionPostedSerialize.call(this.boxedData); - } - - verify(): boolean { - return native.transactionPostedVerify.call(this.boxedData); - } - - getNote(index: number): Buffer { - return native.transactionPostedGetNote.call(this.boxedData, index); - } - - getSpend(index: number): SpendProof { - const result = native.transactionPostedGetSpend.call(this.boxedData, index); - return new SpendProof(result); - } - - get notesLength(): number { - return native.transactionPostedNotesLength.call(this.boxedData); - } - - get spendsLength(): number { - return native.transactionPostedSpendsLength.call(this.boxedData); - } - - get fee(): bigint { - const result = native.transactionPostedFee.call(this.boxedData); - return BigInt(result); - } - - get hash(): Buffer { - return native.transactionPostedHash.call(this.boxedData); - } - - get transactionSignature(): Buffer { - return native.transactionPostedTransactionSignature.call(this.boxedData); - } - - get expirationSequence(): number { - return native.transactionExpirationSequence.call(this.boxedData); - } -} - -export const generateKey: () => Key = native.generateKey -export const generateNewPublicAddress: (privateKey: string) => Key = native.generateNewPublicAddress -export const mineHeaderBatch: (headerBytes: Buffer, initialRandomness: number, target: Buffer, batchSize: number) => mineHeaderResult = native.mineHeaderBatch diff --git a/ironfish-rust-nodejs/package.json b/ironfish-rust-nodejs/package.json index f32c061e6a..c448c43473 100644 --- a/ironfish-rust-nodejs/package.json +++ b/ironfish-rust-nodejs/package.json @@ -3,16 +3,21 @@ "version": "0.1.0", "description": "", "main": "index.js", + "files": [ + "index.d.ts", + "index.js" + ], "scripts": { - "cargo-cp-artifact": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics", - "build": "npm run cargo-cp-artifact -- --release", - "build-debug": "npm run cargo-cp-artifact --", - "build-release": "npm run cargo-cp-artifact -- --release" + "build": "napi build --platform --release", + "build-debug": "napi build --platform", + "build-release": "napi build --platform --release" + }, + "napi": { + "name": "ironfish-rust-nodejs" }, "author": "", "license": "ISC", "devDependencies": { - "cargo-cp-artifact": "0.1.5", - "typescript": "4.3.4" + "@napi-rs/cli": "2.4.2" } } \ No newline at end of file diff --git a/ironfish-rust-nodejs/src/lib.rs b/ironfish-rust-nodejs/src/lib.rs index 6d86b14dbd..41c37b0e2c 100644 --- a/ironfish-rust-nodejs/src/lib.rs +++ b/ironfish-rust-nodejs/src/lib.rs @@ -2,225 +2,80 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use neon::prelude::*; +use napi::bindgen_prelude::*; +use napi::Error; +use napi_derive::napi; use ironfish_rust::mining; use ironfish_rust::sapling_bls12; pub mod structs; -pub trait ToObjectExt { - fn to_object<'a>(&self, cx: &mut impl Context<'a>) -> JsResult<'a, JsObject>; +#[napi(object)] +pub struct Key { + #[napi(js_name = "spending_key")] + pub spending_key: String, + #[napi(js_name = "incoming_view_key")] + pub incoming_view_key: String, + #[napi(js_name = "outgoing_view_key")] + pub outgoing_view_key: String, + #[napi(js_name = "public_address")] + pub public_address: String, } -struct Key { - spending_key: String, - incoming_view_key: String, - outgoing_view_key: String, - public_address: String, -} - -impl ToObjectExt for Key { - fn to_object<'a>(&self, cx: &mut impl Context<'a>) -> JsResult<'a, JsObject> { - let obj = cx.empty_object(); - - let spending_key = cx.string(&self.spending_key); - obj.set(cx, "spending_key", spending_key)?; - - let incoming_view_key = cx.string(&self.incoming_view_key); - obj.set(cx, "incoming_view_key", incoming_view_key)?; - - let outgoing_view_key = cx.string(&self.outgoing_view_key); - obj.set(cx, "outgoing_view_key", outgoing_view_key)?; - - let public_address = cx.string(&self.public_address); - obj.set(cx, "public_address", public_address)?; - - Ok(obj) - } -} - -impl ToObjectExt for mining::MineHeaderResult { - fn to_object<'a>(&self, cx: &mut impl Context<'a>) -> JsResult<'a, JsObject> { - let obj = cx.empty_object(); - - let randomness = cx.number(self.randomness); - obj.set(cx, "randomness", randomness)?; - - let found_match = cx.boolean(self.found_match); - obj.set(cx, "foundMatch", found_match)?; - - Ok(obj) - } -} - -fn generate_key(mut cx: FunctionContext) -> JsResult { +#[napi] +pub fn generate_key() -> Key { let hasher = sapling_bls12::SAPLING.clone(); let sapling_key = sapling_bls12::Key::generate_key(hasher); - let key = Key { + Key { spending_key: sapling_key.hex_spending_key(), incoming_view_key: sapling_key.incoming_view_key().hex_key(), outgoing_view_key: sapling_key.outgoing_view_key().hex_key(), public_address: sapling_key.generate_public_address().hex_public_address(), - }; - - key.to_object(&mut cx) + } } -fn generate_new_public_address(mut cx: FunctionContext) -> JsResult { - let private_key = cx.argument::(0)?.value(&mut cx); +#[napi] +pub fn generate_new_public_address(private_key: String) -> Result { let hasher = sapling_bls12::SAPLING.clone(); let sapling_key = sapling_bls12::Key::from_hex(hasher, &private_key) - .or_else(|err| cx.throw_error(err.to_string()))?; + .map_err(|err| Error::from_reason(err.to_string()))?; - let key = Key { + Ok(Key { spending_key: sapling_key.hex_spending_key(), incoming_view_key: sapling_key.incoming_view_key().hex_key(), outgoing_view_key: sapling_key.outgoing_view_key().hex_key(), public_address: sapling_key.generate_public_address().hex_public_address(), - }; + }) +} - key.to_object(&mut cx) +#[napi(object)] +pub struct MineHeaderNapiResult { + pub randomness: f64, + pub found_match: bool, } -fn mine_header_batch(mut cx: FunctionContext) -> JsResult { - // Argument 1 - let header_buffer = cx.argument::(0)?; - let header_bytes = cx.borrow(&header_buffer, |data| data.as_mut_slice::()); - // Argument 2 - let initial_randomness = cx.argument::(1)?.value(&mut cx) as i64; - // Argument 3 - let target_buffer = cx.argument::(2)?; - let target_slice = cx.borrow(&target_buffer, |data| data.as_slice::()); +#[napi] +pub fn mine_header_batch( + mut header_bytes: Buffer, + initial_randomness: i64, + target_buffer: Buffer, + batch_size: i64, +) -> MineHeaderNapiResult { let mut target_array = [0u8; 32]; - target_array.copy_from_slice(&target_slice[..32]); - // Argument 4 - let batch_size = cx.argument::(3)?.value(&mut cx) as i64; + target_array.copy_from_slice(&target_buffer[..32]); // Execute batch mine operation - let mine_header_result = - mining::mine_header_batch(header_bytes, initial_randomness, &target_array, batch_size); - - // Return result - mine_header_result.to_object(&mut cx) -} - -#[neon::main] -fn main(mut cx: ModuleContext) -> NeonResult<()> { - cx.export_function("generateKey", generate_key)?; - cx.export_function("generateNewPublicAddress", generate_new_public_address)?; - cx.export_function("combineHash", structs::NativeNoteEncrypted::combine_hash)?; - - cx.export_function( - "noteEncryptedDeserialize", - structs::NativeNoteEncrypted::deserialize, - )?; - cx.export_function( - "noteEncryptedSerialize", - structs::NativeNoteEncrypted::serialize, - )?; - cx.export_function("noteEncryptedEquals", structs::NativeNoteEncrypted::equals)?; - cx.export_function( - "noteEncryptedMerkleHash", - structs::NativeNoteEncrypted::merkle_hash, - )?; - cx.export_function( - "noteEncryptedDecryptNoteForOwner", - structs::NativeNoteEncrypted::decrypt_note_for_owner, - )?; - cx.export_function( - "noteEncryptedDecryptNoteForSpender", - structs::NativeNoteEncrypted::decrypt_note_for_spender, - )?; - - cx.export_function("noteNew", structs::NativeNote::new)?; - cx.export_function("noteDeserialize", structs::NativeNote::deserialize)?; - cx.export_function("noteSerialize", structs::NativeNote::serialize)?; - cx.export_function("noteValue", structs::NativeNote::value)?; - cx.export_function("noteMemo", structs::NativeNote::memo)?; - cx.export_function("noteNullifier", structs::NativeNote::nullifier)?; - - cx.export_function( - "simpleTransactionNew", - structs::NativeSimpleTransaction::new, - )?; - cx.export_function( - "simpleTransactionSpend", - structs::NativeSimpleTransaction::spend, - )?; - cx.export_function( - "simpleTransactionReceive", - structs::NativeSimpleTransaction::receive, - )?; - cx.export_function( - "simpleTransactionPost", - structs::NativeSimpleTransaction::post, - )?; - - cx.export_function("transactionNew", structs::NativeTransaction::new)?; - cx.export_function("transactionSpend", structs::NativeTransaction::spend)?; - cx.export_function("transactionReceive", structs::NativeTransaction::receive)?; - cx.export_function("transactionPost", structs::NativeTransaction::post)?; - cx.export_function( - "transactionPostMinersFee", - structs::NativeTransaction::post_miners_fee, - )?; - cx.export_function( - "transactionSetExpirationSequence", - structs::NativeTransaction::set_expiration_sequence, - )?; - - cx.export_function("spendProofNullifier", structs::NativeSpendProof::nullifier)?; - cx.export_function("spendProofRootHash", structs::NativeSpendProof::root_hash)?; - cx.export_function("spendProofTreeSize", structs::NativeSpendProof::tree_size)?; - - cx.export_function( - "transactionPostedDeserialize", - structs::NativeTransactionPosted::deserialize, - )?; - cx.export_function( - "transactionPostedSerialize", - structs::NativeTransactionPosted::serialize, - )?; - cx.export_function( - "transactionPostedVerify", - structs::NativeTransactionPosted::verify, - )?; - cx.export_function( - "transactionPostedNotesLength", - structs::NativeTransactionPosted::notes_length, - )?; - cx.export_function( - "transactionPostedGetNote", - structs::NativeTransactionPosted::get_note, - )?; - cx.export_function( - "transactionPostedSpendsLength", - structs::NativeTransactionPosted::spends_length, - )?; - cx.export_function( - "transactionPostedGetSpend", - structs::NativeTransactionPosted::get_spend, - )?; - cx.export_function( - "transactionPostedFee", - structs::NativeTransactionPosted::fee, - )?; - cx.export_function( - "transactionPostedTransactionSignature", - structs::NativeTransactionPosted::transaction_signature, - )?; - cx.export_function( - "transactionPostedHash", - structs::NativeTransactionPosted::hash, - )?; - cx.export_function( - "transactionExpirationSequence", - structs::NativeTransactionPosted::expiration_sequence, - )?; - - cx.export_function("mineHeaderBatch", mine_header_batch)?; - - Ok(()) + let mine_header_result = mining::mine_header_batch( + header_bytes.as_mut(), + initial_randomness, + &target_array, + batch_size, + ); + + MineHeaderNapiResult { + randomness: mine_header_result.randomness, + found_match: mine_header_result.found_match, + } } diff --git a/ironfish-rust-nodejs/src/structs/note.rs b/ironfish-rust-nodejs/src/structs/note.rs index a452a01ce2..5eeff1e941 100644 --- a/ironfish-rust-nodejs/src/structs/note.rs +++ b/ironfish-rust-nodejs/src/structs/note.rs @@ -2,88 +2,63 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::convert::TryInto; - -use neon::prelude::*; +use napi::bindgen_prelude::*; +use napi::JsBigInt; +use napi_derive::napi; use ironfish_rust::note::Memo; use ironfish_rust::sapling_bls12::{Key, Note, SAPLING}; +#[napi(js_name = "Note")] pub struct NativeNote { pub(crate) note: Note, } -impl Finalize for NativeNote {} - +#[napi] impl NativeNote { - pub fn new(mut cx: FunctionContext) -> JsResult> { - let owner = cx.argument::(0)?.value(&mut cx); - // TODO: Should be BigInt, but no first-class Neon support - let value = cx.argument::(1)?.value(&mut cx); - let memo = cx.argument::(2)?.value(&mut cx); - - let value_u64: u64 = value - .parse::() - .or_else(|err| cx.throw_error(err.to_string()))?; + #[napi(constructor)] + pub fn new(owner: String, value: JsBigInt, memo: String) -> Result { + let value_u64 = value.get_u64()?.0; let owner_address = ironfish_rust::PublicAddress::from_hex(SAPLING.clone(), &owner) - .or_else(|err| cx.throw_error(err.to_string()))?; - Ok(cx.boxed(NativeNote { + .map_err(|err| Error::from_reason(err.to_string()))?; + Ok(NativeNote { note: Note::new(SAPLING.clone(), owner_address, value_u64, Memo::from(memo)), - })) + }) } - pub fn deserialize(mut cx: FunctionContext) -> JsResult> { - let bytes = cx.argument::(0)?; - + #[napi(factory)] + pub fn deserialize(bytes: Buffer) -> Result { let hasher = SAPLING.clone(); - let note = cx - .borrow(&bytes, |data| Note::read(data.as_slice(), hasher)) - .or_else(|err| cx.throw_error(err.to_string()))?; + let note = Note::read(bytes.as_ref(), hasher) + .map_err(|err| Error::from_reason(err.to_string()))?; - Ok(cx.boxed(NativeNote { note })) + Ok(NativeNote { note }) } - pub fn serialize(mut cx: FunctionContext) -> JsResult { - let note = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - + #[napi] + pub fn serialize(&self) -> Result { let mut arr: Vec = vec![]; - note.note + self.note .write(&mut arr) - .or_else(|err| cx.throw_error(err.to_string()))?; - - let mut bytes = cx.buffer(arr.len().try_into().unwrap())?; + .map_err(|err| Error::from_reason(err.to_string()))?; - cx.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&arr[..slice.len()]); - }); - - Ok(bytes) + Ok(Buffer::from(arr)) } /// Value this note represents. - pub fn value(mut cx: FunctionContext) -> JsResult { - let note = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - - // TODO: Should be BigInt, but no first-class Neon support - Ok(cx.string(note.note.value().to_string())) + #[napi] + pub fn value(&self) -> u64 { + self.note.value() } /// Arbitrary note the spender can supply when constructing a spend so the /// receiver has some record from whence it came. /// Note: While this is encrypted with the output, it is not encoded into /// the proof in any way. - pub fn memo(mut cx: FunctionContext) -> JsResult { - let note = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - - Ok(cx.string(note.note.memo().to_string())) + #[napi] + pub fn memo(&self) -> String { + self.note.memo().to_string() } /// Compute the nullifier for this note, given the private key of its owner. @@ -91,30 +66,15 @@ impl NativeNote { /// The nullifier is a series of bytes that is published by the note owner /// only at the time the note is spent. This key is collected in a massive /// 'nullifier set', preventing double-spend. - pub fn nullifier(mut cx: FunctionContext) -> JsResult { - let note = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - let owner_private_key = cx.argument::(0)?.value(&mut cx); - // TODO: Should be BigInt, but no first-class Neon support - let position = cx.argument::(1)?.value(&mut cx); - - let position_u64 = position - .parse::() - .or_else(|err| cx.throw_error(err.to_string()))?; + #[napi] + pub fn nullifier(&self, owner_private_key: String, position: JsBigInt) -> Result { + let position_u64 = position.get_u64()?.0; let private_key = Key::from_hex(SAPLING.clone(), &owner_private_key) - .or_else(|err| cx.throw_error(err.to_string()))?; - - let nullifier = note.note.nullifier(&private_key, position_u64); - - let mut bytes = cx.buffer(nullifier.len().try_into().unwrap())?; + .map_err(|err| Error::from_reason(err.to_string()))?; - cx.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&nullifier[..slice.len()]); - }); + let nullifier: &[u8] = &self.note.nullifier(&private_key, position_u64); - Ok(bytes) + Ok(Buffer::from(nullifier)) } } diff --git a/ironfish-rust-nodejs/src/structs/note_encrypted.rs b/ironfish-rust-nodejs/src/structs/note_encrypted.rs index a45cd36b54..9c9af0c1c8 100644 --- a/ironfish-rust-nodejs/src/structs/note_encrypted.rs +++ b/ironfish-rust-nodejs/src/structs/note_encrypted.rs @@ -2,169 +2,111 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::convert::TryInto; - -use neon::prelude::*; +use napi::bindgen_prelude::*; +use napi_derive::napi; use super::NativeNote; use ironfish_rust::sapling_bls12; use ironfish_rust::MerkleNote; +#[napi(js_name = "NoteEncrypted")] pub struct NativeNoteEncrypted { pub(crate) note: sapling_bls12::MerkleNote, } -impl Finalize for NativeNoteEncrypted {} - +#[napi] impl NativeNoteEncrypted { - pub fn deserialize(mut cx: FunctionContext) -> JsResult> { - let bytes = cx.argument::(0)?; - + #[napi(factory)] + pub fn deserialize(bytes: Buffer) -> Result { let hasher = sapling_bls12::SAPLING.clone(); - let note = cx - .borrow(&bytes, |data| { - let cursor: std::io::Cursor<&[u8]> = std::io::Cursor::new(data.as_slice()); - MerkleNote::read(cursor, hasher) - }) - .or_else(|err| cx.throw_error(err.to_string()))?; - - Ok(cx.boxed(NativeNoteEncrypted { note })) - } - - pub fn serialize(mut cx: FunctionContext) -> JsResult { - // Get the `this` value as a `JsBox` - let note = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - let mut arr: Vec = vec![]; - note.note - .write(&mut arr) - .or_else(|err| cx.throw_error(err.to_string()))?; + let note = MerkleNote::read(bytes.as_ref(), hasher) + .map_err(|err| Error::from_reason(err.to_string()))?; - let mut bytes = cx.buffer(arr.len().try_into().unwrap())?; - - cx.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&arr[..slice.len()]); - }); - - Ok(bytes) + Ok(NativeNoteEncrypted { note }) } - pub fn equals(mut cx: FunctionContext) -> JsResult { - let note = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - - let other = cx.argument::>(0)?; + #[napi] + pub fn serialize(&self) -> Result { + let mut vec: Vec = vec![]; + self.note + .write(&mut vec) + .map_err(|err| Error::from_reason(err.to_string()))?; - Ok(cx.boolean(note.note.eq(&other.note))) + Ok(Buffer::from(vec)) } - pub fn merkle_hash(mut cx: FunctionContext) -> JsResult { - let note = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; + #[napi] + pub fn equals(&self, other: &NativeNoteEncrypted) -> bool { + self.note.eq(&other.note) + } - let mut cursor: Vec = Vec::with_capacity(32); - note.note + #[napi] + pub fn merkle_hash(&self) -> Result { + let mut vec: Vec = Vec::with_capacity(32); + self.note .merkle_hash() - .write(&mut cursor) - .or_else(|err| cx.throw_error(err.to_string()))?; - - // Copy hash to JsBuffer - let mut bytes = cx.buffer(32)?; + .write(&mut vec) + .map_err(|err| Error::from_reason(err.to_string()))?; - cx.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&cursor[..slice.len()]); - }); - - Ok(bytes) + Ok(Buffer::from(vec)) } /// Hash two child hashes together to calculate the hash of the /// new parent - pub fn combine_hash(mut cx: FunctionContext) -> JsResult { - let depth = cx.argument::(0)?.value(&mut cx) as usize; - let left = cx.argument::(1)?; - let right = cx.argument::(2)?; - - let left_hash = cx - .borrow(&left, |data| { - let mut left_hash_reader: std::io::Cursor<&[u8]> = - std::io::Cursor::new(data.as_slice()); - sapling_bls12::MerkleNoteHash::read(&mut left_hash_reader) - }) - .or_else(|err| cx.throw_error(err.to_string()))?; - - let right_hash = cx - .borrow(&right, |data| { - let mut right_hash_reader: std::io::Cursor<&[u8]> = - std::io::Cursor::new(data.as_slice()); - sapling_bls12::MerkleNoteHash::read(&mut right_hash_reader) - }) - .or_else(|err| cx.throw_error(err.to_string()))?; - - let mut cursor = Vec::with_capacity(32); + #[napi] + pub fn combine_hash(depth: i64, left: Buffer, right: Buffer) -> Result { + let left_hash = sapling_bls12::MerkleNoteHash::read(left.as_ref()) + .map_err(|err| Error::from_reason(err.to_string()))?; + + let right_hash = sapling_bls12::MerkleNoteHash::read(right.as_ref()) + .map_err(|err| Error::from_reason(err.to_string()))?; + + let converted_depth: usize = depth + .try_into() + .map_err(|_| Error::from_reason("Value could not fit in usize".to_string()))?; + + let mut vec = Vec::with_capacity(32); sapling_bls12::MerkleNoteHash::new(sapling_bls12::MerkleNoteHash::combine_hash( &sapling_bls12::SAPLING.clone(), - depth, + converted_depth, &left_hash.0, &right_hash.0, )) - .write(&mut cursor) - .or_else(|err| cx.throw_error(err.to_string()))?; - - // Copy hash to JsBuffer - let mut bytes = cx.buffer(32)?; + .write(&mut vec) + .map_err(|err| Error::from_reason(err.to_string()))?; - cx.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&cursor[..slice.len()]); - }); - - Ok(bytes) + Ok(Buffer::from(vec)) } /// Returns undefined if the note was unable to be decrypted with the given key. - pub fn decrypt_note_for_owner(mut cx: FunctionContext) -> JsResult { - let note = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - let owner_hex_key = cx.argument::(0)?.value(&mut cx); - - let owner_view_key = sapling_bls12::IncomingViewKey::from_hex( + #[napi] + pub fn decrypt_note_for_owner(&self, incoming_hex_key: String) -> Result> { + let incoming_view_key = sapling_bls12::IncomingViewKey::from_hex( sapling_bls12::SAPLING.clone(), - &owner_hex_key, + &incoming_hex_key, ) - .or_else(|err| cx.throw_error(err.to_string()))?; + .map_err(|err| Error::from_reason(err.to_string()))?; - Ok(match note.note.decrypt_note_for_owner(&owner_view_key) { - Ok(note) => cx.boxed(NativeNote { note: { note } }).upcast(), - Err(_) => cx.undefined().upcast(), + Ok(match self.note.decrypt_note_for_owner(&incoming_view_key) { + Ok(note) => Some(NativeNote { note: { note } }), + Err(_) => None, }) } /// Returns undefined if the note was unable to be decrypted with the given key. - pub fn decrypt_note_for_spender(mut cx: FunctionContext) -> JsResult { - let note = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - let spender_hex_key = cx.argument::(0)?.value(&mut cx); - - let spender_view_key = sapling_bls12::OutgoingViewKey::from_hex( + #[napi] + pub fn decrypt_note_for_spender(&self, outgoing_hex_key: String) -> Result> { + let outgoing_view_key = sapling_bls12::OutgoingViewKey::from_hex( sapling_bls12::SAPLING.clone(), - &spender_hex_key, + &outgoing_hex_key, ) - .or_else(|err| cx.throw_error(err.to_string()))?; - + .map_err(|err| Error::from_reason(err.to_string()))?; Ok( - match note.note.decrypt_note_for_spender(&spender_view_key) { - Ok(note) => cx.boxed(NativeNote { note: { note } }).upcast(), - Err(_) => cx.undefined().upcast(), + match self.note.decrypt_note_for_spender(&outgoing_view_key) { + Ok(note) => Some(NativeNote { note: { note } }), + Err(_) => None, }, ) } diff --git a/ironfish-rust-nodejs/src/structs/spend_proof.rs b/ironfish-rust-nodejs/src/structs/spend_proof.rs index f158fe4604..1d2cbead29 100644 --- a/ironfish-rust-nodejs/src/structs/spend_proof.rs +++ b/ironfish-rust-nodejs/src/structs/spend_proof.rs @@ -2,61 +2,38 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::convert::TryInto; - -use neon::prelude::*; +use napi::bindgen_prelude::*; +use napi_derive::napi; use ironfish_rust::sapling_bls12::{MerkleNoteHash, SpendProof}; +#[napi] pub struct NativeSpendProof { pub(crate) proof: SpendProof, } -impl Finalize for NativeSpendProof {} - +#[napi] impl NativeSpendProof { - pub fn tree_size(mut cx: FunctionContext) -> JsResult { - let spend_proof = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - - Ok(cx.number(spend_proof.proof.tree_size())) + #[napi] + pub fn tree_size(&self) -> u32 { + self.proof.tree_size() } - pub fn root_hash(mut cx: FunctionContext) -> JsResult { - let spend_proof = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - - let mut arr: Vec = vec![]; - MerkleNoteHash::new(spend_proof.proof.root_hash()) - .write(&mut arr) - .or_else(|err| cx.throw_error(err.to_string()))?; + #[napi] + pub fn root_hash(&self) -> Result { + let mut vec: Vec = vec![]; - let mut bytes = cx.buffer(arr.len().try_into().unwrap())?; + MerkleNoteHash::new(self.proof.root_hash()) + .write(&mut vec) + .map_err(|err| Error::from_reason(err.to_string()))?; - cx.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&arr[..slice.len()]); - }); - - Ok(bytes) + Ok(Buffer::from(vec)) } - pub fn nullifier(mut cx: FunctionContext) -> JsResult { - let spend_proof = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - - let nullifier = spend_proof.proof.nullifier(); - - let mut bytes = cx.buffer(nullifier.len().try_into().unwrap())?; - - cx.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&nullifier[..slice.len()]); - }); + #[napi] + pub fn nullifier(&self) -> Buffer { + let nullifier = self.proof.nullifier(); - Ok(bytes) + Buffer::from(nullifier.as_ref()) } } diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index 18641b91b9..984e6f2fd1 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -5,7 +5,9 @@ use std::cell::RefCell; use std::convert::TryInto; -use neon::prelude::*; +use napi::bindgen_prelude::*; +use napi::JsBigInt; +use napi_derive::napi; use ironfish_rust::sapling_bls12::{ Key, ProposedTransaction, PublicAddress, SimpleTransaction, Transaction, SAPLING, @@ -15,225 +17,175 @@ use super::note::NativeNote; use super::spend_proof::NativeSpendProof; use super::witness::JsWitness; +#[napi(js_name = "TransactionPosted")] pub struct NativeTransactionPosted { transaction: Transaction, } -impl Finalize for NativeTransactionPosted {} - +#[napi] impl NativeTransactionPosted { - pub fn deserialize(mut cx: FunctionContext) -> JsResult> { - let bytes = cx.argument::(0)?; + #[napi(factory)] + pub fn deserialize(bytes: Buffer) -> Result { + let mut cursor = std::io::Cursor::new(bytes); - let transaction = cx - .borrow(&bytes, |data| { - let mut cursor: std::io::Cursor<&[u8]> = std::io::Cursor::new(data.as_slice()); - Transaction::read(SAPLING.clone(), &mut cursor) - }) - .or_else(|err| cx.throw_error(err.to_string()))?; + let transaction = Transaction::read(SAPLING.clone(), &mut cursor) + .map_err(|err| Error::from_reason(err.to_string()))?; - Ok(cx.boxed(NativeTransactionPosted { transaction })) + Ok(NativeTransactionPosted { transaction }) } - pub fn serialize(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - - let mut arr: Vec = vec![]; - transaction - .transaction - .write(&mut arr) - .or_else(|err| cx.throw_error(err.to_string()))?; - - let mut bytes = cx.buffer(arr.len().try_into().unwrap())?; - - cx.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&arr[..slice.len()]); - }); + #[napi] + pub fn serialize(&self) -> Result { + let mut vec: Vec = vec![]; + self.transaction + .write(&mut vec) + .map_err(|err| Error::from_reason(err.to_string()))?; - Ok(bytes) + Ok(Buffer::from(vec)) } - pub fn verify(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - - Ok(match transaction.transaction.verify() { - Ok(_) => cx.boolean(true), - Err(_e) => cx.boolean(false), - }) + #[napi] + pub fn verify(&self) -> bool { + match self.transaction.verify() { + Ok(_) => true, + Err(_e) => false, + } } - pub fn notes_length(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; + #[napi] + pub fn notes_length(&self) -> Result { + let notes_len: i64 = self + .transaction + .receipts() + .len() + .try_into() + .map_err(|_| Error::from_reason("Value out of range".to_string()))?; - Ok(cx.number(transaction.transaction.receipts().len() as f64)) + Ok(notes_len) } - pub fn get_note(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - let index = cx.argument::(0)?.value(&mut cx) as usize; + #[napi] + pub fn get_note(&self, index: i64) -> Result { + let index_usize: usize = index + .try_into() + .map_err(|_| Error::from_reason("Value out of range".to_string()))?; - let proof = &transaction.transaction.receipts()[index]; + let proof = &self.transaction.receipts()[index_usize]; // Note bytes are 275 - let mut arr: Vec = Vec::with_capacity(275); + let mut vec: Vec = Vec::with_capacity(275); proof .merkle_note() - .write(&mut arr) - .or_else(|err| cx.throw_error(err.to_string()))?; - - let mut bytes = cx.buffer(arr.len() as u32)?; + .write(&mut vec) + .map_err(|err| Error::from_reason(err.to_string()))?; - cx.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&arr[..slice.len()]); - }); - - Ok(bytes) + Ok(Buffer::from(vec)) } - pub fn spends_length(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; + #[napi] + pub fn spends_length(&self) -> Result { + let spends_len: i64 = self + .transaction + .spends() + .len() + .try_into() + .map_err(|_| Error::from_reason("Value out of range".to_string()))?; - Ok(cx.number(transaction.transaction.spends().len() as f64)) + Ok(spends_len) } - pub fn get_spend(mut cx: FunctionContext) -> JsResult> { - let transaction = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - let index = cx.argument::(0)?.value(&mut cx) as usize; + #[napi] + pub fn get_spend(&self, index: i64) -> Result { + let index_usize: usize = index + .try_into() + .map_err(|_| Error::from_reason("Value out of range".to_string()))?; - let proof = &transaction.transaction.spends()[index]; - Ok(cx.boxed(NativeSpendProof { + let proof = &self.transaction.spends()[index_usize]; + Ok(NativeSpendProof { proof: proof.clone(), - })) + }) } - pub fn fee(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - - // TODO: Should be BigInt, but no first-class Neon support - Ok(cx.string(transaction.transaction.transaction_fee().to_string())) + #[napi] + pub fn fee(&self) -> i64n { + i64n(self.transaction.transaction_fee()) } - pub fn transaction_signature(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - + #[napi] + pub fn transaction_signature(&self) -> Result { let mut serialized_signature = vec![]; - transaction - .transaction + self.transaction .binding_signature() .write(&mut serialized_signature) - .or_else(|err| cx.throw_error(err.to_string()))?; + .map_err(|err| Error::from_reason(err.to_string()))?; - let mut bytes = cx.buffer(serialized_signature.len() as u32)?; - - cx.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&serialized_signature[..slice.len()]); - }); - - Ok(bytes) + Ok(Buffer::from(serialized_signature)) } - pub fn hash(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - - let hash = transaction.transaction.transaction_signature_hash(); + #[napi] + pub fn hash(&self) -> Buffer { + let hash = self.transaction.transaction_signature_hash(); - let mut bytes = cx.buffer(hash.len() as u32)?; - - cx.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&hash[..slice.len()]); - }); - - Ok(bytes) + Buffer::from(hash.as_ref()) } - pub fn expiration_sequence(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::, _>(&mut cx)?; - Ok(cx.number(transaction.transaction.expiration_sequence())) + #[napi] + pub fn expiration_sequence(&self) -> u32 { + self.transaction.expiration_sequence() } } -type BoxedNativeTransaction = JsBox>; - +#[napi(js_name = "Transaction")] pub struct NativeTransaction { transaction: ProposedTransaction, } -impl Finalize for NativeTransaction {} +impl Default for NativeTransaction { + fn default() -> Self { + Self::new() + } +} +#[napi] impl NativeTransaction { - pub fn new(mut cx: FunctionContext) -> JsResult { - Ok(cx.boxed(RefCell::new(NativeTransaction { + #[napi(constructor)] + pub fn new() -> NativeTransaction { + NativeTransaction { transaction: ProposedTransaction::new(SAPLING.clone()), - }))) + } } /// Create a proof of a new note owned by the recipient in this transaction. - pub fn receive(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::(&mut cx)?; - let spender_hex_key = cx.argument::(0)?.value(&mut cx); - let note = cx.argument::>(1)?; - + #[napi] + pub fn receive(&mut self, spender_hex_key: String, note: &NativeNote) -> Result { let spender_key = Key::from_hex(SAPLING.clone(), &spender_hex_key) - .or_else(|err| cx.throw_error(err.to_string()))?; - transaction - .borrow_mut() - .transaction + .map_err(|err| Error::from_reason(err.to_string()))?; + self.transaction .receive(&spender_key, ¬e.note) - .or_else(|err| cx.throw_error(err.to_string()))?; - Ok(cx.string("")) + .map_err(|err| Error::from_reason(err.to_string()))?; + Ok("".to_string()) } /// Spend the note owned by spender_hex_key at the given witness location. - pub fn spend(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::(&mut cx)?; - let spender_hex_key = cx.argument::(0)?.value(&mut cx); - let note = cx.argument::>(1)?; - // JsBox - let witness = cx.argument::(2)?; - - let ret = cx.string(""); - + #[napi] + pub fn spend( + &mut self, + env: Env, + spender_hex_key: String, + note: &NativeNote, + witness: Object, + ) -> Result { let w = JsWitness { - cx: RefCell::new(cx), + cx: RefCell::new(env), obj: witness, }; let spender_key = Key::from_hex(SAPLING.clone(), &spender_hex_key) - .or_else(|err| w.cx.borrow_mut().throw_error(err.to_string()))?; - transaction - .borrow_mut() - .transaction + .map_err(|err| Error::from_reason(err.to_string()))?; + self.transaction .spend(spender_key, ¬e.note, &w) - .or_else(|err| w.cx.borrow_mut().throw_error(err.to_string()))?; + .map_err(|err| Error::from_reason(err.to_string()))?; - Ok(ret) + Ok("".to_string()) } /// Special case for posting a miners fee transaction. Miner fee transactions @@ -241,17 +193,13 @@ impl NativeTransaction { /// or change and therefore have a negative transaction fee. In normal use, /// a miner would not accept such a transaction unless it was explicitly set /// as the miners fee. - pub fn post_miners_fee(mut cx: FunctionContext) -> JsResult> { - let transaction = cx - .this() - .downcast_or_throw::(&mut cx)?; - - let transaction = transaction - .borrow_mut() + #[napi(js_name = "post_miners_fee")] + pub fn post_miners_fee(&mut self) -> Result { + let transaction = self .transaction .post_miners_fee() - .or_else(|err| cx.throw_error(err.to_string()))?; - Ok(cx.boxed(NativeTransactionPosted { transaction })) + .map_err(|err| Error::from_reason(err.to_string()))?; + Ok(NativeTransactionPosted { transaction }) } /// Post the transaction. This performs a bit of validation, and signs @@ -264,131 +212,100 @@ impl NativeTransaction { /// /// sum(spends) - sum(outputs) - intended_transaction_fee - change = 0 /// aka: self.transaction_fee - intended_transaction_fee - change = 0 - pub fn post(mut cx: FunctionContext) -> JsResult> { - let transaction = cx - .this() - .downcast_or_throw::(&mut cx)?; - let spender_hex_key = cx.argument::(0)?.value(&mut cx); - let change_goes_to = cx.argument::(1)?.value(&mut cx); - // TODO: Should be BigInt, but no first-class Neon support - let intended_transaction_fee = cx.argument::(2)?.value(&mut cx); - - let intended_transaction_fee_u64 = intended_transaction_fee - .parse::() - .or_else(|err| cx.throw_error(err.to_string()))?; + #[napi] + pub fn post( + &mut self, + spender_hex_key: String, + change_goes_to: Option, + intended_transaction_fee: JsBigInt, + ) -> Result { + let intended_transaction_fee_u64 = intended_transaction_fee.get_u64()?.0; let spender_key = Key::from_hex(SAPLING.clone(), &spender_hex_key) - .or_else(|err| cx.throw_error(err.to_string()))?; - let change_key = if !change_goes_to.is_empty() { - Some( - PublicAddress::from_hex(SAPLING.clone(), &change_goes_to) - .or_else(|err| cx.throw_error(err.to_string()))?, - ) - } else { - None + .map_err(|err| Error::from_reason(err.to_string()))?; + let change_key = match change_goes_to { + Some(address) => Some( + PublicAddress::from_hex(SAPLING.clone(), &address) + .map_err(|err| Error::from_reason(err.to_string()))?, + ), + None => None, }; - let posted_transaction = transaction - .borrow_mut() + let posted_transaction = self .transaction .post(&spender_key, change_key, intended_transaction_fee_u64) - .or_else(|err| cx.throw_error(err.to_string()))?; + .map_err(|err| Error::from_reason(err.to_string()))?; - Ok(cx.boxed(NativeTransactionPosted { + Ok(NativeTransactionPosted { transaction: posted_transaction, - })) + }) } - pub fn set_expiration_sequence(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::(&mut cx)?; - let expiration_sequence = cx.argument::(0)?.value(&mut cx) as u32; - transaction - .borrow_mut() - .transaction + #[napi] + pub fn set_expiration_sequence(&mut self, expiration_sequence: u32) -> Undefined { + self.transaction .set_expiration_sequence(expiration_sequence); - - Ok(cx.undefined()) } } -type BoxedNativeSimpleTransaction = JsBox>; - +#[napi(js_name = "SimpleTransaction")] pub struct NativeSimpleTransaction { transaction: SimpleTransaction, } -impl Finalize for NativeSimpleTransaction {} - +#[napi] impl NativeSimpleTransaction { - pub fn new(mut cx: FunctionContext) -> JsResult { - let spender_hex_key = cx.argument::(0)?.value(&mut cx); - let intended_transaction_fee = cx.argument::(1)?.value(&mut cx); - let intended_transaction_fee_u64 = intended_transaction_fee - .parse::() - .or_else(|err| cx.throw_error(err.to_string()))?; + #[napi(constructor)] + pub fn new( + spender_hex_key: String, + intended_transaction_fee: JsBigInt, + ) -> Result { + let intended_transaction_fee_u64 = intended_transaction_fee.get_u64()?.0; let spender_key = Key::from_hex(SAPLING.clone(), &spender_hex_key) - .or_else(|err| cx.throw_error(err.to_string()))?; - Ok(cx.boxed(RefCell::new(NativeSimpleTransaction { + .map_err(|err| Error::from_reason(err.to_string()))?; + + Ok(NativeSimpleTransaction { transaction: SimpleTransaction::new( SAPLING.clone(), spender_key, intended_transaction_fee_u64, ), - }))) + }) } - pub fn spend(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::(&mut cx)?; - let note = cx.argument::>(0)?; - let w = cx.argument::(1)?; - - let ret = cx.string(""); - + #[napi] + pub fn spend(&mut self, env: Env, note: &NativeNote, witness: Object) -> Result { let witness = JsWitness { - cx: RefCell::new(cx), - obj: w, + cx: RefCell::new(env), + obj: witness, }; - transaction - .borrow_mut() - .transaction + self.transaction .spend(¬e.note, &witness) - .or_else(|err| witness.cx.borrow_mut().throw_error(err.to_string()))?; + .map_err(|err| Error::from_reason(err.to_string()))?; - Ok(ret) + Ok("".to_string()) } - pub fn receive(mut cx: FunctionContext) -> JsResult { - let transaction = cx - .this() - .downcast_or_throw::(&mut cx)?; - let note = cx.argument::>(0)?; - - transaction - .borrow_mut() - .transaction + #[napi] + pub fn receive(&mut self, note: &NativeNote) -> Result { + self.transaction .receive(¬e.note) - .or_else(|err| cx.throw_error(err.to_string()))?; - Ok(cx.string("")) - } + .map_err(|err| Error::from_reason(err.to_string()))?; - pub fn post(mut cx: FunctionContext) -> JsResult> { - let transaction = cx - .this() - .downcast_or_throw::(&mut cx)?; + Ok("".to_string()) + } - let posted_transaction = transaction - .borrow_mut() + #[napi] + pub fn post(&mut self) -> Result { + let posted_transaction = self .transaction .post() - .or_else(|err| cx.throw_error(err.to_string()))?; - Ok(cx.boxed(NativeTransactionPosted { + .map_err(|err| Error::from_reason(err.to_string()))?; + + Ok(NativeTransactionPosted { transaction: posted_transaction, - })) + }) } } diff --git a/ironfish-rust-nodejs/src/structs/witness.rs b/ironfish-rust-nodejs/src/structs/witness.rs index 330e12454c..95cad2a17b 100644 --- a/ironfish-rust-nodejs/src/structs/witness.rs +++ b/ironfish-rust-nodejs/src/structs/witness.rs @@ -3,153 +3,115 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::RefCell; -use std::ops::DerefMut; +use std::ops::Deref; -use neon::prelude::*; +use napi::bindgen_prelude::*; +use napi::Env; +use napi::JsObject; use ironfish_rust::sapling_bls12::{Bls12, Fr, MerkleNoteHash}; use ironfish_rust::witness::{WitnessNode, WitnessTrait}; -pub struct JsWitness<'a> { - pub cx: RefCell>, - pub obj: Handle<'a, JsObject>, +pub struct JsWitness { + pub cx: RefCell, + pub obj: Object, } /// Implements WitnessTrait on JsWitness so that witnesses from the /// TypeScript side can be passed into classes that require witnesses, /// like transactions. -impl WitnessTrait for JsWitness<'_> { +impl WitnessTrait for JsWitness { fn verify(&self, hash: &MerkleNoteHash) -> bool { - let mut cx = self.cx.borrow_mut(); - let cxm = cx.deref_mut(); + let f: JsFunction = self.obj.get("verify").unwrap().unwrap(); - let f = self - .obj - .get(cxm, "verify") - .unwrap() - .downcast_or_throw::(cxm) - .unwrap(); + let cx = self.cx.borrow(); let mut arr: Vec = vec![]; hash.write(&mut arr).unwrap(); - let mut bytes = cxm.buffer(arr.len() as u32).unwrap(); - cxm.borrow_mut(&mut bytes, |data| { - let slice = data.as_mut_slice(); - slice.clone_from_slice(&arr[..slice.len()]); - }); + let buf = cx.create_buffer_with_data(arr).unwrap().into_raw(); + let args = [buf]; - f.call(cxm, self.obj, vec![bytes]) + f.call(Some(&self.obj), &args) + .unwrap() + .coerce_to_bool() .unwrap() - .downcast_or_throw::(cxm) + .get_value() .unwrap() - .value(cxm) } fn get_auth_path(&self) -> Vec> { - let mut cx = self.cx.borrow_mut(); - let cxm = cx.deref_mut(); + let f: JsFunction = self.obj.get("authPath").unwrap().unwrap(); - let f = self - .obj - .get(cxm, "authPath") + let args: &[napi::JsBuffer; 0] = &[]; + let arr: JsObject = f + .call(Some(&self.obj), args) .unwrap() - .downcast_or_throw::(cxm) + .coerce_to_object() .unwrap(); - let arr = f - .call::<_, _, JsArray, _>(cxm, self.obj, vec![]) - .unwrap() - .downcast_or_throw::(cxm) - .unwrap() - .to_vec(cxm) - .unwrap(); + let len = arr.get_array_length().unwrap(); + + let mut witness_nodes: Vec> = vec![]; + + for i in 0..len { + let element: JsObject = arr.get_element(i).unwrap(); + + let hash_of_sibling: JsFunction = element.get("hashOfSibling").unwrap().unwrap(); + + let bytes: napi::JsBuffer = hash_of_sibling + .call(Some(&element), args) + .unwrap() + .try_into() + .unwrap(); + + let fr = MerkleNoteHash::read(bytes.into_value().unwrap().deref()) + .unwrap() + .0; - arr.iter() - .map(|element| { - let cast = element.downcast_or_throw::(cxm).unwrap(); - - let bytes = cast - .get(cxm, "hashOfSibling") - .unwrap() - .downcast_or_throw::(cxm) - .unwrap() - .call::<_, _, JsBuffer, _>(cxm, cast, vec![]) - .unwrap() - .downcast_or_throw::(cxm) - .unwrap(); - - // hashOfSibling returns a serialized hash, so convert it - // back into a MerkleNoteHash - let fr = cxm - .borrow(&bytes, |data| { - let mut cursor: std::io::Cursor<&[u8]> = - std::io::Cursor::new(data.as_slice()); - MerkleNoteHash::read(&mut cursor) - }) - .unwrap() - .0; - - let side = cast - .get(cxm, "side") - .unwrap() - .downcast_or_throw::(cxm) - .unwrap() - .call::<_, _, JsString, _>(cxm, cast, vec![]) - .unwrap() - .downcast_or_throw::(cxm) - .unwrap() - .value(cxm); - - if side == "Left" { - WitnessNode::Left(fr) - } else { - WitnessNode::Right(fr) - } - }) - .collect() + let side_fn: JsFunction = element.get("side").unwrap().unwrap(); + + let side_utf8 = side_fn + .call(Some(&element), args) + .unwrap() + .coerce_to_string() + .unwrap() + .into_utf8() + .unwrap(); + let side = side_utf8.as_str().unwrap(); + + if side == "Left" { + witness_nodes.push(WitnessNode::Left(fr)) + } else { + witness_nodes.push(WitnessNode::Right(fr)) + } + } + + witness_nodes } fn root_hash(&self) -> Fr { - let mut cx = self.cx.borrow_mut(); - let cxm = cx.deref_mut(); + let f: JsFunction = self.obj.get("serializeRootHash").unwrap().unwrap(); - let f = self - .obj - .get(cxm, "serializeRootHash") - .unwrap() - .downcast_or_throw::(cxm) - .unwrap(); + let args: &[napi::JsBuffer; 0] = &[]; - let bytes = f - .call::<_, _, JsBuffer, _>(cxm, self.obj, vec![]) - .unwrap() - .downcast_or_throw::(cxm) - .unwrap(); + let bytes: napi::JsBuffer = f.call(Some(&self.obj), args).unwrap().try_into().unwrap(); - cx.borrow(&bytes, |data| { - let mut cursor: std::io::Cursor<&[u8]> = std::io::Cursor::new(data.as_slice()); - MerkleNoteHash::read(&mut cursor) - }) - .unwrap() - .0 + MerkleNoteHash::read(bytes.into_value().unwrap().deref()) + .unwrap() + .0 } fn tree_size(&self) -> u32 { - let mut cx = self.cx.borrow_mut(); - let cxm = cx.deref_mut(); + let f: JsFunction = self.obj.get("treeSize").unwrap().unwrap(); - let f = self - .obj - .get(cxm, "treeSize") - .unwrap() - .downcast_or_throw::(cxm) - .unwrap(); + let args: &[napi::JsBuffer; 0] = &[]; - f.call::<_, _, JsNumber, _>(cxm, self.obj, vec![]) + f.call(Some(&self.obj), args) + .unwrap() + .coerce_to_number() .unwrap() - .downcast_or_throw::(cxm) + .get_uint32() .unwrap() - .value(cxm) as u32 } } diff --git a/ironfish-rust-nodejs/tsconfig.json b/ironfish-rust-nodejs/tsconfig.json deleted file mode 100644 index 2089b02264..0000000000 --- a/ironfish-rust-nodejs/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../config/tsconfig.base.json", - "compilerOptions": { - "baseUrl": ".", - "outDir": ".", - }, - "include": ["index.ts", "index.node", "index.node.d.ts"], - "exclude": [] -} diff --git a/ironfish-rust/src/keys/public_address.rs b/ironfish-rust/src/keys/public_address.rs index 6a4969e7e3..b4c3ce7bbc 100644 --- a/ironfish-rust/src/keys/public_address.rs +++ b/ironfish-rust/src/keys/public_address.rs @@ -69,7 +69,7 @@ impl PublicAddress { sapling_key: &SaplingKey, diversifier: &[u8; 11], ) -> Result, errors::SaplingKeyError> { - Self::from_view_key(&sapling_key.incoming_view_key(), diversifier) + Self::from_view_key(sapling_key.incoming_view_key(), diversifier) } pub fn from_view_key( diff --git a/ironfish-rust/src/keys/view_keys.rs b/ironfish-rust/src/keys/view_keys.rs index 4e2c8a31a0..0b30fbfef2 100644 --- a/ironfish-rust/src/keys/view_keys.rs +++ b/ironfish-rust/src/keys/view_keys.rs @@ -49,7 +49,7 @@ impl IncomingViewKey { sapling: Arc>, value: &str, ) -> Result { - match hex_to_bytes(&value) { + match hex_to_bytes(value) { Err(()) => Err(errors::SaplingKeyError::InvalidViewingKey), Ok(bytes) => { if bytes.len() != 32 { @@ -155,7 +155,7 @@ impl OutgoingViewKey { sapling: Arc>, value: &str, ) -> Result { - match hex_to_bytes(&value) { + match hex_to_bytes(value) { Err(()) => Err(errors::SaplingKeyError::InvalidViewingKey), Ok(bytes) => { if bytes.len() != 32 { @@ -245,7 +245,7 @@ pub(crate) fn shared_secret( let shared_secret = point_to_bytes(&other_public_key.mul(*secret_key, jubjub)) .expect("should be able to convert point to bytes"); let reference_bytes = - point_to_bytes(&reference_public_key).expect("should be able to convert point to bytes"); + point_to_bytes(reference_public_key).expect("should be able to convert point to bytes"); let mut hasher = Blake2b::new() .hash_length(32) @@ -255,6 +255,6 @@ pub(crate) fn shared_secret( hasher.update(&shared_secret); hasher.update(&reference_bytes); let mut hash_result = [0; 32]; - hash_result[..].clone_from_slice(&hasher.finalize().as_ref()); + hash_result[..].clone_from_slice(hasher.finalize().as_ref()); hash_result } diff --git a/ironfish-rust/src/merkle_note.rs b/ironfish-rust/src/merkle_note.rs index 93a6f2cac9..cfe50902fd 100644 --- a/ironfish-rust/src/merkle_note.rs +++ b/ironfish-rust/src/merkle_note.rs @@ -90,10 +90,10 @@ impl MerkleNote { key_bytes[32..].clone_from_slice(secret_key.to_repr().as_ref()); let encryption_key = calculate_key_for_encryption_keys( - &spender_key.outgoing_view_key(), + spender_key.outgoing_view_key(), &value_commitment.cm(&spender_key.sapling.jubjub).into(), ¬e.commitment_point(), - &public_key, + public_key, ); let mut note_encryption_keys = [0; ENCRYPTED_SHARED_KEY_SIZE + aead::MAC_SIZE]; aead::encrypt(&encryption_key, &key_bytes, &mut note_encryption_keys); diff --git a/ironfish-rust/src/merkle_note_hash.rs b/ironfish-rust/src/merkle_note_hash.rs index d1557ee3ed..90476c0634 100644 --- a/ironfish-rust/src/merkle_note_hash.rs +++ b/ironfish-rust/src/merkle_note_hash.rs @@ -28,7 +28,7 @@ impl MerkleNoteHash { MerkleNoteHash(fr) } - pub fn read(reader: &mut R) -> io::Result> { + pub fn read(reader: R) -> io::Result> { let res = read_scalar(reader).map_err(|_| { io::Error::new(io::ErrorKind::InvalidInput, "Unable to convert note hash") }); diff --git a/ironfish-rust/src/mining.rs b/ironfish-rust/src/mining.rs index 9d9e57e694..89062e2a99 100644 --- a/ironfish-rust/src/mining.rs +++ b/ironfish-rust/src/mining.rs @@ -39,7 +39,7 @@ pub fn mine_header_batch( for i in 0..batch_size { let randomness = randomize_header(initial_randomness, i, header_bytes); - let hash = blake3::hash(&header_bytes); + let hash = blake3::hash(header_bytes); let new_target_bytes = hash.as_bytes(); if bytes_lte(new_target_bytes, target) { diff --git a/ironfish-rust/src/serializing.rs b/ironfish-rust/src/serializing.rs index f5c26cd7ce..e5a0ca4be8 100644 --- a/ironfish-rust/src/serializing.rs +++ b/ironfish-rust/src/serializing.rs @@ -111,13 +111,13 @@ pub(crate) mod aead { pub(crate) fn decrypt( key: &[u8], ciphertext: &[u8], - mut plaintext_output: &mut [u8], + plaintext_output: &mut [u8], ) -> Result<(), errors::NoteError> { assert!(plaintext_output.len() == ciphertext.len() - MAC_SIZE); let mut decryptor = ChaCha20Poly1305::new(key, &[0; 8], &[0; 8]); let success = decryptor.decrypt( &ciphertext[..ciphertext.len() - MAC_SIZE], - &mut plaintext_output, + plaintext_output, &ciphertext[ciphertext.len() - MAC_SIZE..], ); diff --git a/ironfish-rust/src/spending.rs b/ironfish-rust/src/spending.rs index 283d7d0e30..1859f26664 100644 --- a/ironfish-rust/src/spending.rs +++ b/ironfish-rust/src/spending.rs @@ -280,8 +280,8 @@ impl SpendProof { mut reader: R, ) -> Result { let proof = groth16::Proof::read(&mut reader)?; - let value_commitment = edwards::Point::::read(&mut reader, &jubjub)?; - let randomized_public_key = redjubjub::PublicKey::::read(&mut reader, &jubjub)?; + let value_commitment = edwards::Point::::read(&mut reader, jubjub)?; + let randomized_public_key = redjubjub::PublicKey::::read(&mut reader, jubjub)?; let root_hash = read_scalar(&mut reader)?; let tree_size = reader.read_u32::()?; let mut nullifier = [0; 32]; diff --git a/ironfish-rust/src/transaction/mod.rs b/ironfish-rust/src/transaction/mod.rs index 5c9b16ee9b..3d4113e09d 100644 --- a/ironfish-rust/src/transaction/mod.rs +++ b/ironfish-rust/src/transaction/mod.rs @@ -166,7 +166,7 @@ impl ProposedTransaction { change_amount as u64, // we checked it was positive Memo([0; 32]), ); - self.receive(&spender_key, &change_note)?; + self.receive(spender_key, &change_note)?; } self._partial_post() } @@ -254,7 +254,7 @@ impl ProposedTransaction { } let mut hash_result = [0; 32]; - hash_result[..].clone_from_slice(&hasher.finalize().as_ref()); + hash_result[..].clone_from_slice(hasher.finalize().as_ref()); hash_result } @@ -529,7 +529,7 @@ impl Transaction { } let mut hash_result = [0; 32]; - hash_result[..].clone_from_slice(&hasher.finalize().as_ref()); + hash_result[..].clone_from_slice(hasher.finalize().as_ref()); hash_result } diff --git a/ironfish/package.json b/ironfish/package.json index e12fa60321..d9cca01e00 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -39,8 +39,8 @@ "lint:fix": "tsc -b && tsc -b tsconfig.test.json && eslint --ext .ts,.tsx,.js,.jsx src/ --fix", "start": "tsc -b -w", "test": "tsc -b && tsc -b tsconfig.test.json && jest", - "test:slow": "tsc -b && tsc -b tsconfig.test.json && TEST_INIT_RUST=true jest --testMatch \"**/*.test.slow.ts\" --testPathIgnorePatterns", - "test:perf": "tsc -b && tsc -b tsconfig.test.json && TEST_INIT_RUST=true jest --testMatch \"**/*.test.perf.ts\" --testPathIgnorePatterns", + "test:slow": "tsc -b && tsc -b tsconfig.test.json && cross-env TEST_INIT_RUST=true jest --testMatch \"**/*.test.slow.ts\" --testPathIgnorePatterns", + "test:perf": "tsc -b && tsc -b tsconfig.test.json && cross-env TEST_INIT_RUST=true jest --testMatch \"**/*.test.perf.ts\" --testPathIgnorePatterns", "test:coverage:html": "tsc -b tsconfig.test.json && jest --coverage --coverage-reporters html --testPathIgnorePatterns", "test:watch": "tsc -b tsconfig.test.json && jest --watch --coverage false" }, @@ -57,6 +57,7 @@ "@types/yup": "0.29.10", "@typescript-eslint/eslint-plugin": "4.28.1", "@typescript-eslint/parser": "4.28.1", + "cross-env": "7.0.3", "eslint": "7.29.0", "eslint-config-ironfish": "*", "eslint-config-prettier": "8.3.0", diff --git a/ironfish/src/mining/miner.test.ts b/ironfish/src/mining/miner.test.ts index 77f0d00ec9..23fd0e2591 100644 --- a/ironfish/src/mining/miner.test.ts +++ b/ironfish/src/mining/miner.test.ts @@ -7,7 +7,9 @@ import { mocked } from 'ts-jest/utils' import { mineHeader } from './mineHeader' import { Miner, MineRequest } from './miner' -jest.mock('ironfish-rust-nodejs') +jest.mock('ironfish-rust-nodejs', () => ({ + mineHeaderBatch: jest.fn(), +})) const testBatchSize = 100 /** diff --git a/ironfish/src/primitives/note.ts b/ironfish/src/primitives/note.ts index f39d656e43..e060f427a9 100644 --- a/ironfish/src/primitives/note.ts +++ b/ironfish/src/primitives/note.ts @@ -29,19 +29,18 @@ export class Note { this.referenceCount-- if (this.referenceCount <= 0) { this.referenceCount = 0 - this.note?.free() this.note = null } } value(): bigint { - const value = this.takeReference().value + const value = this.takeReference().value() this.returnReference() - return value.valueOf() + return value } memo(): string { - const memo = this.takeReference().memo + const memo = this.takeReference().memo() this.returnReference() return memo } diff --git a/ironfish/src/primitives/noteEncrypted.ts b/ironfish/src/primitives/noteEncrypted.ts index 7452d4d9a2..f7eba40a72 100644 --- a/ironfish/src/primitives/noteEncrypted.ts +++ b/ironfish/src/primitives/noteEncrypted.ts @@ -35,7 +35,6 @@ export class NoteEncrypted { this.referenceCount-- if (this.referenceCount <= 0) { this.referenceCount = 0 - this.noteEncrypted?.free() this.noteEncrypted = null } } @@ -45,7 +44,6 @@ export class NoteEncrypted { this.returnReference() if (note) { const serializedNote = note.serialize() - note.free() return new Note(Buffer.from(serializedNote)) } } @@ -55,7 +53,6 @@ export class NoteEncrypted { this.returnReference() if (note) { const serializedNote = note.serialize() - note.free() return new Note(Buffer.from(serializedNote)) } } diff --git a/ironfish/src/primitives/transaction.ts b/ironfish/src/primitives/transaction.ts index 797c6f2993..50ec7863d7 100644 --- a/ironfish/src/primitives/transaction.ts +++ b/ironfish/src/primitives/transaction.ts @@ -48,7 +48,6 @@ export class Transaction { this.referenceCount-- if (this.referenceCount <= 0) { this.referenceCount = 0 - this.transactionPosted?.free() this.transactionPosted = null } } @@ -83,7 +82,7 @@ export class Transaction { * The number of notes in the transaction. */ notesLength(): number { - return this.withReference((t) => t.notesLength) + return this.withReference((t) => t.notesLength()) } getNote(index: number): NoteEncrypted { @@ -112,7 +111,7 @@ export class Transaction { * The number of spends in the transaction. */ spendsLength(): number { - return this.withReference((t) => t.spendsLength) + return this.withReference((t) => t.spendsLength()) } /** @@ -133,12 +132,11 @@ export class Transaction { const spend = t.getSpend(index) const jsSpend = { - size: spend.treeSize, - nullifier: Buffer.from(spend.nullifier), - commitment: Buffer.from(spend.rootHash), + size: spend.treeSize(), + nullifier: spend.nullifier(), + commitment: spend.rootHash(), } - spend.free() return jsSpend }) } @@ -165,14 +163,14 @@ export class Transaction { * Get transaction signature for this transaction. */ transactionSignature(): Buffer { - return this.withReference((t) => Buffer.from(t.transactionSignature)) + return this.withReference((t) => t.transactionSignature()) } /** * Get the transaction hash. */ hash(): TransactionHash { - return this.withReference((t) => Buffer.from(t.hash)) + return this.withReference((t) => t.hash()) } equals(other: Transaction): boolean { @@ -180,7 +178,7 @@ export class Transaction { } expirationSequence(): number { - return this.withReference((t) => t.expirationSequence) + return this.withReference((t) => t.expirationSequence()) } } diff --git a/ironfish/src/strategy.test.slow.ts b/ironfish/src/strategy.test.slow.ts index d04feeab5b..b6f86d11d8 100644 --- a/ironfish/src/strategy.test.slow.ts +++ b/ironfish/src/strategy.test.slow.ts @@ -104,7 +104,7 @@ describe('Demonstrate the Sapling API', () => { expect(transaction.receive(spenderKey.spending_key, minerNote)).toBe('') minerTransaction = transaction.post_miners_fee() expect(minerTransaction).toBeTruthy() - expect(minerTransaction.notesLength).toEqual(1) + expect(minerTransaction.notesLength()).toEqual(1) }) it('Can verify the miner transaction', () => { @@ -114,7 +114,7 @@ describe('Demonstrate the Sapling API', () => { }) it('Can add the miner transaction note to the tree', async () => { - for (let i = 0; i < minerTransaction.notesLength; i++) { + for (let i = 0; i < minerTransaction.notesLength(); i++) { const note = Buffer.from(minerTransaction.getNote(i)) await tree.add(new NoteEncrypted(note)) } @@ -150,19 +150,19 @@ describe('Demonstrate the Sapling API', () => { it('Can verify the transaction', async () => { expect(publicTransaction.verify()).toBeTruthy() - for (let i = 0; i < publicTransaction.notesLength; i++) { + for (let i = 0; i < publicTransaction.notesLength(); i++) { const note = Buffer.from(publicTransaction.getNote(i)) await tree.add(new NoteEncrypted(note)) } }) it('Exposes binding signature on the transaction', () => { - const hex_signature = Buffer.from(publicTransaction.transactionSignature).toString('hex') + const hex_signature = publicTransaction.transactionSignature().toString('hex') expect(hex_signature.toString().length).toBe(128) }) it('Exposes transaction hash', () => { - expect(publicTransaction.hash.length).toBe(32) + expect(publicTransaction.hash().length).toBe(32) }) }) diff --git a/ironfish/src/workerPool/tasks/createMinersFee.ts b/ironfish/src/workerPool/tasks/createMinersFee.ts index 375bcfe1c7..54b7137af2 100644 --- a/ironfish/src/workerPool/tasks/createMinersFee.ts +++ b/ironfish/src/workerPool/tasks/createMinersFee.ts @@ -33,9 +33,5 @@ export function handleCreateMinersFee({ const serializedTransactionPosted = Buffer.from(postedTransaction.serialize()) - minerNote.free() - transaction.free() - postedTransaction.free() - return { type: 'createMinersFee', serializedTransactionPosted } } diff --git a/ironfish/src/workerPool/tasks/createTransaction.ts b/ironfish/src/workerPool/tasks/createTransaction.ts index 8e5ca5bdf6..1167aafa2b 100644 --- a/ironfish/src/workerPool/tasks/createTransaction.ts +++ b/ironfish/src/workerPool/tasks/createTransaction.ts @@ -49,21 +49,16 @@ export function handleCreateTransaction({ note, new Witness(spend.treeSize, spend.rootHash, spend.authPath, noteHasher), ) - note.free() } for (const { publicAddress, amount, memo } of receives) { const note = new Note(publicAddress, amount, memo) transaction.receive(spendKey, note) - note.free() } const postedTransaction = transaction.post(spendKey, undefined, transactionFee) const serializedTransactionPosted = Buffer.from(postedTransaction.serialize()) - transaction.free() - postedTransaction.free() - return { type: 'createTransaction', serializedTransactionPosted } } diff --git a/ironfish/src/workerPool/tasks/getUnspentNotes.ts b/ironfish/src/workerPool/tasks/getUnspentNotes.ts index 8ad3093458..d855d86019 100644 --- a/ironfish/src/workerPool/tasks/getUnspentNotes.ts +++ b/ironfish/src/workerPool/tasks/getUnspentNotes.ts @@ -27,7 +27,7 @@ export function handleGetUnspentNotes({ const results: GetUnspentNotesResponse['notes'] = [] - for (let i = 0; i < transaction.notesLength; i++) { + for (let i = 0; i < transaction.notesLength(); i++) { const serializedNote = transaction.getNote(i) const note = NoteEncrypted.deserialize(serializedNote) diff --git a/ironfish/src/workerPool/tasks/transactionFee.ts b/ironfish/src/workerPool/tasks/transactionFee.ts index 768d866d48..88b73dd9b7 100644 --- a/ironfish/src/workerPool/tasks/transactionFee.ts +++ b/ironfish/src/workerPool/tasks/transactionFee.ts @@ -18,9 +18,7 @@ export function handleTransactionFee({ serializedTransactionPosted, }: TransactionFeeRequest): TransactionFeeResponse { const transaction = TransactionPosted.deserialize(serializedTransactionPosted) - const fee = transaction.fee + const fee = transaction.fee() - transaction.free() - - return { type: 'transactionFee', transactionFee: fee.valueOf() } + return { type: 'transactionFee', transactionFee: fee } } diff --git a/ironfish/src/workerPool/tasks/verifyTransaction.ts b/ironfish/src/workerPool/tasks/verifyTransaction.ts index 88f83e2f3f..139c14c32f 100644 --- a/ironfish/src/workerPool/tasks/verifyTransaction.ts +++ b/ironfish/src/workerPool/tasks/verifyTransaction.ts @@ -29,15 +29,13 @@ export function handleVerifyTransaction({ try { transaction = TransactionPosted.deserialize(serializedTransactionPosted) - if (verifyFees && transaction.fee < BigInt(0)) { + if (verifyFees && transaction.fee() < BigInt(0)) { throw new Error('Transaction has negative fees') } verified = transaction.verify() } catch { verified = false - } finally { - transaction?.free() } return { type: 'verify', verified } diff --git a/ironfish/tsconfig.test.json b/ironfish/tsconfig.test.json index e3ec8bfa43..641a393a95 100644 --- a/ironfish/tsconfig.test.json +++ b/ironfish/tsconfig.test.json @@ -5,7 +5,5 @@ "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo" }, "include": ["src", "package.json"], - "references": [ - { "path": "../ironfish-rust-nodejs" }, - ] + "references": [], } diff --git a/rust-toolchain b/rust-toolchain index fc2daadbe3..0472a57dc6 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.52.1 \ No newline at end of file +1.58.1 \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9012d49cfd..c1d29279aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1292,6 +1292,11 @@ "@napi-rs/blake-hash-win32-ia32-msvc" "1.3.1" "@napi-rs/blake-hash-win32-x64-msvc" "1.3.1" +"@napi-rs/cli@2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@napi-rs/cli/-/cli-2.4.2.tgz#89b32c7d8776004bc9617915605aea769339cf6f" + integrity sha512-+yCOuPqernvD8BMphbadF87ElaJ0rjanOZrbnauaEdR07YyoalGw3FTk15HHyflIwQKlYd69gkG5EM4WFkICKw== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2804,11 +2809,6 @@ cardinal@^2.1.1: ansicolors "~0.3.2" redeyed "~2.1.0" -cargo-cp-artifact@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/cargo-cp-artifact/-/cargo-cp-artifact-0.1.5.tgz#2c35f7d658f22acfe9b1fa2774344d9a389efd14" - integrity sha512-mWwNdfrEyvMPxDHoAhbCYwQBNP3RyW9bV+yi5VaaHtVZqoDXbGU5JQF5qG7s65SJhqP7HIVAQD7wZM6Fk2COuw== - caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" From ea063c9d38aaaa116d62902daa567bd837cf439d Mon Sep 17 00:00:00 2001 From: Alexander Decurnou Date: Wed, 9 Feb 2022 17:47:48 -0500 Subject: [PATCH 03/24] Peer Persistence (#938) * Adjust PeerAddress type * Add hostsStore to sdk.ts * Move hostsStore instantiation to AddressManager * Add helper functions for tests; fix filesystem access * Implement AddressManager functions; implement tests * Implement persistence, remove min version check for requests, fix tests * Change request interval to 60 secs, and request on peer connect * Add missing dataDir option to peerManager instantiation * Smaller nits first * Remove addAddressesFromPeerList, change parameters * Relax strictness of filtering for removal * Adjust tests * Flatten elses, simplify logic * Use connect attempts along with random peer sample * Add TODO comment * Move request send to its own handler; fix tests * Move hostsStore instantiation to node; fix tests Having the hostsStore instantiation in the AddressManager requires an await on the load() for the backing file store. This leads to a floating promise, which should be addressed. As one cannot use async/await inside a class constructor, the only other choices are to do something similar to what we already do in ironfish/src/node.ts in which we make a static async init() function, or to instantiate and load the hosts store earlier in the node setup and pass it to PeerManager for use in creating AddressManager. This PR is already fairly large, so I am not sure that adding init functions at every necessary point is the way to go when we can instead load the hosts store at the same time as the other file stores. In the future, we can create the aforementioned init() functions if we deem it necessary. * Supply non-disconnected peer identities only --- ironfish/src/fileStores/fileStore.ts | 9 +- ironfish/src/fileSystems/fileSystem.ts | 1 + ironfish/src/fileSystems/nodeFileSystem.ts | 5 + .../messageRouters/fireAndForget.test.ts | 10 +- .../network/messageRouters/globalRpc.test.ts | 25 +- .../src/network/messageRouters/gossip.test.ts | 21 +- .../src/network/messageRouters/rpc.test.ts | 13 +- ironfish/src/network/peerNetwork.test.ts | 41 ++- ironfish/src/network/peerNetwork.ts | 10 +- .../src/network/peers/addressManager.test.ts | 116 ++++++++- ironfish/src/network/peers/addressManager.ts | 81 +++++- ironfish/src/network/peers/peerAddress.ts | 4 +- .../peers/peerConnectionManager.test.ts | 17 +- .../network/peers/peerConnectionManager.ts | 7 + .../src/network/peers/peerManager.test.ts | 244 +++++------------- ironfish/src/network/peers/peerManager.ts | 121 +++++---- ironfish/src/network/testUtilities/helpers.ts | 13 + .../network/testUtilities/mockHostsStore.ts | 17 +- ironfish/src/node.ts | 13 +- ironfish/src/sdk.ts | 5 + 20 files changed, 447 insertions(+), 326 deletions(-) diff --git a/ironfish/src/fileStores/fileStore.ts b/ironfish/src/fileStores/fileStore.ts index 2a52d5dbdb..60f4cc1c82 100644 --- a/ironfish/src/fileStores/fileStore.ts +++ b/ironfish/src/fileStores/fileStore.ts @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { promises as fs } from 'fs' import path from 'path' import { FileSystem } from '../fileSystems' import { JSONUtils, PartialRecursive } from '../utils' @@ -22,7 +21,7 @@ export class FileStore> { } async load(): Promise | null> { - const configExists = await fs + const configExists = await this.files .access(this.configPath) .then(() => true) .catch(() => false) @@ -30,7 +29,7 @@ export class FileStore> { let config = null if (configExists) { - const data = await fs.readFile(this.configPath, { encoding: 'utf8' }) + const data = await this.files.readFile(this.configPath) config = JSONUtils.parse>(data, this.configName) } @@ -39,7 +38,7 @@ export class FileStore> { async save(data: PartialRecursive): Promise { const json = JSON.stringify(data, undefined, ' ') - await fs.mkdir(this.dataDir, { recursive: true }) - await fs.writeFile(this.configPath, json) + await this.files.mkdir(this.dataDir, { recursive: true }) + await this.files.writeFile(this.configPath, json) } } diff --git a/ironfish/src/fileSystems/fileSystem.ts b/ironfish/src/fileSystems/fileSystem.ts index 3210c2007e..cd5b2d3a99 100644 --- a/ironfish/src/fileSystems/fileSystem.ts +++ b/ironfish/src/fileSystems/fileSystem.ts @@ -5,6 +5,7 @@ import type fs from 'fs' export abstract class FileSystem { abstract init(): Promise + abstract access(path: fs.PathLike, mode?: number | undefined): Promise abstract writeFile( path: string, data: string, diff --git a/ironfish/src/fileSystems/nodeFileSystem.ts b/ironfish/src/fileSystems/nodeFileSystem.ts index ffd0544ca4..2d92184c13 100644 --- a/ironfish/src/fileSystems/nodeFileSystem.ts +++ b/ironfish/src/fileSystems/nodeFileSystem.ts @@ -19,6 +19,11 @@ export class NodeFileProvider extends FileSystem { return this } + async access(path: fs.PathLike, mode?: number | undefined): Promise { + Assert.isNotNull(this.fs, `Must call FileSystem.init()`) + await this.fs.access(path, mode) + } + async writeFile( path: string, data: string, diff --git a/ironfish/src/network/messageRouters/fireAndForget.test.ts b/ironfish/src/network/messageRouters/fireAndForget.test.ts index 859ffe85de..40bfe9d6d8 100644 --- a/ironfish/src/network/messageRouters/fireAndForget.test.ts +++ b/ironfish/src/network/messageRouters/fireAndForget.test.ts @@ -21,7 +21,7 @@ jest.useFakeTimers() describe('FireAndForget Router', () => { it('sends a fire and forget message', () => { - const peers = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const peers = new PeerManager(mockLocalPeer(), mockHostsStore()) const sendToMock = jest.spyOn(peers, 'sendTo') const router = new FireAndForgetRouter(peers) @@ -34,7 +34,7 @@ describe('FireAndForget Router', () => { }) it('handles an incoming fire and forget message', async () => { - const peers = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const peers = new PeerManager(mockLocalPeer(), mockHostsStore()) const router = new FireAndForgetRouter(peers) const handleMock = jest.fn((_message: IncomingFireAndForgetGeneric<'incoming'>) => @@ -52,6 +52,8 @@ describe('FireAndForget Router', () => { }) it('routes a fire and forget message as fire and forget', async () => { + const addressManager = new AddressManager(mockHostsStore()) + addressManager.hostsStore = mockHostsStore() const network = new PeerNetwork({ identity: mockPrivateIdentity('local'), agent: 'sdk/1/cli', @@ -59,7 +61,7 @@ describe('FireAndForget Router', () => { node: mockNode(), chain: mockChain(), strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) const fireAndForgetMock = jest.fn(async () => {}) @@ -79,6 +81,6 @@ describe('FireAndForget Router', () => { }) expect(fireAndForgetMock).toBeCalled() - network.stop() + await network.stop() }) }) diff --git a/ironfish/src/network/messageRouters/globalRpc.test.ts b/ironfish/src/network/messageRouters/globalRpc.test.ts index 59e057b39c..5872dc3bbc 100644 --- a/ironfish/src/network/messageRouters/globalRpc.test.ts +++ b/ironfish/src/network/messageRouters/globalRpc.test.ts @@ -8,7 +8,6 @@ jest.mock('ws') import '../testUtilities' import { mocked } from 'ts-jest/utils' import { InternalMessageType, MessageType } from '../messages' -import { AddressManager } from '../peers/addressManager' import { PeerManager } from '../peers/peerManager' import { getConnectedPeer, mockHostsStore, mockLocalPeer } from '../testUtilities' import { GlobalRpcRouter } from './globalRpc' @@ -25,7 +24,7 @@ describe('select peers', () => { it('Returns null when no peers available', () => { const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) router.register('take', jest.fn()) @@ -34,7 +33,7 @@ describe('select peers', () => { it('Selects the peer if there is only one', () => { const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) const pm = router.rpcRouter.peerManager @@ -46,7 +45,7 @@ describe('select peers', () => { it('Selects peer2 if peer1 is saturated`', () => { const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) router.register('take', jest.fn()) const pm = router.rpcRouter.peerManager @@ -63,7 +62,7 @@ describe('select peers', () => { it('Selects peer2 if peer1 failed', () => { const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) const pm = router.rpcRouter.peerManager @@ -84,7 +83,7 @@ describe('select peers', () => { it('Selects the peer1 if both failed', () => { const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) const pm = router.rpcRouter.peerManager @@ -110,7 +109,7 @@ describe('select peers', () => { it('Clears requestFails when peers disconnect', () => { const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) const pm = router.rpcRouter.peerManager @@ -130,14 +129,14 @@ describe('Global Rpc', () => { it('Constructs a global RPC Router correctly', () => { const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) expect(router.requestFails.size).toBe(0) }) it('Registers a global RPC Handler with the direct rpc router', async () => { const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) const handler = jest.fn() router.register('test', handler) @@ -161,7 +160,7 @@ describe('Global Rpc', () => { it('throws when there are no peers available', async () => { const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) router.register('test', () => Promise.resolve(undefined)) @@ -173,7 +172,7 @@ describe('Global Rpc', () => { mocked(nextRpcId).mockReturnValue(44) const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) const pm = router.rpcRouter.peerManager @@ -198,7 +197,7 @@ describe('Global Rpc', () => { it('handles a round trip successfully with one peer', async () => { const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) const pm = router.rpcRouter.peerManager @@ -229,7 +228,7 @@ describe('Global Rpc', () => { it('retries if first attempt returns cannot fulfill request', async () => { const router = new GlobalRpcRouter( - new RpcRouter(new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore()))), + new RpcRouter(new PeerManager(mockLocalPeer(), mockHostsStore())), ) const pm = router.rpcRouter.peerManager diff --git a/ironfish/src/network/messageRouters/gossip.test.ts b/ironfish/src/network/messageRouters/gossip.test.ts index 1b1dec45f1..25b0ef9ac1 100644 --- a/ironfish/src/network/messageRouters/gossip.test.ts +++ b/ironfish/src/network/messageRouters/gossip.test.ts @@ -10,7 +10,6 @@ import { v4 as uuid } from 'uuid' import ws from 'ws' import { mockChain, mockNode, mockStrategy } from '../../testUtilities/mocks' import { PeerNetwork, RoutingStyle } from '../peerNetwork' -import { AddressManager } from '../peers/addressManager' import { PeerManager } from '../peers/peerManager' import { getConnectedPeer, mockHostsStore, mockLocalPeer } from '../testUtilities' import { GossipRouter } from './gossip' @@ -20,7 +19,7 @@ jest.useFakeTimers() describe('Gossip Router', () => { it('Broadcasts a message on gossip', () => { mocked(uuid).mockReturnValue('test_broadcast') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const broadcastMock = jest.spyOn(pm, 'broadcast').mockImplementation(() => {}) const router = new GossipRouter(pm) router.register('test', jest.fn()) @@ -32,7 +31,7 @@ describe('Gossip Router', () => { }) it('Handles an incoming gossip message', async () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const broadcastMock = jest.spyOn(pm, 'broadcast').mockImplementation(() => {}) const { peer: peer1 } = getConnectedPeer(pm) const { peer: peer2 } = getConnectedPeer(pm) @@ -65,7 +64,7 @@ describe('Gossip Router', () => { }) it('Does not process a seen message twice', async () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const broadcastMock = jest.spyOn(pm, 'broadcast').mockImplementation(() => {}) const { peer: peer1 } = getConnectedPeer(pm) const { peer: peer2 } = getConnectedPeer(pm) @@ -95,7 +94,7 @@ describe('Gossip Router', () => { }) it('Does not send messages to peers of peer that sent it', async () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const broadcastMock = jest.spyOn(pm, 'broadcast').mockImplementation(() => {}) const { peer: peer1 } = getConnectedPeer(pm) const { peer: peer2 } = getConnectedPeer(pm) @@ -125,7 +124,7 @@ describe('Gossip Router', () => { node: mockNode(), chain: mockChain(), strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) const gossipMock = jest.fn(async () => {}) @@ -136,12 +135,12 @@ describe('Gossip Router', () => { () => Promise.resolve({ name: '' }), () => true, ) - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer } = getConnectedPeer(pm) const message = { type: 'hello', nonce: 'test_handler1', payload: { test: 'payload' } } await network['handleMessage'](peer, { peerIdentity: peer.getIdentityOrThrow(), message }) expect(gossipMock).toBeCalled() - network.stop() + await network.stop() }) it('does not handle a poorly formatted gossip message as gossip', async () => { @@ -151,7 +150,7 @@ describe('Gossip Router', () => { node: mockNode(), chain: mockChain(), strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) const gossipMock = jest.fn(async () => {}) @@ -166,7 +165,7 @@ describe('Gossip Router', () => { const logFn = jest.fn() network['logger'].mock(() => logFn) - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer } = getConnectedPeer(pm) // This is the wrong type so it tests that it fails @@ -180,6 +179,6 @@ describe('Gossip Router', () => { expect(gossipMock).not.toBeCalled() expect(logFn).toBeCalled() - network.stop() + await network.stop() }) }) diff --git a/ironfish/src/network/messageRouters/rpc.test.ts b/ironfish/src/network/messageRouters/rpc.test.ts index 5ab663bed4..856f28f69a 100644 --- a/ironfish/src/network/messageRouters/rpc.test.ts +++ b/ironfish/src/network/messageRouters/rpc.test.ts @@ -4,7 +4,6 @@ jest.mock('./rpcId') import { mocked } from 'ts-jest/utils' -import { AddressManager } from '../peers/addressManager' import { NetworkError } from '../peers/connections/errors' import { PeerManager } from '../peers/peerManager' import { getConnectedPeer, mockHostsStore, mockLocalPeer } from '../testUtilities' @@ -23,7 +22,7 @@ describe('RPC Router', () => { }) it('Registers an RPC Handler', () => { - const peers = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const peers = new PeerManager(mockLocalPeer(), mockHostsStore()) const router = new RpcRouter(peers) const handler = jest.fn() router.register('test', handler) @@ -32,7 +31,7 @@ describe('RPC Router', () => { }) it('should time out RPC requests', async () => { - const peers = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const peers = new PeerManager(mockLocalPeer(), mockHostsStore()) const sendToMock = jest.spyOn(peers, 'sendTo') const { peer } = getConnectedPeer(peers) @@ -58,7 +57,7 @@ describe('RPC Router', () => { }) it('should reject requests when connection disconnects', async () => { - const peers = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const peers = new PeerManager(mockLocalPeer(), mockHostsStore()) const sendToMock = jest.spyOn(peers, 'sendTo') const { peer, connection } = getConnectedPeer(peers) @@ -89,7 +88,7 @@ describe('RPC Router', () => { it('should increment and decrement pendingRPC', async () => { mocked(nextRpcId).mockReturnValue(91) - const peers = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const peers = new PeerManager(mockLocalPeer(), mockHostsStore()) jest.spyOn(peers, 'sendTo') const { peer } = getConnectedPeer(peers, 'peer') @@ -115,7 +114,7 @@ describe('RPC Router', () => { mocked(nextRpcId).mockReturnValue(91) mocked(rpcTimeoutMillis).mockReturnValue(1000) - const peers = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const peers = new PeerManager(mockLocalPeer(), mockHostsStore()) const router = new RpcRouter(peers) router.register('test', jest.fn()) @@ -146,7 +145,7 @@ describe('RPC Router', () => { it('Catches a cannotSatisfy error and returns the appropriate type', async () => { mocked(nextRpcId).mockReturnValue(18) - const peers = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const peers = new PeerManager(mockLocalPeer(), mockHostsStore()) const sendToMock = jest.fn() peers.sendTo = sendToMock diff --git a/ironfish/src/network/peerNetwork.test.ts b/ironfish/src/network/peerNetwork.test.ts index c35922822c..9972753b18 100644 --- a/ironfish/src/network/peerNetwork.test.ts +++ b/ironfish/src/network/peerNetwork.test.ts @@ -13,14 +13,13 @@ import { createNodeTest } from '../testUtilities' import { mockChain, mockNode, mockStrategy } from '../testUtilities/mocks' import { DisconnectingMessage, NodeMessageType } from './messages' import { PeerNetwork, RoutingStyle } from './peerNetwork' -import { AddressManager } from './peers/addressManager' import { getConnectedPeer, mockHostsStore, mockPrivateIdentity } from './testUtilities' jest.useFakeTimers() describe('PeerNetwork', () => { describe('stop', () => { - it('stops the peer manager', () => { + it('stops the peer manager', async () => { const peerNetwork = new PeerNetwork({ identity: mockPrivateIdentity('local'), agent: 'sdk/1/cli', @@ -28,17 +27,17 @@ describe('PeerNetwork', () => { node: mockNode(), chain: mockChain(), strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) const stopSpy = jest.spyOn(peerNetwork.peerManager, 'stop') - peerNetwork.stop() + await peerNetwork.stop() expect(stopSpy).toBeCalled() }) }) describe('registerHandler', () => { - it('stores the type in the routingStyles', () => { + it('stores the type in the routingStyles', async () => { const peerNetwork = new PeerNetwork({ identity: mockPrivateIdentity('local'), agent: 'sdk/1/cli', @@ -46,7 +45,7 @@ describe('PeerNetwork', () => { node: mockNode(), chain: mockChain(), strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) const type = 'hello' @@ -57,7 +56,7 @@ describe('PeerNetwork', () => { () => {}, ) expect(peerNetwork['routingStyles'].get(type)).toBe(RoutingStyle.gossip) - peerNetwork.stop() + await peerNetwork.stop() }) }) @@ -70,7 +69,7 @@ describe('PeerNetwork', () => { node: mockNode(), chain: mockChain(), strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) const handlerMock = jest.fn(() => {}) @@ -88,12 +87,12 @@ describe('PeerNetwork', () => { message, }) expect(handlerMock).not.toBeCalled() - peerNetwork.stop() + await peerNetwork.stop() }) }) describe('when peers connect', () => { - it('changes isReady', () => { + it('changes isReady', async () => { const peerNetwork = new PeerNetwork({ identity: mockPrivateIdentity('local'), agent: 'sdk/1/cli', @@ -102,7 +101,7 @@ describe('PeerNetwork', () => { chain: mockChain(), strategy: mockStrategy(), minPeers: 1, - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) expect(peerNetwork.isReady).toBe(false) @@ -119,7 +118,7 @@ describe('PeerNetwork', () => { peer.close() expect(peerNetwork.isReady).toBe(false) - peerNetwork.stop() + await peerNetwork.stop() expect(peerNetwork.isReady).toBe(false) expect(readyChanged).toBeCalledTimes(2) @@ -129,7 +128,7 @@ describe('PeerNetwork', () => { }) describe('when at max peers', () => { - it('rejects websocket connections', () => { + it('rejects websocket connections', async () => { const wsActual = jest.requireActual('ws') const peerNetwork = new PeerNetwork({ @@ -143,7 +142,7 @@ describe('PeerNetwork', () => { port: 0, minPeers: 1, maxPeers: 0, - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) const rejectSpy = jest @@ -162,7 +161,7 @@ describe('PeerNetwork', () => { const sendSpy = jest.spyOn(conn, 'send').mockReturnValue(undefined) const closeSpy = jest.spyOn(conn, 'close').mockReturnValue(undefined) server.server.emit('connection', conn, req) - peerNetwork.stop() + await peerNetwork.stop() expect(rejectSpy).toHaveBeenCalled() expect(sendSpy).toHaveBeenCalled() @@ -185,7 +184,7 @@ describe('PeerNetwork', () => { node: mockNode(), chain: mockChain(), strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) const { peer } = getConnectedPeer(peerNetwork.peerManager) @@ -224,7 +223,7 @@ describe('PeerNetwork', () => { }, }, strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) const { accounts, memPool, workerPool } = node @@ -274,7 +273,7 @@ describe('PeerNetwork', () => { node, chain, strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) const { accounts, memPool } = node @@ -323,7 +322,7 @@ describe('PeerNetwork', () => { node, chain, strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), }) // Spy on new transactions @@ -372,7 +371,7 @@ describe('PeerNetwork', () => { node: mockNode(), chain: mockChain(), strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), } const peerNetwork1 = new PeerNetwork({ ...networkArgs, enableSyncing: false }) @@ -425,7 +424,7 @@ describe('PeerNetwork', () => { node: mockNode(), chain: mockChain(), strategy: mockStrategy(), - addressManager: new AddressManager(mockHostsStore()), + hostsStore: mockHostsStore(), } const peerNetwork1 = new PeerNetwork({ ...networkArgs, enableSyncing: false }) diff --git a/ironfish/src/network/peerNetwork.ts b/ironfish/src/network/peerNetwork.ts index d36c840eb0..e1fa558de1 100644 --- a/ironfish/src/network/peerNetwork.ts +++ b/ironfish/src/network/peerNetwork.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import tweetnacl from 'tweetnacl' +import { HostsStore } from '..' import { Assert } from '../assert' import { Blockchain } from '../blockchain' import { MAX_REQUESTED_BLOCKS } from '../consensus' @@ -58,7 +59,6 @@ import { NodeMessageType, PayloadType, } from './messages' -import { AddressManager } from './peers/addressManager' import { LocalPeer } from './peers/localPeer' import { BAN_SCORE, Peer } from './peers/peer' import { PeerConnectionManager } from './peers/peerConnectionManager' @@ -147,7 +147,7 @@ export class PeerNetwork { node: IronfishNode strategy: Strategy chain: Blockchain - addressManager: AddressManager + hostsStore: HostsStore }) { const identity = options.identity || tweetnacl.box.keyPair() const enableSyncing = options.enableSyncing ?? true @@ -178,7 +178,7 @@ export class PeerNetwork { this.peerManager = new PeerManager( this.localPeer, - options.addressManager, + options.hostsStore, this.logger, this.metrics, maxPeers, @@ -396,10 +396,10 @@ export class PeerNetwork { * Call close when shutting down the PeerNetwork to clean up * outstanding connections. */ - stop(): void { + async stop(): Promise { this.started = false this.peerConnectionManager.stop() - this.peerManager.stop() + await this.peerManager.stop() this.webSocketServer?.close() this.updateIsReady() } diff --git a/ironfish/src/network/peers/addressManager.test.ts b/ironfish/src/network/peers/addressManager.test.ts index 93ce101e52..a145729014 100644 --- a/ironfish/src/network/peers/addressManager.test.ts +++ b/ironfish/src/network/peers/addressManager.test.ts @@ -3,26 +3,126 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ jest.mock('ws') -import { mockHostsStore } from '../testUtilities' +import { + getConnectedPeer, + getConnectingPeer, + getDisconnectedPeer, + mockHostsStore, + mockLocalPeer, +} from '../testUtilities' import { AddressManager } from './addressManager' +import { ConnectionDirection, ConnectionType } from './connections' +import { Peer } from './peer' +import { PeerAddress } from './peerAddress' +import { PeerManager } from './peerManager' jest.useFakeTimers() describe('AddressManager', () => { it('constructor loads addresses from HostsStore', () => { const addressManager = new AddressManager(mockHostsStore()) + addressManager.hostsStore = mockHostsStore() expect(addressManager.priorConnectedPeerAddresses).toMatchObject([ { address: '127.0.0.1', port: 9999, }, ]) - expect(addressManager.possiblePeerAddresses).toMatchObject([ - { - address: '1.1.1.1', - port: 1111, - identity: undefined, - }, - ]) + }) + + it('removePeerAddress should remove a peer address', () => { + const addressManager = new AddressManager(mockHostsStore()) + addressManager.hostsStore = mockHostsStore() + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) + const { peer: peer1 } = getConnectedPeer(pm) + const allPeers: Peer[] = [peer1] + const allPeerAddresses: PeerAddress[] = [] + + for (const peer of allPeers) { + allPeerAddresses.push({ + address: peer.address, + port: peer.port, + identity: peer.state.identity, + name: peer.name, + }) + } + addressManager.hostsStore.set('possiblePeers', allPeerAddresses) + addressManager.hostsStore.set('priorPeers', allPeerAddresses) + addressManager.removePeerAddress(peer1) + expect(addressManager.priorConnectedPeerAddresses.length).toEqual(0) + }) + + it('getRandomDisconnectedPeer should return a randomly-sampled disconnected peer', () => { + const addressManager = new AddressManager(mockHostsStore()) + addressManager.hostsStore = mockHostsStore() + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) + const { peer: connectedPeer } = getConnectedPeer(pm) + const { peer: connectingPeer } = getConnectingPeer(pm) + const disconnectedPeer = getDisconnectedPeer(pm) + const nonDisconnectedPeers: Peer[] = [connectedPeer, connectingPeer] + const allPeerAddresses: PeerAddress[] = [] + + for (const peer of [...nonDisconnectedPeers, disconnectedPeer]) { + allPeerAddresses.push({ + address: peer.address, + port: peer.port, + identity: peer.state.identity, + name: peer.name, + }) + } + + const nonDisconnectedIdentities = nonDisconnectedPeers.flatMap((peer) => { + if (peer.state.type !== 'DISCONNECTED' && peer.state.identity !== null) { + return peer.state.identity + } else { + return [] + } + }) + + addressManager.hostsStore.set('priorPeers', allPeerAddresses) + + const sample = addressManager.getRandomDisconnectedPeerAddress(nonDisconnectedIdentities) + expect(sample).not.toBeNull() + if (sample !== null) { + expect(allPeerAddresses).toContainEqual(sample) + expect(sample.address).toEqual(disconnectedPeer.address) + expect(sample.port).toEqual(disconnectedPeer.port) + } + }) + + describe('save', () => { + it('save should persist connected peers', async () => { + const addressManager = new AddressManager(mockHostsStore()) + addressManager.hostsStore = mockHostsStore() + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) + const { peer: connectedPeer } = getConnectedPeer(pm) + const { peer: connectingPeer } = getConnectingPeer(pm) + const disconnectedPeer = getDisconnectedPeer(pm) + const address: PeerAddress = { + address: connectedPeer.address, + port: connectedPeer.port, + identity: connectedPeer.state.identity, + name: connectedPeer.name, + } + + await addressManager.save([connectedPeer, connectingPeer, disconnectedPeer]) + expect(addressManager.priorConnectedPeerAddresses).toContainEqual(address) + }) + + it('should not persist peers that will never retry connecting', async () => { + const addressManager = new AddressManager(mockHostsStore()) + addressManager.hostsStore = mockHostsStore() + expect(addressManager.priorConnectedPeerAddresses.length).toEqual(1) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) + const { peer: connectedPeer } = getConnectedPeer(pm) + const { peer: connectingPeer } = getConnectingPeer(pm) + const disconnectedPeer = getDisconnectedPeer(pm) + connectedPeer + .getConnectionRetry(ConnectionType.WebSocket, ConnectionDirection.Outbound) + ?.neverRetryConnecting() + + await addressManager.save([connectedPeer, connectingPeer, disconnectedPeer]) + expect(addressManager.priorConnectedPeerAddresses.length).toEqual(0) + }) }) }) diff --git a/ironfish/src/network/peers/addressManager.ts b/ironfish/src/network/peers/addressManager.ts index d0424d2ffe..d2766f5889 100644 --- a/ironfish/src/network/peers/addressManager.ts +++ b/ironfish/src/network/peers/addressManager.ts @@ -2,6 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { HostsStore } from '../../fileStores' +import { ArrayUtils } from '../../utils' +import { Peer } from '..' +import { ConnectionDirection, ConnectionType } from './connections' import { PeerAddress } from './peerAddress' /** @@ -9,17 +12,85 @@ import { PeerAddress } from './peerAddress' * and provides functionality for persistence of said data. */ export class AddressManager { - hosts: HostsStore + hostsStore: HostsStore constructor(hostsStore: HostsStore) { - this.hosts = hostsStore + this.hostsStore = hostsStore } get priorConnectedPeerAddresses(): ReadonlyArray> { - return this.hosts.getArray('priorPeers') + return this.hostsStore.getArray('priorPeers') } - get possiblePeerAddresses(): ReadonlyArray> { - return this.hosts.getArray('possiblePeers') + /** + * Returns a peer address for a disconnected peer by using current peers to + * filter out peer addresses. It attempts to find a previously-connected + * peer address that is not part of an active connection. + */ + getRandomDisconnectedPeerAddress(peerIdentities: string[]): PeerAddress | null { + if (this.priorConnectedPeerAddresses.length === 0) { + return null + } + + const currentPeerIdentities = new Set(peerIdentities) + + const disconnectedPriorAddresses = this.filterConnectedIdentities( + this.priorConnectedPeerAddresses, + currentPeerIdentities, + ) + if (disconnectedPriorAddresses.length) { + return ArrayUtils.sampleOrThrow(disconnectedPriorAddresses) + } + + return null + } + + private filterConnectedIdentities( + priorConnectedAddresses: readonly Readonly[], + connectedPeerIdentities: Set, + ): PeerAddress[] { + const disconnectedAddresses = priorConnectedAddresses.filter( + (address) => address.identity !== null && !connectedPeerIdentities.has(address.identity), + ) + + return disconnectedAddresses + } + + /** + * Removes address associated with a peer from address stores + */ + removePeerAddress(peer: Peer): void { + const filteredPriorConnected = this.priorConnectedPeerAddresses.filter( + (prior) => prior.identity !== peer.state.identity, + ) + + this.hostsStore.set('priorPeers', filteredPriorConnected) + } + + /** + * Persist all currently connected peers and unused peer addresses to disk + */ + async save(peers: Peer[]): Promise { + // TODO: Ideally, we would like persist peers with whom we've + // successfully established an outbound Websocket connection at + // least once. + const inUsePeerAddresses: PeerAddress[] = peers.flatMap((peer) => { + if ( + peer.state.type === 'CONNECTED' && + !peer.getConnectionRetry(ConnectionType.WebSocket, ConnectionDirection.Outbound) + .willNeverRetryConnecting + ) { + return { + address: peer.address, + port: peer.port, + identity: peer.state.identity ?? null, + name: peer.name ?? null, + } + } else { + return [] + } + }) + this.hostsStore.set('priorPeers', inUsePeerAddresses) + await this.hostsStore.save() } } diff --git a/ironfish/src/network/peers/peerAddress.ts b/ironfish/src/network/peers/peerAddress.ts index 4c6291be69..8e8f4776fa 100644 --- a/ironfish/src/network/peers/peerAddress.ts +++ b/ironfish/src/network/peers/peerAddress.ts @@ -6,6 +6,6 @@ import { Identity } from '../identity' export type PeerAddress = { address: string | null port: number | null - identity?: Identity | null - name?: string | null + identity: Identity | null + name: string | null } diff --git a/ironfish/src/network/peers/peerConnectionManager.test.ts b/ironfish/src/network/peers/peerConnectionManager.test.ts index 3767c1432b..c6b6009c1c 100644 --- a/ironfish/src/network/peers/peerConnectionManager.test.ts +++ b/ironfish/src/network/peers/peerConnectionManager.test.ts @@ -13,7 +13,6 @@ import { webRtcCanInitiateIdentity, webRtcLocalIdentity, } from '../testUtilities' -import { AddressManager } from './addressManager' import { ConnectionDirection, ConnectionType, @@ -27,7 +26,7 @@ jest.useFakeTimers() describe('connectToDisconnectedPeers', () => { it('Should not connect to disconnected peers without an address or peers', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const peer = pm.getOrCreatePeer(null) const pcm = new PeerConnectionManager(pm, createRootLogger(), { maxPeers: 50 }) pm['logger'].mockTypes(() => jest.fn()) @@ -39,7 +38,7 @@ describe('connectToDisconnectedPeers', () => { }) it('Should connect to disconnected unidentified peers with an address', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const peer = pm.getOrCreatePeer(null) peer.setWebSocketAddress('testuri.com', 9033) const pcm = new PeerConnectionManager(pm, createRootLogger(), { maxPeers: 50 }) @@ -54,7 +53,7 @@ describe('connectToDisconnectedPeers', () => { }) it('Should connect to disconnected identified peers with an address over WS', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const identity = mockIdentity('peer') const peer = pm.getOrCreatePeer(identity) @@ -76,7 +75,7 @@ describe('connectToDisconnectedPeers', () => { }) it('Should connect to webrtc and websockets', () => { - const peers = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const peers = new PeerManager(mockLocalPeer(), mockHostsStore()) const identity = mockIdentity('peer') const peer = peers.getOrCreatePeer(identity) @@ -106,7 +105,7 @@ describe('connectToDisconnectedPeers', () => { const peerIdentity = webRtcCanInitiateIdentity() const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const { peer: brokeringPeer } = getConnectedPeer(pm, 'brokering') const peer = pm.getOrCreatePeer(peerIdentity) @@ -129,7 +128,7 @@ describe('maintainOneConnectionPerPeer', () => { it('Should not close WS connection if the WebRTC connection is not in CONNECTED', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const peer = pm.connectToWebSocketAddress('testuri') const identity = webRtcCanInitiateIdentity() @@ -178,7 +177,7 @@ describe('maintainOneConnectionPerPeer', () => { it('Should close WebSocket connection if a peer has WS and WebRTC connections', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const peer = pm.connectToWebSocketAddress('testuri') const identity = webRtcCanInitiateIdentity() @@ -229,7 +228,7 @@ describe('attemptToEstablishWebRtcConnectionsToWSPeers', () => { it('Should attempt to establish a WebRTC connection if we have a WebSocket connection', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const peer = pm.connectToWebSocketAddress('testuri') const identity = webRtcCanInitiateIdentity() diff --git a/ironfish/src/network/peers/peerConnectionManager.ts b/ironfish/src/network/peers/peerConnectionManager.ts index 6bb7ba6670..48f962ee12 100644 --- a/ironfish/src/network/peers/peerConnectionManager.ts +++ b/ironfish/src/network/peers/peerConnectionManager.ts @@ -86,6 +86,13 @@ export class PeerConnectionManager { } } + if (connectAttempts < CONNECT_ATTEMPTS_MAX && this.peerManager.canCreateNewConnections()) { + const peer = this.peerManager.createRandomDisconnectedPeer() + if (peer && this.connectToEligiblePeers(peer)) { + connectAttempts++ + } + } + this.eventLoopTimer = setTimeout(() => this.eventLoop(), EVENT_LOOP_MS) } diff --git a/ironfish/src/network/peers/peerManager.test.ts b/ironfish/src/network/peers/peerManager.test.ts index e4a362bf70..79c84a1719 100644 --- a/ironfish/src/network/peers/peerManager.test.ts +++ b/ironfish/src/network/peers/peerManager.test.ts @@ -55,7 +55,6 @@ import { webRtcLocalIdentity, } from '../testUtilities' import { VERSION_PROTOCOL, VERSION_PROTOCOL_MIN } from '../version' -import { AddressManager } from './addressManager' import { ConnectionDirection, ConnectionType, @@ -69,7 +68,7 @@ jest.useFakeTimers() describe('PeerManager', () => { describe('Dispose peers', () => { it('Should not dispose of peers that have a CONNECTED peer', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const peer1Identity = mockIdentity('peer1') const peer2Identity = mockIdentity('peer2') const { peer: peer1 } = getConnectedPeer(pm, peer1Identity) @@ -88,7 +87,7 @@ describe('PeerManager', () => { }) it('Should dispose of two DISCONNECTED peers that have each other in knownPeers', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const peer1Identity = mockIdentity('peer1') const peer2Identity = mockIdentity('peer2') const { peer: peer1 } = getConnectedPeer(pm, peer1Identity) @@ -118,113 +117,9 @@ describe('PeerManager', () => { }) }) - describe('Distribute peers', () => { - it('Should send peer list requests to newer protocol version', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) - - const { peer: peer1 } = getConnectedPeer(pm, 'peer1') - - peer1.version = 9 - - expect(pm.identifiedPeers.size).toBe(1) - - const mockSend = jest.spyOn(peer1, 'send') - - pm['distributePeerList']() - expect(mockSend).toBeCalledTimes(1) - expect(mockSend).toBeCalledWith({ - type: InternalMessageType.peerListRequest, - }) - }) - - it('Should not send peer list requests to older protocol version', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) - - const { peer: peer1 } = getConnectedPeer(pm, 'peer1') - - peer1.version = 8 - - expect(pm.identifiedPeers.size).toBe(1) - - const mockSend = jest.spyOn(peer1, 'send') - - pm['distributePeerList']() - expect(mockSend).toHaveBeenCalledTimes(1) - expect(mockSend).not.toHaveBeenCalledWith({ - type: InternalMessageType.peerListRequest, - }) - }) - - it('Should not send peer list requests to null protocol version', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) - - const { peer: peer1 } = getConnectedPeer(pm, 'peer1') - - expect(pm.identifiedPeers.size).toBe(1) - - const mockSend = jest.spyOn(peer1, 'send') - - pm['distributePeerList']() - expect(mockSend).toHaveBeenCalledTimes(1) - expect(mockSend).not.toHaveBeenCalledWith({ - type: InternalMessageType.peerListRequest, - }) - }) - - it('Should broadcast peer list to older protocol version', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) - - const { peer: peer1 } = getConnectedPeer(pm, 'peer1') - const { peer: peer2 } = getConnectedPeer(pm, 'peer2') - - peer1.version = 8 - - expect(pm.identifiedPeers.size).toBe(2) - - const mockSend = jest.spyOn(peer1, 'send') - - pm['distributePeerList']() - expect(mockSend).toBeCalledTimes(1) - expect(mockSend).toBeCalledWith({ - type: InternalMessageType.peerList, - payload: { - connectedPeers: [ - { address: 'testuri.com', port: 9033, identity: peer1.getIdentityOrThrow() }, - { address: 'testuri.com', port: 9033, identity: peer2.getIdentityOrThrow() }, - ], - }, - }) - }) - - it('Should not broadcast peer list to newer protocol version', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) - - const { peer: peer1 } = getConnectedPeer(pm, 'peer1') - const { peer: peer2 } = getConnectedPeer(pm, 'peer2') - - peer1.version = 9 - - expect(pm.identifiedPeers.size).toBe(2) - - const mockSend = jest.spyOn(peer1, 'send') - - pm['distributePeerList']() - expect(mockSend).toBeCalledTimes(1) - expect(mockSend).not.toBeCalledWith({ - type: InternalMessageType.peerList, - payload: { - connectedPeers: [ - { address: 'testuri.com', port: 9033, identity: peer1.getIdentityOrThrow() }, - { address: 'testuri.com', port: 9033, identity: peer2.getIdentityOrThrow() }, - ], - }, - }) - }) - }) - it('should handle duplicate connections from the same peer', () => { const localPeer = mockLocalPeer({ identity: webRtcLocalIdentity() }) - const peers = new PeerManager(localPeer, new AddressManager(mockHostsStore())) + const peers = new PeerManager(localPeer, mockHostsStore()) const { peer: peerOut, connection: connectionOut } = getWaitingForIdentityPeer( peers, @@ -341,10 +236,7 @@ describe('PeerManager', () => { it('Sends identity when a connection is successfully made', () => { const localIdentity = mockPrivateIdentity('local') - const pm = new PeerManager( - mockLocalPeer({ identity: localIdentity }), - new AddressManager(mockHostsStore()), - ) + const pm = new PeerManager(mockLocalPeer({ identity: localIdentity }), mockHostsStore()) const { peer, connection } = getConnectingPeer(pm) @@ -376,7 +268,7 @@ describe('PeerManager', () => { it('should disconnect connection on CONNECTED', () => { const localPeer = mockLocalPeer() - const peers = new PeerManager(localPeer, new AddressManager(mockHostsStore())) + const peers = new PeerManager(localPeer, mockHostsStore()) const { peer: peer1, connection: connection1 } = getConnectingPeer(peers) const { peer: peer2, connection: connection2 } = getWaitingForIdentityPeer(peers) @@ -409,7 +301,7 @@ describe('PeerManager', () => { describe('connect', () => { it('Creates a peer and adds it to unidentifiedConnections', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) expect(pm.peers.length).toBe(0) const peer = pm.connectToWebSocketAddress('testUri') @@ -436,7 +328,7 @@ describe('PeerManager', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const { connection, brokeringPeer } = getSignalingWebRtcPeer( pm, @@ -469,7 +361,7 @@ describe('PeerManager', () => { it('Attempts to establish a WebSocket connection to a peer with a webSocketAddress', () => { const peer1Identity = mockIdentity('peer1') const peer2Identity = mockIdentity('peer2') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) // Create the peers const { peer: peer1 } = getConnectedPeer(pm, peer1Identity) @@ -498,7 +390,7 @@ describe('PeerManager', () => { it('Attempts to establish a WebRTC connection through brokering peer', () => { const peers = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) // Create the peers @@ -521,7 +413,7 @@ describe('PeerManager', () => { it('Can establish a WebRTC connection to a peer using an existing WebSocket connection to the same peer', async () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const { peer, connection } = getConnectedPeer(pm, webRtcCanInitiateIdentity()) @@ -549,7 +441,9 @@ describe('PeerManager', () => { // Emitting new signal data should trigger a send on the WS connection expect(pm.identifiedPeers.size).toBe(1) expect(pm.peers).toHaveLength(1) - const sendSpy = jest.spyOn(connection, 'send') + + const sendSpy = mocked(connection.send) + await peer.state.connections.webRtc.onSignal.emitAsync({ type: 'candidate', candidate: { @@ -558,13 +452,22 @@ describe('PeerManager', () => { sdpMid: '0', }, }) - expect(sendSpy).toBeCalledTimes(1) + + expect(sendSpy).toBeCalledWith({ + type: 'signal', + payload: { + sourceIdentity: 'bGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGw=', + destinationIdentity: 'a2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2s=', + nonce: 'boxMessageNonce', + signal: 'boxMessageMessage', + }, + }) }) it('Attempts to request WebRTC signaling through brokering peer', () => { const peers = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) // Create the peer to broker the connection through @@ -601,7 +504,7 @@ describe('PeerManager', () => { }) it('Does not create a connection if Peer has disconnectUntil set', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer } = getConnectedPeer(pm, 'peer') peer.close() @@ -620,7 +523,7 @@ describe('PeerManager', () => { }) it('Sets disconnectUntil to null if current time is after disconnectUntil', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer } = getConnectedPeer(pm, 'peer') peer.close() @@ -638,14 +541,7 @@ describe('PeerManager', () => { }) it('Does not create a connection to a disconnected Peer above targetPeers', () => { - const pm = new PeerManager( - mockLocalPeer(), - new AddressManager(mockHostsStore()), - undefined, - undefined, - 50, - 1, - ) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore(), undefined, undefined, 50, 1) // Add one connected peer getConnectedPeer(pm, 'peer1') @@ -671,7 +567,7 @@ describe('PeerManager', () => { describe('create peers', () => { it('Returns the same peer when calling createPeer twice with the same identity', () => { const peerIdentity = mockIdentity('peer') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const peer1 = pm.getOrCreatePeer(peerIdentity) const peer1Again = pm.getOrCreatePeer(peerIdentity) @@ -684,7 +580,7 @@ describe('PeerManager', () => { it('Merges peers when an unidentified peer connects with the same identity as an identified webrtc peer', () => { const brokerIdentity = mockIdentity('brokering') const peerIdentity = webRtcCanInitiateIdentity() - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer } = getSignalingWebRtcPeer(pm, brokerIdentity, peerIdentity) @@ -695,10 +591,18 @@ describe('PeerManager', () => { throw new Error('Peer should have a WebRTC connection') } const webRtcConnection = peer.state.connections.webRtc + + // TODO: webRtcConnection.datachannel never actually opens during a test + // so when peer.send() gets called as part of the onConnect event, it + // closes the webRTC connection. For now, we'll mock the close function, + // but in the future, we should mock the datachannel class to make tests + // more robust -- deekerno + const closeSpy = jest.spyOn(webRtcConnection, 'close').mockImplementationOnce(() => {}) webRtcConnection.setState({ type: 'CONNECTED', identity: peerIdentity, }) + expect(closeSpy).toBeCalledTimes(1) expect(pm.peers.length).toBe(2) expect(pm.identifiedPeers.size).toBe(2) @@ -742,7 +646,7 @@ describe('PeerManager', () => { const peerIdentity = webRtcCanInitiateIdentity() const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const { peer, connection } = getConnectedPeer(pm, peerIdentity) @@ -800,7 +704,7 @@ describe('PeerManager', () => { }) it('Emits onConnectedPeersChanged when a peer enters CONNECTED or DISCONNECTED', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const onConnectedPeersChangedMock = jest.fn() pm.onConnectedPeersChanged.on(onConnectedPeersChangedMock) @@ -821,7 +725,7 @@ describe('PeerManager', () => { describe('Message: Identity', () => { it('Adds the peer to identifiedPeers after receiving a valid identity message', () => { const other = mockIdentity('other') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) expect(pm.identifiedPeers.size).toBe(0) expect(pm.peers.length).toBe(0) @@ -858,7 +762,7 @@ describe('PeerManager', () => { it('Closes the connection when versions do not match', () => { const other = mockPrivateIdentity('other') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer, connection } = getWaitingForIdentityPeer(pm) @@ -894,7 +798,7 @@ describe('PeerManager', () => { }) it('Closes the connection when an identity message with an invalid public key is sent', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer, connection } = getWaitingForIdentityPeer(pm) @@ -931,10 +835,7 @@ describe('PeerManager', () => { it('Closes the connection if an unidentified peer returns the local identity', () => { const localIdentity = mockPrivateIdentity('local') - const pm = new PeerManager( - mockLocalPeer({ identity: localIdentity }), - new AddressManager(mockHostsStore()), - ) + const pm = new PeerManager(mockLocalPeer({ identity: localIdentity }), mockHostsStore()) expect(pm.identifiedPeers.size).toBe(0) expect(pm.peers.length).toBe(0) @@ -966,10 +867,7 @@ describe('PeerManager', () => { it('Closes the connection if an identified peer returns the local identity', () => { const localIdentity = mockPrivateIdentity('local') - const pm = new PeerManager( - mockLocalPeer({ identity: localIdentity }), - new AddressManager(mockHostsStore()), - ) + const pm = new PeerManager(mockLocalPeer({ identity: localIdentity }), mockHostsStore()) const { peer: peer1 } = getConnectedPeer(pm, 'peer1') @@ -1028,7 +926,7 @@ describe('PeerManager', () => { it('Moves the connection to another peer if it returns a different identity', () => { const peer1Identity = mockIdentity('peer1') const peer2Identity = mockIdentity('peer2') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer: peer1 } = getConnectedPeer(pm, peer1Identity) @@ -1086,10 +984,7 @@ describe('PeerManager', () => { it('Closes the connection if the peer has disconnectUntil set', () => { const localIdentity = mockPrivateIdentity('local') const peerIdentity = mockIdentity('peer') - const pm = new PeerManager( - mockLocalPeer({ identity: localIdentity }), - new AddressManager(mockHostsStore()), - ) + const pm = new PeerManager(mockLocalPeer({ identity: localIdentity }), mockHostsStore()) const { peer } = getConnectedPeer(pm, peerIdentity) peer.close() @@ -1133,7 +1028,7 @@ describe('PeerManager', () => { describe('Message: SignalRequest', () => { it('Forwards SignalRequest message intended for another peer', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer: destinationPeer } = getConnectedPeer(pm, webRtcCannotInitiateIdentity()) const { connection: sourcePeerConnection, peer: sourcePeer } = getConnectedPeer( @@ -1162,7 +1057,7 @@ describe('PeerManager', () => { }) it('Drops SignalRequest message originating from an different peer than sourceIdentity', () => { - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer: peer1 } = getConnectedPeer(pm) const { peer: peer2 } = getConnectedPeer(pm) @@ -1187,7 +1082,7 @@ describe('PeerManager', () => { it('reject SignalRequest when source peer should initiate', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const initWebRtcConnectionMock = jest.fn() pm['initWebRtcConnection'] = initWebRtcConnectionMock @@ -1214,7 +1109,7 @@ describe('PeerManager', () => { it('Initiates webRTC connection when request intended for local peer', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const initWebRtcConnectionMock = jest.fn() pm['initWebRtcConnection'] = initWebRtcConnectionMock @@ -1243,7 +1138,7 @@ describe('PeerManager', () => { it('Sends a disconnect message if we are at max peers', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), undefined, undefined, 1, @@ -1279,7 +1174,7 @@ describe('PeerManager', () => { it('Does not send a disconnect message if we are at max peers but we have an existing connection to the peer', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), undefined, undefined, 1, @@ -1308,7 +1203,7 @@ describe('PeerManager', () => { it('Forwards signaling messages intended for another peer', () => { const peer1Identity = mockIdentity('peer1') const peer2Identity = mockIdentity('peer2') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { connection: peer1Connection, peer: peer1 } = getConnectedPeer(pm, peer1Identity) const { peer: peer2 } = getConnectedPeer(pm, peer2Identity) @@ -1332,7 +1227,7 @@ describe('PeerManager', () => { const peer1Identity = mockIdentity('peer1') const peer2Identity = mockIdentity('peer2') const peer3Identity = mockIdentity('peer3') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer: peer1 } = getConnectedPeer(pm, peer1Identity) const { peer: peer2 } = getConnectedPeer(pm, peer2Identity) @@ -1358,7 +1253,7 @@ describe('PeerManager', () => { it('Sends a disconnect message if we are at max peers', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), undefined, undefined, 1, @@ -1396,7 +1291,7 @@ describe('PeerManager', () => { it('Does not send a disconnect message if we are at max peers but we have an existing connection to the peer', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), undefined, undefined, 1, @@ -1427,7 +1322,7 @@ describe('PeerManager', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const { connection, brokeringConnection, brokeringPeer } = getSignalingWebRtcPeer( @@ -1469,7 +1364,7 @@ describe('PeerManager', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const { connection, brokeringConnection, brokeringPeer } = getSignalingWebRtcPeer( pm, @@ -1504,7 +1399,7 @@ describe('PeerManager', () => { const pm = new PeerManager( mockLocalPeer({ identity: webRtcLocalIdentity() }), - new AddressManager(mockHostsStore()), + mockHostsStore(), ) const { connection, brokeringConnection, brokeringPeer } = getSignalingWebRtcPeer( pm, @@ -1536,7 +1431,7 @@ describe('PeerManager', () => { it('Sends a peer list message in response', () => { const peerIdentity = mockIdentity('peer') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { connection, peer } = getConnectedPeer(pm, peerIdentity) expect(pm.peers.length).toBe(1) @@ -1571,10 +1466,7 @@ describe('PeerManager', () => { const localIdentity = mockPrivateIdentity('local') const peerIdentity = mockIdentity('peer') - const pm = new PeerManager( - mockLocalPeer({ identity: localIdentity }), - new AddressManager(mockHostsStore()), - ) + const pm = new PeerManager(mockLocalPeer({ identity: localIdentity }), mockHostsStore()) const { connection, peer } = getConnectedPeer(pm, peerIdentity) @@ -1600,7 +1492,7 @@ describe('PeerManager', () => { const peerIdentity = mockIdentity('peer') const newPeerIdentity = mockIdentity('new') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { connection, peer } = getConnectedPeer(pm, peerIdentity) @@ -1631,7 +1523,7 @@ describe('PeerManager', () => { const peerIdentity = mockIdentity('peer') const newPeerIdentity = mockIdentity('new') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { connection, peer } = getConnectedPeer(pm, peerIdentity) @@ -1681,7 +1573,7 @@ describe('PeerManager', () => { const peerIdentity = mockIdentity('peer') const newPeerIdentity = mockIdentity('new') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { connection, peer } = getConnectedPeer(pm, peerIdentity) @@ -1738,7 +1630,7 @@ describe('PeerManager', () => { const peer1Identity = mockIdentity('peer1') const peer2Identity = mockIdentity('peer2') const peer3Identity = mockIdentity('peer3') - const pm = new PeerManager(mockLocalPeer(), new AddressManager(mockHostsStore())) + const pm = new PeerManager(mockLocalPeer(), mockHostsStore()) const { peer: peer1 } = getConnectedPeer(pm, peer1Identity) const { peer: peer2 } = getConnectedPeer(pm, peer2Identity) @@ -1763,7 +1655,7 @@ describe('PeerManager', () => { it('Should set peerRequestedDisconnectUntil on unidentified Peer', () => { const localPeer = mockLocalPeer() - const pm = new PeerManager(localPeer, new AddressManager(mockHostsStore())) + const pm = new PeerManager(localPeer, mockHostsStore()) const peerIdentity = mockIdentity('peer') const { peer, connection } = getConnectingPeer(pm) expect(peer.peerRequestedDisconnectUntil).toBeNull() @@ -1790,7 +1682,7 @@ describe('PeerManager', () => { it('Should set peerRequestedDisconnectUntil on CONNECTED Peer when sender is not sourceIdentity', () => { const localPeer = mockLocalPeer({ identity: webRtcLocalIdentity() }) - const pm = new PeerManager(localPeer, new AddressManager(mockHostsStore())) + const pm = new PeerManager(localPeer, mockHostsStore()) const { peer, brokeringConnection, brokeringPeer } = getSignalingWebRtcPeer( pm, @@ -1821,7 +1713,7 @@ describe('PeerManager', () => { it('Should set peerRequestedDisconnectUntil on CONNECTED Peer when sender is sourceIdentity', () => { const localPeer = mockLocalPeer() - const pm = new PeerManager(localPeer, new AddressManager(mockHostsStore())) + const pm = new PeerManager(localPeer, mockHostsStore()) const peerIdentity = mockIdentity('peer') const { peer, connection } = getConnectedPeer(pm, peerIdentity) expect(peer.peerRequestedDisconnectUntil).toBeNull() diff --git a/ironfish/src/network/peers/peerManager.ts b/ironfish/src/network/peers/peerManager.ts index fb6c1490db..7ae8842c60 100644 --- a/ironfish/src/network/peers/peerManager.ts +++ b/ironfish/src/network/peers/peerManager.ts @@ -4,6 +4,7 @@ import type { SignalData } from './connections/webRtcConnection' import WSWebSocket from 'ws' +import { HostsStore } from '../..' import { Event } from '../../event' import { createRootLogger, Logger } from '../../logger' import { MetricsMonitor } from '../../metrics' @@ -52,12 +53,6 @@ import { Peer } from './peer' */ const MAX_WEBRTC_BROKERING_ATTEMPTS = 5 -/** - * The minimum version at which the peer manager will send peer list requests - * to a connected peer - */ -const MIN_VERSION_FOR_PEER_LIST_REQUESTS = 9 - /** * PeerManager keeps the state of Peers and their underlying connections up to date, * determines how to establish a connection to a given Peer, and provides an event @@ -87,10 +82,10 @@ export class PeerManager { addressManager: AddressManager /** - * setInterval handle for distributePeerList, which sends out peer lists and + * setInterval handle for requestPeerList, which sends out peer lists and * requests for peer lists */ - private distributePeerListHandle: SetIntervalToken | undefined + private requestPeerListHandle: SetIntervalToken | undefined /** * setInterval handle for peer disposal, which removes peers from the list that we @@ -98,6 +93,12 @@ export class PeerManager { */ private disposePeersHandle: SetIntervalToken | undefined + /** + * setInterval handle for peer address persistence, which saves connected + * peers to disk + */ + private savePeerAddressesHandle: SetIntervalToken | undefined + /** * Event fired when a new connection is successfully opened. Sends some identifying * information about the peer. @@ -147,7 +148,7 @@ export class PeerManager { constructor( localPeer: LocalPeer, - addressManager: AddressManager, + hostsStore: HostsStore, logger: Logger = createRootLogger(), metrics?: MetricsMonitor, maxPeers = 10000, @@ -155,12 +156,12 @@ export class PeerManager { logPeerMessages = false, ) { this.logger = logger.withTag('peermanager') - this.addressManager = addressManager this.metrics = metrics || new MetricsMonitor(this.logger) this.localPeer = localPeer this.maxPeers = maxPeers this.targetPeers = targetPeers this.logPeerMessages = logPeerMessages + this.addressManager = new AddressManager(hostsStore) } /** @@ -452,8 +453,7 @@ export class PeerManager { } const canEstablishNewConnection = - peer.state.type !== 'DISCONNECTED' || - this.getPeersWithConnection().length < this.targetPeers + peer.state.type !== 'DISCONNECTED' || this.canCreateNewConnections() const disconnectOk = peer.peerRequestedDisconnectUntil === null || now >= peer.peerRequestedDisconnectUntil @@ -480,8 +480,7 @@ export class PeerManager { } const canEstablishNewConnection = - peer.state.type !== 'DISCONNECTED' || - this.getPeersWithConnection().length < this.targetPeers + peer.state.type !== 'DISCONNECTED' || this.canCreateNewConnections() const disconnectOk = peer.peerRequestedDisconnectUntil === null || now >= peer.peerRequestedDisconnectUntil @@ -562,6 +561,14 @@ export class PeerManager { }) } + /** + * Returns true if the total number of connected peers is less + * than the target amount of peers + */ + canCreateNewConnections(): boolean { + return this.getPeersWithConnection().length < this.targetPeers + } + /** * True if we should reject connections from disconnected Peers. */ @@ -745,6 +752,12 @@ export class PeerManager { } }) + peer.onStateChanged.on(({ prevState }) => { + if (prevState.type !== 'CONNECTED' && peer.state.type === 'CONNECTED') { + peer.send({ type: InternalMessageType.peerListRequest }) + } + }) + peer.onBanned.on(() => this.banPeer(peer)) return peer @@ -783,55 +796,60 @@ export class PeerManager { } start(): void { - this.distributePeerListHandle = setInterval(() => this.distributePeerList(), 5000) + this.requestPeerListHandle = setInterval(() => this.requestPeerList(), 60000) this.disposePeersHandle = setInterval(() => this.disposePeers(), 2000) + this.savePeerAddressesHandle = setInterval( + () => void this.addressManager.save(this.peers), + 60000, + ) } /** * Call when shutting down the PeerManager to clean up * outstanding connections. */ - stop(): void { - this.distributePeerListHandle && clearInterval(this.distributePeerListHandle) + async stop(): Promise { + this.requestPeerListHandle && clearInterval(this.requestPeerListHandle) this.disposePeersHandle && clearInterval(this.disposePeersHandle) + this.savePeerAddressesHandle && clearInterval(this.savePeerAddressesHandle) + await this.addressManager.save(this.peers) for (const peer of this.peers) { this.disconnect(peer, DisconnectingReason.ShuttingDown, 0) } } - private distributePeerList() { - const connectedPeers = [] - - for (const p of this.identifiedPeers.values()) { - if (p.state.type !== 'CONNECTED') { - continue - } - - connectedPeers.push({ - identity: p.state.identity, - name: p.name || undefined, - address: p.address, - port: p.port, - }) - } - - const peerList: PeerList = { - type: InternalMessageType.peerList, - payload: { connectedPeers }, - } - + private requestPeerList() { const peerListRequest: PeerListRequest = { type: InternalMessageType.peerListRequest, } for (const peer of this.getConnectedPeers()) { - if (peer.version !== null && peer.version >= MIN_VERSION_FOR_PEER_LIST_REQUESTS) { - peer.send(peerListRequest) - continue + peer.send(peerListRequest) + } + } + + /** + * Gets a random disconnected peer address and returns a peer created from + * said address + */ + createRandomDisconnectedPeer(): Peer | null { + const connectedPeers = Array.from(this.identifiedPeers.values()).flatMap((peer) => { + if (peer.state.type !== 'DISCONNECTED' && peer.state.identity !== null) { + return peer.state.identity + } else { + return [] } + }) - peer.send(peerList) + const peerAddress = this.addressManager.getRandomDisconnectedPeerAddress(connectedPeers) + if (!peerAddress) { + return null } + + const peer = this.getOrCreatePeer(peerAddress.identity) + peer.setWebSocketAddress(peerAddress.address, peerAddress.port) + peer.name = peerAddress.name || null + return peer } private disposePeers(): void { @@ -852,19 +870,22 @@ export class PeerManager { if ( peer.state.type === 'DISCONNECTED' && - !hasAConnectedPeer && peer.getConnectionRetry(ConnectionType.WebSocket, ConnectionDirection.Outbound) ?.willNeverRetryConnecting ) { - this.logger.debug( - `Disposing of peer with identity ${String(peer.state.identity)} (may be a duplicate)`, - ) + this.addressManager.removePeerAddress(peer) + + if (!hasAConnectedPeer) { + this.logger.debug( + `Disposing of peer with identity ${String(peer.state.identity)} (may be a duplicate)`, + ) - peer.dispose() - if (peer.state.identity && this.identifiedPeers.get(peer.state.identity) === peer) { - this.identifiedPeers.delete(peer.state.identity) + peer.dispose() + if (peer.state.identity && this.identifiedPeers.get(peer.state.identity) === peer) { + this.identifiedPeers.delete(peer.state.identity) + } + this.peers = this.peers.filter((p) => p !== peer) } - this.peers = this.peers.filter((p) => p !== peer) return true } diff --git a/ironfish/src/network/testUtilities/helpers.ts b/ironfish/src/network/testUtilities/helpers.ts index cb1c3c4a81..9b428b4961 100644 --- a/ironfish/src/network/testUtilities/helpers.ts +++ b/ironfish/src/network/testUtilities/helpers.ts @@ -94,6 +94,19 @@ export function getConnectedPeer( return { peer, connection: connection } } +export function getDisconnectedPeer(pm: PeerManager, identity?: string | Identity): Peer { + if (!identity) { + identity = jest.requireActual('uuid').v4() + } + + if (!isIdentity(identity)) { + identity = mockIdentity(identity) + } + + const peer = pm.getOrCreatePeer(identity) + return peer +} + export function getSignalingWebRtcPeer( pm: PeerManager, brokeringPeerIdentity: Identity, diff --git a/ironfish/src/network/testUtilities/mockHostsStore.ts b/ironfish/src/network/testUtilities/mockHostsStore.ts index 6401b85cf7..87df56b9b0 100644 --- a/ironfish/src/network/testUtilities/mockHostsStore.ts +++ b/ironfish/src/network/testUtilities/mockHostsStore.ts @@ -24,12 +24,17 @@ class MockFileSystem extends FileSystem { return this } + // eslint-disable-next-line @typescript-eslint/require-await + async access(): Promise { + throw new Error('File does not exist') + } + // eslint-disable-next-line @typescript-eslint/no-empty-function async writeFile(): Promise {} // eslint-disable-next-line @typescript-eslint/require-await async readFile(): Promise { - return '' + return '{}' } // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -51,14 +56,16 @@ class MockHostsStore extends HostsStore { { address: '127.0.0.1', port: 9999, - identity: undefined, + identity: null, + name: null, }, ]) super.set('possiblePeers', [ { address: '1.1.1.1', port: 1111, - identity: undefined, + identity: null, + name: null, }, ]) } @@ -80,3 +87,7 @@ class MockHostsStore extends HostsStore { export function mockHostsStore(): MockHostsStore { return new MockHostsStore() } + +export function mockFileSystem(): MockFileSystem { + return new MockFileSystem() +} diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index f43046183f..28edd6f2cb 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -12,7 +12,6 @@ import { MemPool } from './memPool' import { MetricsMonitor } from './metrics' import { MiningDirector } from './mining' import { PeerNetwork, PrivateIdentity } from './network' -import { AddressManager } from './network/peers/addressManager' import { IsomorphicWebSocketConstructor } from './network/types' import { Package } from './package' import { Platform } from './platform' @@ -49,7 +48,6 @@ export class IronfishNode { files, config, internal, - hosts, accounts, strategy, metrics, @@ -59,12 +57,12 @@ export class IronfishNode { logger, webSocket, privateIdentity, + hostsStore, }: { pkg: Package files: FileSystem config: Config internal: InternalStore - hosts: HostsStore accounts: Accounts chain: Blockchain strategy: Strategy @@ -75,6 +73,7 @@ export class IronfishNode { logger: Logger webSocket: IsomorphicWebSocketConstructor privateIdentity?: PrivateIdentity + hostsStore: HostsStore }) { this.files = files this.config = config @@ -108,7 +107,7 @@ export class IronfishNode { chain: chain, strategy: strategy, metrics: this.metrics, - addressManager: new AddressManager(hosts), + hostsStore: hostsStore, }) this.syncer = new Syncer({ @@ -164,8 +163,8 @@ export class IronfishNode { await internal.load() } - const hosts = new HostsStore(files, dataDir) - await hosts.load() + const hostsStore = new HostsStore(files, dataDir) + await hostsStore.load() if (databaseName) { config.setOverride('databaseName', databaseName) @@ -225,7 +224,6 @@ export class IronfishNode { files, config, internal, - hosts, accounts, metrics, miningDirector: mining, @@ -234,6 +232,7 @@ export class IronfishNode { logger, webSocket, privateIdentity, + hostsStore, }) } diff --git a/ironfish/src/sdk.ts b/ironfish/src/sdk.ts index 60261291db..e6b9313095 100644 --- a/ironfish/src/sdk.ts +++ b/ironfish/src/sdk.ts @@ -39,6 +39,7 @@ export class IronfishSdk { internal: InternalStore strategyClass: typeof Strategy | null privateIdentity: BoxKeyPair | null | undefined + dataDir?: string private constructor( pkg: Package, @@ -50,6 +51,7 @@ export class IronfishSdk { logger: Logger, metrics: MetricsMonitor, strategyClass: typeof Strategy | null = null, + dataDir?: string, ) { this.pkg = pkg this.client = client @@ -60,6 +62,7 @@ export class IronfishSdk { this.logger = logger this.metrics = metrics this.strategyClass = strategyClass + this.dataDir = dataDir } static async init({ @@ -155,6 +158,7 @@ export class IronfishSdk { logger, metrics, strategyClass, + dataDir, ) } @@ -181,6 +185,7 @@ export class IronfishSdk { strategyClass: this.strategyClass, webSocket: webSocket, privateIdentity: privateIdentity, + dataDir: this.dataDir, }) if (this.config.get('enableRpcIpc')) { From 1ee07b3a56f124fc5be2a20bd10e146a2cdeeaf9 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 9 Feb 2022 20:51:16 -0500 Subject: [PATCH 04/24] refactor(ironfish): Clean up existing Telemetry into single service (#966) --- ironfish/src/blockchain/blockchain.ts | 2 +- ironfish/src/metrics/metricsMonitor.ts | 55 +++--- ironfish/src/mining/director.ts | 10 +- ironfish/src/network/peerNetwork.ts | 2 +- ironfish/src/network/peers/peerManager.ts | 2 +- ironfish/src/node.ts | 45 +++-- ironfish/src/sdk.ts | 2 +- ironfish/src/syncer.ts | 6 +- ironfish/src/telemetry/DisabledTelemetry.ts | 43 ----- ironfish/src/telemetry/NodeTelemetry.ts | 57 ------- .../__snapshots__/submit.test.ts.snap | 92 ---------- ironfish/src/telemetry/index.ts | 158 +----------------- ironfish/src/telemetry/interfaces/field.ts | 8 + ironfish/src/telemetry/interfaces/metric.ts | 44 +++++ ironfish/src/telemetry/interfaces/tag.ts | 7 + ironfish/src/telemetry/submit.test.ts | 127 -------------- ironfish/src/telemetry/telemetry.test.ts | 118 +++++++++++++ ironfish/src/telemetry/telemetry.ts | 86 ++++++++++ .../telemetry/telemetryBackgroundTask.test.ts | 50 ------ .../src/telemetry/telemetryBackgroundTask.ts | 78 --------- ironfish/src/webApi.ts | 5 + ironfish/src/workerPool/messages.ts | 23 +-- ironfish/src/workerPool/pool.ts | 12 ++ ironfish/src/workerPool/tasks/index.ts | 4 + .../src/workerPool/tasks/submitTelemetry.ts | 22 +++ 25 files changed, 397 insertions(+), 661 deletions(-) delete mode 100644 ironfish/src/telemetry/DisabledTelemetry.ts delete mode 100644 ironfish/src/telemetry/NodeTelemetry.ts delete mode 100644 ironfish/src/telemetry/__snapshots__/submit.test.ts.snap create mode 100644 ironfish/src/telemetry/interfaces/field.ts create mode 100644 ironfish/src/telemetry/interfaces/metric.ts create mode 100644 ironfish/src/telemetry/interfaces/tag.ts delete mode 100644 ironfish/src/telemetry/submit.test.ts create mode 100644 ironfish/src/telemetry/telemetry.test.ts create mode 100644 ironfish/src/telemetry/telemetry.ts delete mode 100644 ironfish/src/telemetry/telemetryBackgroundTask.test.ts delete mode 100644 ironfish/src/telemetry/telemetryBackgroundTask.ts create mode 100644 ironfish/src/workerPool/tasks/submitTelemetry.ts diff --git a/ironfish/src/blockchain/blockchain.ts b/ironfish/src/blockchain/blockchain.ts index 9f9dcb577f..1e7cf4562c 100644 --- a/ironfish/src/blockchain/blockchain.ts +++ b/ironfish/src/blockchain/blockchain.ts @@ -148,7 +148,7 @@ export class Blockchain { this.strategy = options.strategy this.logger = logger.withTag('blockchain') - this.metrics = options.metrics || new MetricsMonitor(this.logger) + this.metrics = options.metrics || new MetricsMonitor({ logger: this.logger }) this.verifier = new Verifier(this) this.db = createDB({ location: options.location }) this.addSpeed = this.metrics.addMeter() diff --git a/ironfish/src/metrics/metricsMonitor.ts b/ironfish/src/metrics/metricsMonitor.ts index 589e924ca2..31159bae63 100644 --- a/ironfish/src/metrics/metricsMonitor.ts +++ b/ironfish/src/metrics/metricsMonitor.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { createRootLogger, Logger } from '../logger' -import { submitMetric } from '../telemetry' +import { Telemetry } from '../telemetry/telemetry' import { SetIntervalToken } from '../utils' import { Gauge } from './gauge' import { Meter } from './meter' @@ -11,7 +11,8 @@ import { Meter } from './meter' export class MetricsMonitor { private _started = false private _meters: Meter[] = [] - readonly logger: Logger + private readonly telemetry: Telemetry | null + private readonly logger: Logger readonly p2p_InboundTraffic: Meter readonly p2p_InboundTraffic_WS: Meter @@ -29,8 +30,9 @@ export class MetricsMonitor { private readonly memoryRefreshPeriodMs = 1000 private readonly memoryTelemetryPeriodMs = 15 * 1000 - constructor(logger: Logger = createRootLogger()) { - this.logger = logger + constructor({ telemetry, logger }: { telemetry?: Telemetry; logger?: Logger }) { + this.telemetry = telemetry ?? null + this.logger = logger ?? createRootLogger() this.p2p_InboundTraffic = this.addMeter() this.p2p_InboundTraffic_WS = this.addMeter() @@ -56,10 +58,12 @@ export class MetricsMonitor { this._meters.forEach((m) => m.start()) this.memoryInterval = setInterval(() => this.refreshMemory(), this.memoryRefreshPeriodMs) - this.memoryTelemetryInterval = setInterval( - () => this.submitMemoryTelemetry(), - this.memoryTelemetryPeriodMs, - ) + if (this.telemetry) { + this.memoryTelemetryInterval = setInterval( + () => void this.submitMemoryTelemetry(), + this.memoryTelemetryPeriodMs, + ) + } } stop(): void { @@ -91,21 +95,24 @@ export class MetricsMonitor { this.rss.value = memoryUsage.rss } - private submitMemoryTelemetry(): void { - submitMetric({ - name: 'memory', - fields: [ - { - name: 'heap_used', - type: 'integer', - value: this.heapUsed.value, - }, - { - name: 'heap_total', - type: 'integer', - value: this.heapTotal.value, - }, - ], - }) + private async submitMemoryTelemetry(): Promise { + if (this.telemetry) { + await this.telemetry.submit({ + measurement: 'node', + name: 'memory', + fields: [ + { + name: 'heap_used', + type: 'integer', + value: this.heapUsed.value, + }, + { + name: 'heap_total', + type: 'integer', + value: this.heapTotal.value, + }, + ], + }) + } } } diff --git a/ironfish/src/mining/director.ts b/ironfish/src/mining/director.ts index e63e67e1e4..a6837cad08 100644 --- a/ironfish/src/mining/director.ts +++ b/ironfish/src/mining/director.ts @@ -14,7 +14,7 @@ import { BlockHash, BlockHeader, BlockHeaderSerde } from '../primitives/blockhea import { Target } from '../primitives/target' import { Transaction } from '../primitives/transaction' import { Strategy } from '../strategy' -import { submitMetric } from '../telemetry' +import { Telemetry } from '../telemetry/telemetry' import { AsyncUtils, ErrorUtils, GraffitiUtils, SetTimeoutToken } from '../utils' /** @@ -41,6 +41,7 @@ type DirectorState = { type: 'STARTED' } | { type: 'STOPPED' } export class MiningDirector { readonly chain: Blockchain readonly memPool: MemPool + readonly telemetry: Telemetry /** * The event creates a block header with loose transactions that have been @@ -148,6 +149,7 @@ export class MiningDirector { chain: Blockchain memPool: MemPool strategy: Strategy + telemetry: Telemetry logger?: Logger graffiti?: string account?: Account @@ -157,6 +159,7 @@ export class MiningDirector { this.chain = options.chain this.memPool = options.memPool + this.telemetry = options.telemetry this.strategy = options.strategy this.logger = logger.withTag('director') @@ -498,8 +501,9 @@ export class MiningDirector { this.onNewBlock.emit(block) - submitMetric({ - name: 'minedBlock', + await this.telemetry.submit({ + measurement: 'node', + name: 'block_mined', fields: [ { name: 'difficulty', diff --git a/ironfish/src/network/peerNetwork.ts b/ironfish/src/network/peerNetwork.ts index e1fa558de1..f0d0894c3b 100644 --- a/ironfish/src/network/peerNetwork.ts +++ b/ironfish/src/network/peerNetwork.ts @@ -156,7 +156,7 @@ export class PeerNetwork { this.chain = options.chain this.strategy = options.strategy this.logger = (options.logger || createRootLogger()).withTag('peernetwork') - this.metrics = options.metrics || new MetricsMonitor(this.logger) + this.metrics = options.metrics || new MetricsMonitor({ logger: this.logger }) this.bootstrapNodes = options.bootstrapNodes || [] this.localPeer = new LocalPeer( diff --git a/ironfish/src/network/peers/peerManager.ts b/ironfish/src/network/peers/peerManager.ts index 7ae8842c60..008992aa93 100644 --- a/ironfish/src/network/peers/peerManager.ts +++ b/ironfish/src/network/peers/peerManager.ts @@ -156,7 +156,7 @@ export class PeerManager { logPeerMessages = false, ) { this.logger = logger.withTag('peermanager') - this.metrics = metrics || new MetricsMonitor(this.logger) + this.metrics = metrics || new MetricsMonitor({ logger: this.logger }) this.localPeer = localPeer this.maxPeers = maxPeers this.targetPeers = targetPeers diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index 28edd6f2cb..072c4235fc 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -18,7 +18,7 @@ import { Platform } from './platform' import { RpcServer } from './rpc/server' import { Strategy } from './strategy' import { Syncer } from './syncer' -import { setDefaultTags, startCollecting, stopCollecting, submitMetric } from './telemetry' +import { Telemetry } from './telemetry/telemetry' import { WorkerPool } from './workerPool' export class IronfishNode { @@ -37,6 +37,7 @@ export class IronfishNode { peerNetwork: PeerNetwork syncer: Syncer pkg: Package + telemetry: Telemetry started = false shutdownPromise: Promise | null = null @@ -56,6 +57,7 @@ export class IronfishNode { workerPool, logger, webSocket, + telemetry, privateIdentity, hostsStore, }: { @@ -72,6 +74,7 @@ export class IronfishNode { workerPool: WorkerPool logger: Logger webSocket: IsomorphicWebSocketConstructor + telemetry: Telemetry privateIdentity?: PrivateIdentity hostsStore: HostsStore }) { @@ -88,6 +91,7 @@ export class IronfishNode { this.rpc = new RpcServer(this) this.logger = logger this.pkg = pkg + this.telemetry = telemetry this.peerNetwork = new PeerNetwork({ identity: privateIdentity, @@ -111,9 +115,10 @@ export class IronfishNode { }) this.syncer = new Syncer({ - chain: chain, - metrics: metrics, - logger: logger, + chain, + metrics, + logger, + telemetry, peerNetwork: this.peerNetwork, strategy: this.strategy, blocksPerMessage: config.get('blocksPerMessage'), @@ -151,7 +156,6 @@ export class IronfishNode { privateIdentity?: PrivateIdentity }): Promise { logger = logger.withTag('ironfishnode') - metrics = metrics || new MetricsMonitor(logger) if (!config) { config = new Config(files, dataDir) @@ -184,6 +188,14 @@ export class IronfishNode { strategyClass = strategyClass || Strategy const strategy = new strategyClass(workerPool) + const telemetry = new Telemetry(config, workerPool, logger, [ + { name: 'node_id', value: internal.get('telemetryNodeId') }, + { name: 'session_id', value: uuid() }, + { name: 'version', value: pkg.version }, + ]) + + metrics = metrics || new MetricsMonitor({ telemetry, logger }) + const chain = new Blockchain({ location: config.chainDatabasePath, strategy, @@ -203,20 +215,15 @@ export class IronfishNode { const accounts = new Accounts({ database: accountDB, workerPool: workerPool, chain: chain }) const mining = new MiningDirector({ - chain: chain, + chain, + memPool, + telemetry, strategy: strategy, - memPool: memPool, logger: logger, graffiti: config.get('blockGraffiti'), force: config.get('miningForce'), }) - setDefaultTags([ - { name: 'node_id', value: internal.get('telemetryNodeId') }, - { name: 'session_id', value: uuid() }, - { name: 'version', value: pkg.version }, - ]) - return new IronfishNode({ pkg, chain, @@ -231,6 +238,7 @@ export class IronfishNode { workerPool, logger, webSocket, + telemetry, privateIdentity, hostsStore, }) @@ -274,7 +282,7 @@ export class IronfishNode { this.workerPool.start() if (this.config.get('enableTelemetry')) { - startCollecting(this.config.get('telemetryApi')) + this.telemetry.start() } if (this.config.get('enableMetrics')) { @@ -288,7 +296,8 @@ export class IronfishNode { await this.rpc.start() } - submitMetric({ + await this.telemetry.submit({ + measurement: 'node', name: 'started', fields: [{ name: 'online', type: 'boolean', value: true }], }) @@ -304,7 +313,7 @@ export class IronfishNode { this.syncer.stop(), this.peerNetwork.stop(), this.rpc.stop(), - stopCollecting(), + this.telemetry.stop(), this.metrics.stop(), this.workerPool.stop(), this.miningDirector.shutdown(), @@ -347,9 +356,9 @@ export class IronfishNode { } case 'enableTelemetry': { if (newValue) { - startCollecting(this.config.get('telemetryApi')) + this.telemetry.start() } else { - await stopCollecting() + this.telemetry.stop() } break } diff --git a/ironfish/src/sdk.ts b/ironfish/src/sdk.ts index e6b9313095..49676493c5 100644 --- a/ironfish/src/sdk.ts +++ b/ironfish/src/sdk.ts @@ -128,7 +128,7 @@ export class IronfishSdk { } if (!metrics) { - metrics = metrics || new MetricsMonitor(logger) + metrics = metrics || new MetricsMonitor({ logger }) } const client = new IronfishIpcClient( diff --git a/ironfish/src/syncer.ts b/ironfish/src/syncer.ts index 62678b14fb..a2a596bfe3 100644 --- a/ironfish/src/syncer.ts +++ b/ironfish/src/syncer.ts @@ -13,6 +13,7 @@ import { BAN_SCORE, PeerState } from './network/peers/peer' import { Block, SerializedBlock } from './primitives/block' import { BlockHeader } from './primitives/blockheader' import { Strategy } from './strategy' +import { Telemetry } from './telemetry' import { BenchUtils, ErrorUtils, HashUtils, MathUtils, SetTimeoutToken } from './utils' import { ArrayUtils } from './utils/array' @@ -45,6 +46,7 @@ export class Syncer { peerNetwork: PeerNetwork chain: Blockchain strategy: Strategy + telemetry: Telemetry metrics?: MetricsMonitor logger?: Logger blocksPerMessage?: number @@ -54,8 +56,10 @@ export class Syncer { this.peerNetwork = options.peerNetwork this.chain = options.chain this.strategy = options.strategy - this.metrics = options.metrics || new MetricsMonitor() this.logger = logger.withTag('syncer') + this.metrics = + options.metrics || + new MetricsMonitor({ telemetry: options.telemetry, logger: this.logger }) this.state = 'stopped' this.speed = this.metrics.addMeter() diff --git a/ironfish/src/telemetry/DisabledTelemetry.ts b/ironfish/src/telemetry/DisabledTelemetry.ts deleted file mode 100644 index 1f6e50dfc3..0000000000 --- a/ironfish/src/telemetry/DisabledTelemetry.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { EnabledTelemetry, Metric, Telemetry } from '.' - -/** - * Implementation of Telemetry interface that discards metrics. - */ - -export default class DisabledTelemetry implements Telemetry { - /** - * Called if the user requests to stop submitting metrics. - * Returns a new NodeTelemetry on node; can be adapted for use in browser, - * but that isn't implemented yet. - * - * @returns an enabled telemetry and a status message to display to the user - */ - startCollecting(endpoint: string): { status: string; next: Telemetry } { - return { status: 'Collecting telemetry data', next: new EnabledTelemetry(endpoint) } - } - - /** - * Called if the user requests to stop submitting metrics. - * Since disabled telemetry is already not submitting metrics, - * it is a noop - * - * @returns this and a status message to send to the user - */ - async stopCollecting(): Promise<{ status: string; next: Telemetry }> { - return Promise.resolve({ status: "Not collecting telemetry; can't stop now", next: this }) - } - - /** - * Black hole to submit metrics to when telemetry is disabled. - */ - submit(_metric: Metric): void { - // discard - } - - isEnabled(): boolean { - return false - } -} diff --git a/ironfish/src/telemetry/NodeTelemetry.ts b/ironfish/src/telemetry/NodeTelemetry.ts deleted file mode 100644 index 761b678d5e..0000000000 --- a/ironfish/src/telemetry/NodeTelemetry.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Worker } from 'worker_threads' -import DisabledTelemetry from './DisabledTelemetry' -import { Metric, Telemetry } from './index' - -/** - * Telemetry implementation that sends metrics to a node worker thread - * to be posted. - */ - -export default class NodeTelemetry implements Telemetry { - worker: Worker - - constructor(endpoint: string) { - this.worker = new Worker(__dirname + '/telemetryBackgroundTask.js', { - workerData: { endpoint }, - }) - } - - /** - * Called if the user requests to submit metrics. - * This is a noop if metrics are already enabled. - * - * @returns this and a status message to send to the user - */ - startCollecting(_endpoint: string): { status: string; next: Telemetry } { - return { status: 'Telemetry is already enabled', next: this } - } - - /** - * Called if the user request to stop recording metrics. - * - * Shut down the workers read and returns new DisabledTelemetry - * - * @returns new DisabledTelemetry to replace this one and a status message - * to send to the user - */ - async stopCollecting(): Promise<{ status: string; next: Telemetry }> { - await this.worker.terminate() - return { status: 'Stopped collecting telemetry', next: new DisabledTelemetry() } - } - - /** - * Submit the provided metric to the metric server. - * - * This returns immediately, but a background task is scheduled. - */ - submit(metric: Metric): void { - this.worker.postMessage(metric) - } - - isEnabled(): boolean { - return true - } -} diff --git a/ironfish/src/telemetry/__snapshots__/submit.test.ts.snap b/ironfish/src/telemetry/__snapshots__/submit.test.ts.snap deleted file mode 100644 index b9a7549748..0000000000 --- a/ironfish/src/telemetry/__snapshots__/submit.test.ts.snap +++ /dev/null @@ -1,92 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Telemetry submitMetric function Succeeds with a validly formatted metric 1`] = ` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "fields": Array [ - Object { - "name": "hello", - "type": "string", - "value": "world", - }, - ], - "name": "test metric", - "tags": Array [ - Object { - "name": "you know", - "value": "me", - }, - ], - "timestamp": 2020-12-31T00:00:00.000Z, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`Telemetry submitMetric function submits with default date if unspecified 1`] = ` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "fields": Array [ - Object { - "name": "hello", - "type": "string", - "value": "world", - }, - ], - "name": "test metric", - "tags": Array [ - Object { - "name": "you know", - "value": "me", - }, - ], - "timestamp": 1999-12-31T00:00:00.000Z, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`Telemetry submitMetric function submits with no tags if unspecified 1`] = ` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "fields": Array [ - Object { - "name": "hello", - "type": "string", - "value": "world", - }, - ], - "name": "test metric", - "tags": Array [], - "timestamp": 2020-12-31T00:00:00.000Z, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], -} -`; diff --git a/ironfish/src/telemetry/index.ts b/ironfish/src/telemetry/index.ts index d73f2af1f8..be80595fd1 100644 --- a/ironfish/src/telemetry/index.ts +++ b/ironfish/src/telemetry/index.ts @@ -1,157 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -// WARNING: this file contains nodejs-specific functionality -// that will need to be ported to the browser. - -import DisabledTelemetry from './DisabledTelemetry' -import NodeTelemetry from './NodeTelemetry' - -export { NodeTelemetry, DisabledTelemetry } - -export type Field = { - name: string - type: 'string' | 'boolean' | 'float' | 'integer' - value: string | boolean | number -} - -interface Tag { - name: string - value: string -} - -/** - * A specific datapoint being collected. - */ -export type Metric = { - /** - * The name of whatever is being measured. - */ - name: string - /** - * The exact time at which the metric was recorded. - * JS gives us millisecond accuracy here. - * Defaults to new Date() if not specified - */ - timestamp?: Date - /** - * Collection of string keys and values to help identify - * this metric. - * - * Expected values will be something like: "clientid": "xxx" - * or "software version": "xxx". - */ - tags?: Tag[] - /** - * Array of measured values for this particular measurement. - * There must be at least one field. - * Each field has a name, type, and a single value. - */ - fields: Field[] -} - -/** - * Tool for collecting metrics. Connects to a node and sets up - * event listeners for all known metrics. - */ -export interface Telemetry { - startCollecting(endpoint: string): { status: string; next: Telemetry } - stopCollecting(): Promise<{ - next: Telemetry - status: string - }> - submit(metric: Metric): void - isEnabled(): boolean -} - -// This can be changed to a switch for browser implementation -export const EnabledTelemetry = NodeTelemetry - -let telemetry: Telemetry = new DisabledTelemetry() - -// List of tags that get added to every metric. -let defaultTags: Tag[] = [] - -/** - * Check if telemetry reporting is currently active - */ -export function isEnabled(): boolean { - return telemetry.isEnabled() -} - -/** - * Set the telemetry used for collecting metrics. - * - * This is primarily exposed for unit testing and initialization. - * Prefer the startCollecting and stopCollecting state managers - * in the general case. - */ -export function setTelemetry(newTelemetry: Telemetry): void { - telemetry = newTelemetry -} - -/** - * Instruct the current telemetry to start collecting data. - * - * Is a noop if it is already collecting. - * - * Returns a status message intended for be displayed to the user - */ -export function startCollecting(endpoint: string): string { - const result = telemetry.startCollecting(endpoint) - telemetry = result.next - return result.status -} - -/** - * Instruct the current telemetry to stop collecting data. - * - * Is a noop if it is not collecting. - * - * Returns a status message intended for display to the user - */ -export async function stopCollecting(): Promise { - const result = await telemetry.stopCollecting() - telemetry = result.next - return result.status -} - -/** - * Set key-value tags that get attached to every - * request. - * - * These will probably be set on node startup, and never - * changed. - * - * They can be set before telemetry is enabled. - */ -export function setDefaultTags(tags: Tag[]): void { - defaultTags = tags -} - -/** - * Submit a metric to the telemetry service. - * - * This can be called unconditionally; the currently enabled - * telemetry will decide whether to discard it if telemetry - * is disabled. - */ -export function submitMetric(metric: Metric): void { - if (metric.fields.length === 0) { - throw new Error('Metric must have at least one field') - } - - let tags = defaultTags - if (metric.tags) { - tags = tags.concat(metric.tags) - } - - const toSubmit = { - ...metric, - timestamp: metric.timestamp || new Date(), - tags, - } - - telemetry.submit(toSubmit) -} +export { Field } from './interfaces/field' +export { Metric } from './interfaces/metric' +export { Tag } from './interfaces/tag' +export { Telemetry } from './telemetry' diff --git a/ironfish/src/telemetry/interfaces/field.ts b/ironfish/src/telemetry/interfaces/field.ts new file mode 100644 index 0000000000..96a07a2402 --- /dev/null +++ b/ironfish/src/telemetry/interfaces/field.ts @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +export interface Field { + name: string + type: 'string' | 'boolean' | 'float' | 'integer' + value: string | boolean | number +} diff --git a/ironfish/src/telemetry/interfaces/metric.ts b/ironfish/src/telemetry/interfaces/metric.ts new file mode 100644 index 0000000000..2a09dbfe0b --- /dev/null +++ b/ironfish/src/telemetry/interfaces/metric.ts @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Field } from './field' +import { Tag } from './tag' + +/** + * A specific datapoint being collected. + */ +export interface Metric { + /** + * A description for the container that the fields measure. Defaults to + * 'node' because all metrics are submitted from an Iron Fish node. + */ + measurement: 'node' + + /** + * The name of whatever is being measured. + */ + name: string + + /** + * The exact time at which the metric was recorded. + * JS gives us millisecond accuracy here. + * Defaults to new Date() if not specified + */ + timestamp?: Date + + /** + * Collection of string keys and values to help identify + * this metric. + * + * Expected values will be something like: "client_id": "xxx" + * or "version": "xxx". + */ + tags?: Tag[] + + /** + * Array of measured values for this particular measurement. + * There must be at least one field. + * Each field has a name, type, and a single value. + */ + fields: Field[] +} diff --git a/ironfish/src/telemetry/interfaces/tag.ts b/ironfish/src/telemetry/interfaces/tag.ts new file mode 100644 index 0000000000..c00ccad75f --- /dev/null +++ b/ironfish/src/telemetry/interfaces/tag.ts @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +export interface Tag { + name: string + value: string +} diff --git a/ironfish/src/telemetry/submit.test.ts b/ironfish/src/telemetry/submit.test.ts deleted file mode 100644 index 2aeb25678e..0000000000 --- a/ironfish/src/telemetry/submit.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -import { Worker } from 'worker_threads' -import { - DisabledTelemetry, - EnabledTelemetry, - Metric, - setDefaultTags, - setTelemetry, - submitMetric, -} from '.' - -jest.mock('worker_threads') -// Tell typescript to treat it as a mock -const MockWorker = Worker as unknown as jest.Mock - -describe('Enabled and disabled telemetry', () => { - const metric: Metric = { - name: 'test metric', - timestamp: new Date('2020-12-31'), - fields: [{ name: 'hello', type: 'string', value: 'world' }], - } - - beforeEach(() => { - MockWorker.mockReset() - }) - - it("doesn't crash when submitting a metric to disabled telemetry", () => { - const telemetry = new DisabledTelemetry() - expect(() => telemetry.submit(metric)).not.toThrow() - expect(Worker).not.toHaveBeenCalled() - }) - - it('submits to the worker when submitting to enabled telemetry', () => { - const telemetry = new EnabledTelemetry('an url') - expect(() => telemetry.submit(metric)).not.toThrow() - expect(telemetry.worker.postMessage).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "fields": Array [ - Object { - "name": "hello", - "type": "string", - "value": "world", - }, - ], - "name": "test metric", - "timestamp": 2020-12-31T00:00:00.000Z, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - } - `) - }) -}) - -describe('Telemetry submitMetric function', () => { - const metric: Metric = { - name: 'test metric', - timestamp: new Date('2020-12-31'), - tags: [{ name: 'you know', value: 'me' }], - fields: [{ name: 'hello', type: 'string', value: 'world' }], - } - - const telemetry = new DisabledTelemetry() - const mockSubmit = jest.fn() - telemetry.submit = mockSubmit - setTelemetry(telemetry) - - beforeEach(() => { - mockSubmit.mockClear() - setDefaultTags([]) - }) - - it('Succeeds with a validly formatted metric', () => { - submitMetric(metric) - expect(mockSubmit).toMatchSnapshot() - }) - - it('throws if fields is empty', () => { - const fieldlessMetric = { ...metric } - fieldlessMetric.fields = [] - expect(() => submitMetric(fieldlessMetric)).toThrowErrorMatchingInlineSnapshot( - `"Metric must have at least one field"`, - ) - expect(mockSubmit).not.toBeCalled() - }) - - it('submits with no tags if unspecified', () => { - const taglessMetric = { ...metric } - delete taglessMetric.tags - submitMetric(taglessMetric) - expect(mockSubmit).toMatchSnapshot() - }) - - it('submits with default tags', () => { - setDefaultTags([{ name: 'my', value: 'default tag' }]) - submitMetric(metric) - const expectedMetric = { - ...metric, - tags: [ - { name: 'my', value: 'default tag' }, - { name: 'you know', value: 'me' }, - ], - } - expect(mockSubmit.mock.calls).toMatchObject([[expectedMetric]]) - }) - - it('submits with default date if unspecified', () => { - const now = new Date('1999-12-31') - jest.spyOn(global, 'Date').mockImplementation(() => now as unknown as string) - const datelessMetric = { ...metric } - delete datelessMetric.timestamp - submitMetric(datelessMetric) - expect(mockSubmit).toMatchSnapshot() - }) -}) diff --git a/ironfish/src/telemetry/telemetry.test.ts b/ironfish/src/telemetry/telemetry.test.ts new file mode 100644 index 0000000000..ffc9531f72 --- /dev/null +++ b/ironfish/src/telemetry/telemetry.test.ts @@ -0,0 +1,118 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Metric } from './interfaces/metric' +import { Telemetry } from './telemetry' + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +describe('Telemetry', () => { + let telemetry: Telemetry + + const mockTelemetry = (enabled = true): Telemetry => { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const mockConfig: any = { + get: jest.fn().mockResolvedValueOnce(enabled), + } + const mockPool: any = { + submitTelemetry: jest.fn(), + } + const mockLogger: any = { + debug: jest.fn(), + error: jest.fn(), + } + /* eslint-enable @typescript-eslint/no-explicit-any */ + return new Telemetry(mockConfig, mockPool, mockLogger, []) + } + + const mockMetric: Metric = { + measurement: 'node', + name: 'memory', + fields: [ + { + name: 'heap_used', + type: 'integer', + value: 0, + }, + ], + } + + beforeEach(() => { + telemetry = mockTelemetry() + }) + + describe('submit', () => { + describe('when disabled', () => { + it('does nothing', async () => { + const disabledTelemetry = mockTelemetry(false) + const currentPoints = disabledTelemetry['points'] + await disabledTelemetry.submit(mockMetric) + expect(disabledTelemetry['points']).toEqual(currentPoints) + }) + }) + + describe('when submitting a metric without fields', () => { + it('throws an error', async () => { + const metric: Metric = { + measurement: 'node', + name: 'memory', + fields: [], + } + await expect(telemetry.submit(metric)).rejects.toThrowError() + }) + }) + + describe('when the queue max size has been reached', () => { + it('flushes the queue', async () => { + const flush = jest.spyOn(telemetry, 'flush') + const points = [] + for (let i = 0; i < telemetry['MAX_QUEUE_SIZE']; i++) { + points.push(mockMetric) + } + telemetry['points'] = points + + await telemetry.submit(mockMetric) + expect(flush).toHaveBeenCalled() + }) + }) + + it('stores the metric', async () => { + const currentPointsLength = telemetry['points'].length + await telemetry.submit(mockMetric) + + const points = telemetry['points'] + expect(points).toHaveLength(currentPointsLength + 1) + expect(points[points.length - 1]).toMatchObject(mockMetric) + }) + }) + + describe('flush', () => { + describe('when the pool throws an error and the queue is not saturated', () => { + it('retries the points and logs an error', async () => { + jest.spyOn(telemetry['pool'], 'submitTelemetry').mockImplementationOnce(() => { + throw new Error() + }) + const error = jest.spyOn(telemetry['logger'], 'error') + + const points = [] + for (let i = 0; i < telemetry['MAX_QUEUE_SIZE'] - 1; i++) { + points.push(mockMetric) + } + telemetry['points'] = points + + await telemetry.flush() + expect(telemetry['points']).toEqual(points) + expect(error).toHaveBeenCalled() + }) + }) + + it('submits telemetry to the pool', async () => { + const submitTelemetry = jest.spyOn(telemetry['pool'], 'submitTelemetry') + await telemetry.submit(mockMetric) + await telemetry.flush() + + expect(submitTelemetry).toHaveBeenCalled() + }) + }) +}) diff --git a/ironfish/src/telemetry/telemetry.ts b/ironfish/src/telemetry/telemetry.ts new file mode 100644 index 0000000000..6dbb57edfb --- /dev/null +++ b/ironfish/src/telemetry/telemetry.ts @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Config } from '../fileStores' +import { Logger } from '../logger' +import { renderError, SetIntervalToken } from '../utils' +import { WorkerPool } from '../workerPool' +import { Metric } from './interfaces/metric' +import { Tag } from './interfaces/tag' + +export class Telemetry { + private readonly FLUSH_INTERVAL = 5000 + private readonly MAX_QUEUE_SIZE = 1000 + + private readonly enabled: boolean + private readonly defaultTags: Tag[] + private readonly logger: Logger + private readonly pool: WorkerPool + + private flushInterval: SetIntervalToken | null + private points: Metric[] + + constructor(config: Config, pool: WorkerPool, logger: Logger, defaultTags: Tag[]) { + this.enabled = config.get('enableTelemetry') + this.logger = logger + this.pool = pool + this.defaultTags = defaultTags + + this.flushInterval = null + this.points = [] + } + + start(): void { + if (this.enabled) { + this.flushInterval = setInterval(() => void this.flush(), this.FLUSH_INTERVAL) + } + } + + stop(): void { + if (this.flushInterval) { + clearTimeout(this.flushInterval) + } + } + + async submit(metric: Metric): Promise { + if (!this.enabled) { + return + } + + if (metric.fields.length === 0) { + throw new Error('Cannot submit metrics without fields') + } + + let tags = this.defaultTags + if (metric.tags) { + tags = tags.concat(metric.tags) + } + + this.points.push({ + ...metric, + timestamp: metric.timestamp || new Date(), + tags, + }) + + if (this.points.length >= this.MAX_QUEUE_SIZE) { + await this.flush() + } + } + + async flush(): Promise { + const points = this.points + this.points = [] + + try { + await this.pool.submitTelemetry(points) + this.logger.debug(`Submitted ${points.length} telemetry points`) + } catch (error: unknown) { + this.logger.error(`Error submitting telemetry to API: ${renderError(error)}`) + + if (points.length < this.MAX_QUEUE_SIZE) { + this.logger.debug('Retrying telemetry submission') + this.points = points + } + } + } +} diff --git a/ironfish/src/telemetry/telemetryBackgroundTask.test.ts b/ironfish/src/telemetry/telemetryBackgroundTask.test.ts deleted file mode 100644 index 15061f6519..0000000000 --- a/ironfish/src/telemetry/telemetryBackgroundTask.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -import axios from 'axios' -import { Metric } from '.' -import { handleMetric, MAX_QUEUE_BEFORE_SUBMIT, sendMetrics } from './telemetryBackgroundTask' - -jest.mock('worker_threads') -jest.mock('axios') - -describe('Telemetry background thread', () => { - const postMock = jest.fn().mockImplementation(() => Promise.resolve({})) - axios.post = postMock - const metric: Metric = { - name: 'test metric', - timestamp: new Date('2020-12-31'), - fields: [{ name: 'hello', type: 'string', value: 'world' }], - } - const endpoint = 'http://localhost:8000/writeMetric' - - afterEach(() => { - postMock.mockClear() - }) - - it('posts a metric', () => { - handleMetric(metric, endpoint) - expect(postMock).not.toHaveBeenCalled() - sendMetrics(endpoint) - expect(axios.post).toHaveBeenCalledWith('http://localhost:8000/writeMetric', { - points: [ - { - measurement: 'node', - name: 'test metric', - timestamp: new Date('2020-12-31'), - fields: [{ name: 'hello', type: 'string', value: 'world' }], - }, - ], - }) - }) - - it('posts immediately if there are many metrics', () => { - for (let i = 0; i < MAX_QUEUE_BEFORE_SUBMIT; i++) { - handleMetric(metric, endpoint) - } - expect(postMock).not.toHaveBeenCalled() - handleMetric(metric, endpoint) - expect(postMock).toHaveBeenCalled() - }) -}) diff --git a/ironfish/src/telemetry/telemetryBackgroundTask.ts b/ironfish/src/telemetry/telemetryBackgroundTask.ts deleted file mode 100644 index 0dd4547b6f..0000000000 --- a/ironfish/src/telemetry/telemetryBackgroundTask.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -/** - * You might think metrics are an io-bound problem, but in order to support batching - * and retries, we've placed them on a worker thread. - */ - -// WARNING: This file only runs on node and will need to be ported -// to webworkers to collect metrics in the browser - -import axios, { AxiosError } from 'axios' -import { MessagePort, parentPort, workerData } from 'worker_threads' -import { createRootLogger, Logger } from '../logger' -import { Metric } from '.' - -/// 5 seconds between sending batches of metrics -const BATCH_INTERVAL = 5000 -/// Send batch early if the queue is large -export const MAX_QUEUE_BEFORE_SUBMIT = 1000 -/// Max length of queue before dumping metrics (in event of network outage) -const MAX_QUEUE_BEFORE_DUMP = 10000 - -type MetricOnWire = Metric & { - measurement: 'node' -} - -let metrics: MetricOnWire[] = [] - -export function handleMetric(metric: Metric, endpoint: string, logger?: Logger): void { - metrics.push({ - ...metric, - measurement: 'node', - }) - if (metrics.length > MAX_QUEUE_BEFORE_SUBMIT) { - sendMetrics(endpoint, logger) - } -} - -export function sendMetrics(endpoint: string, logger?: Logger): void { - if (metrics.length === 0) { - return - } - - const toSubmit = metrics - metrics = [] - - axios - .post(endpoint, { points: toSubmit }) - .then(() => { - if (logger) { - logger.debug(`Submitted batch of ${toSubmit.length} metrics`) - } - }) - .catch((err: AxiosError) => { - if (logger) { - logger.warn('Unable to submit metrics', err.code || '') - } - - // Put the metrics back on the queue to try again - // But if metric server is unavailable dump buffer to prevent memory leak - if (metrics.length < MAX_QUEUE_BEFORE_DUMP) { - metrics.push(...toSubmit) - } - }) -} - -export function startTelemetryWorker(port: MessagePort): void { - const logger = createRootLogger().withTag('telemetryWorker') - const { endpoint } = workerData as unknown as { endpoint: string } - port.on('message', (metric: Metric) => handleMetric(metric, endpoint, logger)) - setInterval(() => sendMetrics(endpoint, logger), BATCH_INTERVAL) -} - -if (parentPort !== null) { - startTelemetryWorker(parentPort) -} diff --git a/ironfish/src/webApi.ts b/ironfish/src/webApi.ts index 6070c29e98..769f628f60 100644 --- a/ironfish/src/webApi.ts +++ b/ironfish/src/webApi.ts @@ -4,6 +4,7 @@ import axios, { AxiosRequestConfig } from 'axios' import { FollowChainStreamResponse } from './rpc/routes/chain/followChain' +import { Metric } from './telemetry' import { UnwrapPromise } from './utils/types' type FaucetTransaction = { @@ -139,6 +140,10 @@ export class WebApi { return response.data } + async submitTelemetry(payload: { points: Metric[] }): Promise { + await axios.post(`${this.host}/telemetry`, payload) + } + options(headers: Record = {}): AxiosRequestConfig { return { headers: { diff --git a/ironfish/src/workerPool/messages.ts b/ironfish/src/workerPool/messages.ts index 4573a9c1a4..ecd7975033 100644 --- a/ironfish/src/workerPool/messages.ts +++ b/ironfish/src/workerPool/messages.ts @@ -9,6 +9,7 @@ import { CreateTransactionRequest, CreateTransactionResponse } from './tasks/cre import { GetUnspentNotesRequest, GetUnspentNotesResponse } from './tasks/getUnspentNotes' import { MineHeaderRequest, MineHeaderResponse } from './tasks/mineHeader' import { SleepRequest, SleepResponse } from './tasks/sleep' +import { SubmitTelemetryRequest, SubmitTelemetryResponse } from './tasks/submitTelemetry' import { TransactionFeeRequest, TransactionFeeResponse } from './tasks/transactionFee' import { UnboxMessageRequest, UnboxMessageResponse } from './tasks/unboxMessage' import { VerifyTransactionRequest, VerifyTransactionResponse } from './tasks/verifyTransaction' @@ -38,25 +39,27 @@ export type WorkerResponseMessage = { } export type WorkerRequest = + | BoxMessageRequest | CreateMinersFeeRequest | CreateTransactionRequest | GetUnspentNotesRequest - | TransactionFeeRequest - | VerifyTransactionRequest - | BoxMessageRequest - | UnboxMessageRequest + | JobAbortRequest | MineHeaderRequest | SleepRequest - | JobAbortRequest + | SubmitTelemetryRequest + | TransactionFeeRequest + | UnboxMessageRequest + | VerifyTransactionRequest export type WorkerResponse = + | BoxMessageResponse | CreateMinersFeeResponse | CreateTransactionResponse | GetUnspentNotesResponse - | TransactionFeeResponse - | VerifyTransactionResponse - | BoxMessageResponse - | UnboxMessageResponse + | JobErrorResponse | MineHeaderResponse | SleepResponse - | JobErrorResponse + | SubmitTelemetryResponse + | TransactionFeeResponse + | UnboxMessageResponse + | VerifyTransactionResponse diff --git a/ironfish/src/workerPool/pool.ts b/ironfish/src/workerPool/pool.ts index 917c6cf286..8468ac166c 100644 --- a/ironfish/src/workerPool/pool.ts +++ b/ironfish/src/workerPool/pool.ts @@ -20,8 +20,10 @@ import { Meter, MetricsMonitor } from '../metrics' import { Identity, PrivateIdentity } from '../network' import { Note } from '../primitives/note' import { Transaction } from '../primitives/transaction' +import { Metric } from '../telemetry/interfaces/metric' import { Job } from './job' import { WorkerRequest } from './messages' +import { SubmitTelemetryRequest } from './tasks/submitTelemetry' import { VerifyTransactionOptions } from './tasks/verifyTransaction' import { getWorkerPath, Worker } from './worker' @@ -57,6 +59,7 @@ export class WorkerPool { ['mineHeader', { complete: 0, error: 0, queue: 0, execute: 0 }], ['transactionFee', { complete: 0, error: 0, queue: 0, execute: 0 }], ['jobAbort', { complete: 0, error: 0, queue: 0, execute: 0 }], + ['submitTelemetry', { complete: 0, error: 0, queue: 0, execute: 0 }], ]) get saturated(): boolean { @@ -324,6 +327,15 @@ export class WorkerPool { return job } + async submitTelemetry(points: Metric[]): Promise { + const request: SubmitTelemetryRequest = { + type: 'submitTelemetry', + points, + } + + await this.execute(request).result() + } + private execute(request: Readonly): Job { const jobId = this.lastJobId++ const job = new Job({ jobId: jobId, body: request }) diff --git a/ironfish/src/workerPool/tasks/index.ts b/ironfish/src/workerPool/tasks/index.ts index 1f9b99a338..fae818b6dd 100644 --- a/ironfish/src/workerPool/tasks/index.ts +++ b/ironfish/src/workerPool/tasks/index.ts @@ -11,6 +11,7 @@ import { handleCreateTransaction } from './createTransaction' import { handleGetUnspentNotes } from './getUnspentNotes' import { handleMineHeader } from './mineHeader' import { handleSleep } from './sleep' +import { submitTelemetry } from './submitTelemetry' import { handleTransactionFee } from './transactionFee' import { handleUnboxMessage } from './unboxMessage' import { handleVerifyTransaction } from './verifyTransaction' @@ -63,6 +64,9 @@ export async function handleRequest( break case 'jobAbort': throw new Error('ControlMessage not handled') + case 'submitTelemetry': + response = await submitTelemetry(body) + break default: { Assert.isNever(body) } diff --git a/ironfish/src/workerPool/tasks/submitTelemetry.ts b/ironfish/src/workerPool/tasks/submitTelemetry.ts new file mode 100644 index 0000000000..a55c9d8154 --- /dev/null +++ b/ironfish/src/workerPool/tasks/submitTelemetry.ts @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Metric } from '../../telemetry/interfaces/metric' +import { WebApi } from '../../webApi' + +export type SubmitTelemetryRequest = { + type: 'submitTelemetry' + points: Metric[] +} + +export type SubmitTelemetryResponse = { + type: 'submitTelemetry' +} + +export async function submitTelemetry({ + points, +}: SubmitTelemetryRequest): Promise { + const api = new WebApi() + await api.submitTelemetry({ points }) + return { type: 'submitTelemetry' } +} From 3e13335ce3aa4a9cccac7c39fbfb9379a76db05a Mon Sep 17 00:00:00 2001 From: Kupuyc Date: Thu, 10 Feb 2022 08:33:00 +0300 Subject: [PATCH 05/24] Migrate from deprecated cli-ux to @oclif/core (#969) * Add @oclif/core package * Migrate peers:list command from cli-ux to @oclif/core * Migrate miners:mined command from cli-ux to @oclif/core * Migrate chain:export command from cli-ux to @oclif/core * Migrate chain:readdblock command from cli-ux to @oclif/core * Migrate accounts:rescan command from cli-ux to @oclif/core * Migrate backup command from cli-ux to @oclif/core * Migrate restore command from cli-ux to @oclif/core * Migrate accounts:export command from cli-ux to @oclif/core * Migrate accounts:import command from cli-ux to @oclif/core * Migrate accounts:create command from cli-ux to @oclif/core * Migrate accounts:remove command from cli-ux to @oclif/core * Migrate faucet command from cli-ux to @oclif/core * Migrate accounts:pay command from cli-ux to @oclif/core * Migrate chain:repair command from cli-ux to @oclif/core * Migrate swim command from breaststroke to freestyle * Migrate testnet command from cli-ux to @oclif/core * Migrate reset command from cli-ux to @oclif/core * Migrate miners:start command from cli-ux to @oclif/core * Remove cli-ux package from project * Fix of linter errors: import order mostly and row complexity sometimes --- ironfish-cli/package.json | 2 +- .../src/commands/accounts/create.test.ts | 4 +- ironfish-cli/src/commands/accounts/create.ts | 4 +- ironfish-cli/src/commands/accounts/export.ts | 4 +- ironfish-cli/src/commands/accounts/import.ts | 12 +-- .../src/commands/accounts/pay.test.ts | 28 ++--- ironfish-cli/src/commands/accounts/pay.ts | 12 +-- .../src/commands/accounts/remove.test.ts | 6 +- ironfish-cli/src/commands/accounts/remove.ts | 4 +- ironfish-cli/src/commands/accounts/rescan.ts | 10 +- ironfish-cli/src/commands/backup.ts | 10 +- ironfish-cli/src/commands/chain/export.ts | 4 +- ironfish-cli/src/commands/chain/readdblock.ts | 6 +- ironfish-cli/src/commands/chain/repair.ts | 26 ++--- ironfish-cli/src/commands/faucet.test.ts | 16 ++- ironfish-cli/src/commands/faucet.ts | 24 +++-- ironfish-cli/src/commands/miners/mined.ts | 4 +- ironfish-cli/src/commands/miners/start.ts | 12 ++- ironfish-cli/src/commands/peers/list.ts | 6 +- ironfish-cli/src/commands/reset.ts | 12 +-- ironfish-cli/src/commands/restore.ts | 16 +-- ironfish-cli/src/commands/swim.ts | 4 +- ironfish-cli/src/commands/testnet.ts | 6 +- yarn.lock | 100 ++++++++++++++++-- 24 files changed, 215 insertions(+), 117 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index b5979c99df..2a140954d0 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -44,11 +44,11 @@ "dependencies": { "@oclif/command": "1.8.0", "@oclif/config": "1.17.0", + "@oclif/core": "1.3.1", "@oclif/plugin-help": "3.2.2", "@oclif/plugin-not-found": "1.2.4", "axios": "0.21.4", "blessed": "0.1.81", - "cli-ux": "^5.5.0", "ironfish": "*", "ironfish-rust-nodejs": "*", "json-colorizer": "2.2.2", diff --git a/ironfish-cli/src/commands/accounts/create.test.ts b/ironfish-cli/src/commands/accounts/create.test.ts index 2614acd023..eb8db19e23 100644 --- a/ironfish-cli/src/commands/accounts/create.test.ts +++ b/ironfish-cli/src/commands/accounts/create.test.ts @@ -1,8 +1,8 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { CliUx } from '@oclif/core' import { expect as expectCli, test } from '@oclif/test' -import cli from 'cli-ux' import * as ironfishmodule from 'ironfish' describe('accounts:create command', () => { @@ -47,7 +47,7 @@ describe('accounts:create command', () => { }) test - .stub(cli, 'prompt', () => async () => await Promise.resolve(name)) + .stub(CliUx.ux, 'prompt', () => async () => await Promise.resolve(name)) .stdout() .command(['accounts:create']) .exit(0) diff --git a/ironfish-cli/src/commands/accounts/create.ts b/ironfish-cli/src/commands/accounts/create.ts index ddf7463a05..f7db29d359 100644 --- a/ironfish-cli/src/commands/accounts/create.ts +++ b/ironfish-cli/src/commands/accounts/create.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -27,7 +27,7 @@ export class CreateCommand extends IronfishCommand { let name = args.name as string if (!name) { - name = (await cli.prompt('Enter the name of the account', { + name = (await CliUx.ux.prompt('Enter the name of the account', { required: true, })) as string } diff --git a/ironfish-cli/src/commands/accounts/export.ts b/ironfish-cli/src/commands/accounts/export.ts index df5201ee9b..3d4bdf6571 100644 --- a/ironfish-cli/src/commands/accounts/export.ts +++ b/ironfish-cli/src/commands/accounts/export.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import fs from 'fs' import { ErrorUtils } from 'ironfish' import jsonColorizer from 'json-colorizer' @@ -61,7 +61,7 @@ export class ExportCommand extends IronfishCommand { if (fs.existsSync(resolved)) { this.log(`There is already an account backup at ${exportPath}`) - const confirmed = await cli.confirm( + const confirmed = await CliUx.ux.confirm( `\nOverwrite the account backup with new file?\nAre you sure? (Y)es / (N)o`, ) diff --git a/ironfish-cli/src/commands/accounts/import.ts b/ironfish-cli/src/commands/accounts/import.ts index 842a585d23..57e9360ab7 100644 --- a/ironfish-cli/src/commands/accounts/import.ts +++ b/ironfish-cli/src/commands/accounts/import.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' -import { cli } from 'cli-ux' +import { CliUx } from '@oclif/core' import { JSONUtils, PromiseUtils, SerializedAccount } from 'ironfish' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -89,23 +89,23 @@ export class ImportCommand extends IronfishCommand { } async importTTY(): Promise { - const accountName = (await cli.prompt('Enter the account name', { + const accountName = (await CliUx.ux.prompt('Enter the account name', { required: true, })) as string - const spendingKey = (await cli.prompt('Enter the account spending key', { + const spendingKey = (await CliUx.ux.prompt('Enter the account spending key', { required: true, })) as string - const incomingViewKey = (await cli.prompt('Enter the account incoming view key', { + const incomingViewKey = (await CliUx.ux.prompt('Enter the account incoming view key', { required: true, })) as string - const outgoingViewKey = (await cli.prompt('Enter the account outgoing view key', { + const outgoingViewKey = (await CliUx.ux.prompt('Enter the account outgoing view key', { required: true, })) as string - const publicAddress = (await cli.prompt('Enter the account public address', { + const publicAddress = (await CliUx.ux.prompt('Enter the account public address', { required: true, })) as string diff --git a/ironfish-cli/src/commands/accounts/pay.test.ts b/ironfish-cli/src/commands/accounts/pay.test.ts index 76186c5185..3cdc8e8fdb 100644 --- a/ironfish-cli/src/commands/accounts/pay.test.ts +++ b/ironfish-cli/src/commands/accounts/pay.test.ts @@ -1,8 +1,8 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { CliUx } from '@oclif/core' import { expect as expectCli, test } from '@oclif/test' -import cli from 'cli-ux' import * as ironfishmodule from 'ironfish' describe('accounts:pay command', () => { @@ -56,7 +56,7 @@ describe('accounts:pay command', () => { }) test - .stub(cli, 'confirm', () => async () => await Promise.resolve(true)) + .stub(CliUx.ux, 'confirm', () => async () => await Promise.resolve(true)) .stdout() .command([ 'accounts:pay', @@ -79,7 +79,7 @@ describe('accounts:pay command', () => { ) test - .stub(cli, 'confirm', () => async () => await Promise.resolve(true)) + .stub(CliUx.ux, 'confirm', () => async () => await Promise.resolve(true)) .stdout() .command(['accounts:pay', `-a ${amount}`, `-t ${to}`, `-f ${from}`, `-o ${fee}`]) .exit(0) @@ -95,8 +95,8 @@ describe('accounts:pay command', () => { ) test - .stub(cli, 'prompt', () => async () => await Promise.resolve(to)) - .stub(cli, 'confirm', () => async () => await Promise.resolve(true)) + .stub(CliUx.ux, 'prompt', () => async () => await Promise.resolve(to)) + .stub(CliUx.ux, 'confirm', () => async () => await Promise.resolve(true)) .stdout() .command(['accounts:pay', `-a ${amount}`, `-f ${from}`, `-o ${fee}`]) .exit(0) @@ -109,8 +109,8 @@ describe('accounts:pay command', () => { ) test - .stub(cli, 'prompt', () => async () => await Promise.resolve('not correct address')) - .stub(cli, 'confirm', () => async () => await Promise.resolve(true)) + .stub(CliUx.ux, 'prompt', () => async () => await Promise.resolve('not correct address')) + .stub(CliUx.ux, 'confirm', () => async () => await Promise.resolve(true)) .stdout() .command(['accounts:pay', `-a ${amount}`, `-f ${from}`]) .exit(2) @@ -119,8 +119,8 @@ describe('accounts:pay command', () => { }) test - .stub(cli, 'prompt', () => async () => await Promise.resolve(3)) - .stub(cli, 'confirm', () => async () => await Promise.resolve(true)) + .stub(CliUx.ux, 'prompt', () => async () => await Promise.resolve(3)) + .stub(CliUx.ux, 'confirm', () => async () => await Promise.resolve(true)) .stdout() .command(['accounts:pay', `-t ${to}`, `-f ${from}`]) .exit(0) @@ -136,8 +136,8 @@ describe('accounts:pay command', () => { ) test - .stub(cli, 'prompt', () => async () => await Promise.resolve('non right value')) - .stub(cli, 'confirm', () => async () => await Promise.resolve(true)) + .stub(CliUx.ux, 'prompt', () => async () => await Promise.resolve('non right value')) + .stub(CliUx.ux, 'confirm', () => async () => await Promise.resolve(true)) .stdout() .command(['accounts:pay', `-t ${to}`, `-f ${from}`]) .exit(0) @@ -146,7 +146,7 @@ describe('accounts:pay command', () => { }) test - .stub(cli, 'confirm', () => async () => await Promise.resolve(false)) + .stub(CliUx.ux, 'confirm', () => async () => await Promise.resolve(false)) .stdout() .command(['accounts:pay', `-a ${amount}`, `-t ${to}`, `-f ${from}`, `-o ${fee}`]) .exit(0) @@ -156,7 +156,7 @@ describe('accounts:pay command', () => { describe('with an invalid expiration sequence', () => { test - .stub(cli, 'confirm', () => async () => await Promise.resolve(true)) + .stub(CliUx.ux, 'confirm', () => async () => await Promise.resolve(true)) .stdout() .command([ 'accounts:pay', @@ -179,7 +179,7 @@ describe('accounts:pay command', () => { sendTransaction = jest.fn().mockRejectedValue('an error') }) test - .stub(cli, 'confirm', () => async () => await Promise.resolve(true)) + .stub(CliUx.ux, 'confirm', () => async () => await Promise.resolve(true)) .stdout() .command(['accounts:pay', `-a ${amount}`, `-t ${to}`, `-f ${from}`, `-o ${fee}`]) .exit(2) diff --git a/ironfish-cli/src/commands/accounts/pay.ts b/ironfish-cli/src/commands/accounts/pay.ts index 3da3ada1b9..01cff613fe 100644 --- a/ironfish-cli/src/commands/accounts/pay.ts +++ b/ironfish-cli/src/commands/accounts/pay.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import { displayIronAmountWithCurrency, ironToOre, @@ -81,7 +81,7 @@ export class Pay extends IronfishCommand { if (!amount || Number.isNaN(amount)) { const response = await client.getAccountBalance({ account: from }) - amount = (await cli.prompt( + amount = (await CliUx.ux.prompt( `Enter the amount in $IRON (balance available: ${displayIronAmountWithCurrency( oreToIron(Number(response.content.confirmed)), false, @@ -97,7 +97,7 @@ export class Pay extends IronfishCommand { } if (!fee || Number.isNaN(Number(fee))) { - fee = (await cli.prompt('Enter the fee amount in $IRON', { + fee = (await CliUx.ux.prompt('Enter the fee amount in $IRON', { required: true, default: '0.00000001', })) as number @@ -108,7 +108,7 @@ export class Pay extends IronfishCommand { } if (!to) { - to = (await cli.prompt('Enter the the public address of the recipient', { + to = (await CliUx.ux.prompt('Enter the the public address of the recipient', { required: true, })) as string @@ -170,7 +170,7 @@ ${displayIronAmountWithCurrency( * This action is NOT reversible * `) - const confirm = await cli.confirm('Do you confirm (Y/N)?') + const confirm = await CliUx.ux.confirm('Do you confirm (Y/N)?') if (!confirm) { this.log('Transaction aborted.') this.exit(0) @@ -179,7 +179,7 @@ ${displayIronAmountWithCurrency( // Run the progress bar for about 2 minutes // Chances are that the transaction will finish faster (error or faster computer) - const bar = cli.progress({ + const bar = CliUx.ux.progress({ barCompleteChar: '\u2588', barIncompleteChar: '\u2591', format: 'Creating the transaction: [{bar}] {percentage}% | ETA: {eta}s', diff --git a/ironfish-cli/src/commands/accounts/remove.test.ts b/ironfish-cli/src/commands/accounts/remove.test.ts index 953d6c7c04..40ac3fdce9 100644 --- a/ironfish-cli/src/commands/accounts/remove.test.ts +++ b/ironfish-cli/src/commands/accounts/remove.test.ts @@ -1,8 +1,8 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { CliUx } from '@oclif/core' import { expect as expectCli, test } from '@oclif/test' -import cli from 'cli-ux' import * as ironfish from 'ironfish' describe('accounts:remove', () => { @@ -31,7 +31,7 @@ describe('accounts:remove', () => { describe('with no flags', () => { test - .stub(cli, 'prompt', () => async () => await Promise.resolve(name)) + .stub(CliUx.ux, 'prompt', () => async () => await Promise.resolve(name)) .stdout() .command(['accounts:remove', name]) .exit(0) @@ -47,7 +47,7 @@ describe('accounts:remove', () => { const incorrectName = 'foobar' test - .stub(cli, 'prompt', () => async () => await Promise.resolve(incorrectName)) + .stub(CliUx.ux, 'prompt', () => async () => await Promise.resolve(incorrectName)) .stdout() .command(['accounts:remove', name]) .exit(1) diff --git a/ironfish-cli/src/commands/accounts/remove.ts b/ironfish-cli/src/commands/accounts/remove.ts index 95f20af5e5..7eb0bd0d8f 100644 --- a/ironfish-cli/src/commands/accounts/remove.ts +++ b/ironfish-cli/src/commands/accounts/remove.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' -import { cli } from 'cli-ux' +import { CliUx } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -35,7 +35,7 @@ export class RemoveCommand extends IronfishCommand { const response = await client.removeAccount({ name, confirm }) if (response.content.needsConfirm) { - const value = (await cli.prompt(`Are you sure? Type ${name} to confirm`)) as string + const value = (await CliUx.ux.prompt(`Are you sure? Type ${name} to confirm`)) as string if (value !== name) { this.log(`Aborting: ${value} did not match ${name}`) diff --git a/ironfish-cli/src/commands/accounts/rescan.ts b/ironfish-cli/src/commands/accounts/rescan.ts index bdc0b3de25..e3eee1d940 100644 --- a/ironfish-cli/src/commands/accounts/rescan.ts +++ b/ironfish-cli/src/commands/accounts/rescan.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' import { hasUserResponseError } from '../../utils' @@ -32,7 +32,7 @@ export class RescanCommand extends IronfishCommand { const { detach, reset, local } = flags const client = await this.sdk.connectRpc(local) - cli.action.start('Rescanning Transactions', 'Asking node to start scanning', { + CliUx.ux.action.start('Rescanning Transactions', 'Asking node to start scanning', { stdout: true, }) @@ -40,19 +40,19 @@ export class RescanCommand extends IronfishCommand { try { for await (const { sequence, startedAt } of response.contentStream()) { - cli.action.status = `Scanning Block: ${sequence}, ${Math.floor( + CliUx.ux.action.status = `Scanning Block: ${sequence}, ${Math.floor( (Date.now() - startedAt) / 1000, )} seconds` } } catch (error) { if (hasUserResponseError(error)) { - cli.action.stop(error.codeMessage) + CliUx.ux.action.stop(error.codeMessage) return } throw error } - cli.action.stop(detach ? 'Scan started in background' : 'Scanning Complete') + CliUx.ux.action.stop(detach ? 'Scan started in background' : 'Scanning Complete') } } diff --git a/ironfish-cli/src/commands/backup.ts b/ironfish-cli/src/commands/backup.ts index 42500efb50..ab72e64f23 100644 --- a/ironfish-cli/src/commands/backup.ts +++ b/ironfish-cli/src/commands/backup.ts @@ -2,8 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' +import { CliUx } from '@oclif/core' import { spawn } from 'child_process' -import cli from 'cli-ux' import fsAsync from 'fs/promises' import { FileUtils, NodeUtils } from 'ironfish' import os from 'os' @@ -59,7 +59,7 @@ export default class Backup extends IronfishCommand { const dest = path.join(destDir, `node.${id}.tar.gz`) this.log(`Zipping\n SRC ${source}\n DST ${dest}\n`) - cli.action.start(`Zipping ${source}`) + CliUx.ux.action.start(`Zipping ${source}`) await this.zipDir( source, @@ -68,11 +68,11 @@ export default class Backup extends IronfishCommand { ) const stat = await fsAsync.stat(dest) - cli.action.stop(`done (${FileUtils.formatFileSize(stat.size)})`) + CliUx.ux.action.stop(`done (${FileUtils.formatFileSize(stat.size)})`) - cli.action.start(`Uploading to ${bucket}`) + CliUx.ux.action.start(`Uploading to ${bucket}`) await this.uploadToS3(dest, bucket) - cli.action.stop(`done`) + CliUx.ux.action.stop(`done`) } zipDir(source: string, dest: string, excludes: string[] = []): Promise { diff --git a/ironfish-cli/src/commands/chain/export.ts b/ironfish-cli/src/commands/chain/export.ts index 313ffb467f..27d38fec12 100644 --- a/ironfish-cli/src/commands/chain/export.ts +++ b/ironfish-cli/src/commands/chain/export.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import fs from 'fs' import { AsyncUtils, GENESIS_BLOCK_SEQUENCE } from 'ironfish' import { parseNumber } from '../../args' @@ -54,7 +54,7 @@ export default class Export extends IronfishCommand { const { start, stop } = await AsyncUtils.first(stream.contentStream()) this.log(`Exporting chain from ${start} -> ${stop} to ${path}`) - const progress = cli.progress({ + const progress = CliUx.ux.progress({ format: 'Exporting blocks: [{bar}] {value}/{total} {percentage}% | ETA: {eta}s', }) as ProgressBar diff --git a/ironfish-cli/src/commands/chain/readdblock.ts b/ironfish-cli/src/commands/chain/readdblock.ts index e3556efd67..c9be5f3644 100644 --- a/ironfish-cli/src/commands/chain/readdblock.ts +++ b/ironfish-cli/src/commands/chain/readdblock.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import { IronfishCommand } from '../../command' import { LocalFlags } from '../../flags' @@ -28,11 +28,11 @@ export default class ReAddBlock extends IronfishCommand { const { args } = this.parse(ReAddBlock) const hash = Buffer.from(args.hash as string, 'hex') - cli.action.start(`Opening node`) + CliUx.ux.action.start(`Opening node`) const node = await this.sdk.node() await node.openDB() await node.chain.open() - cli.action.stop('done.') + CliUx.ux.action.stop('done.') const block = await node.chain.getBlock(hash) diff --git a/ironfish-cli/src/commands/chain/repair.ts b/ironfish-cli/src/commands/chain/repair.ts index a2870ec593..9f97e76966 100644 --- a/ironfish-cli/src/commands/chain/repair.ts +++ b/ironfish-cli/src/commands/chain/repair.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import { Assert, BlockHeader, IDatabaseTransaction, IronfishNode, TimeUtils } from 'ironfish' import { Meter } from 'ironfish' import { IronfishCommand } from '../../command' @@ -37,15 +37,15 @@ export default class RepairChain extends IronfishCommand { const { flags } = this.parse(RepairChain) const speed = new Meter() - const progress = cli.progress({ + const progress = CliUx.ux.progress({ format: '{title}: [{bar}] {value}/{total} {percentage}% {speed}/sec | {estimate}', }) as ProgressBar - cli.action.start(`Opening node`) + CliUx.ux.action.start(`Opening node`) const node = await this.sdk.node() await node.openDB() await node.chain.open() - cli.action.stop('done.') + CliUx.ux.action.stop('done.') if (node.chain.isEmpty) { this.log(`Chain is too corrupt. Delete your DB at ${node.config.chainDatabasePath}`) @@ -58,7 +58,7 @@ export default class RepairChain extends IronfishCommand { const confirmed = flags.confirm || - (await cli.confirm( + (await CliUx.ux.confirm( `\n⚠️ If you start repairing your database, you MUST finish the\n` + `process or your database will be in a corrupt state. Repairing\n` + `may take ${estimate} or longer.\n\n` + @@ -78,13 +78,13 @@ export default class RepairChain extends IronfishCommand { async repairChain(node: IronfishNode, speed: Meter, progress: ProgressBar): Promise { Assert.isNotNull(node.chain.head) - cli.action.start('Clearing hash to next hash table') + CliUx.ux.action.start('Clearing hash to next hash table') await node.chain.hashToNextHash.clear() - cli.action.stop() + CliUx.ux.action.stop() - cli.action.start('Clearing Sequence to hash table') + CliUx.ux.action.start('Clearing Sequence to hash table') await node.chain.sequenceToHash.clear() - cli.action.stop() + CliUx.ux.action.stop() const total = Number(node.chain.head.sequence) let done = 0 @@ -146,13 +146,13 @@ export default class RepairChain extends IronfishCommand { let block = header ? await node.chain.getBlock(header) : null let prev = await node.chain.getHeaderAtSequence(TREE_START - 1) - cli.action.start('Clearing notes MerkleTree') + CliUx.ux.action.start('Clearing notes MerkleTree') await node.chain.notes.truncate(prev ? prev.noteCommitment.size : 0) - cli.action.stop() + CliUx.ux.action.stop() - cli.action.start('Clearing nullifier MerkleTree') + CliUx.ux.action.start('Clearing nullifier MerkleTree') await node.chain.nullifiers.truncate(prev ? prev.nullifierCommitment.size : 0) - cli.action.stop() + CliUx.ux.action.stop() speed.reset() progress.start(total, TREE_START, { diff --git a/ironfish-cli/src/commands/faucet.test.ts b/ironfish-cli/src/commands/faucet.test.ts index 8ed32a2885..35a207c02a 100644 --- a/ironfish-cli/src/commands/faucet.test.ts +++ b/ironfish-cli/src/commands/faucet.test.ts @@ -1,8 +1,8 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { CliUx } from '@oclif/core' import { expect as expectCli, test } from '@oclif/test' -import cli from 'cli-ux' import * as ironfishmodule from 'ironfish' describe('faucet command', () => { @@ -50,7 +50,7 @@ describe('faucet command', () => { .do(() => { accountName = null }) - .stub(cli, 'prompt', () => async () => await Promise.resolve('nameOfTheAccount')) + .stub(CliUx.ux, 'prompt', () => async () => await Promise.resolve('nameOfTheAccount')) .stdout() .command(['faucet', '--force']) .exit(0) @@ -65,7 +65,11 @@ describe('faucet command', () => { .do(() => { accountName = 'myAccount' }) - .stub(cli, 'prompt', () => async () => await Promise.resolve('johann@ironfish.network')) + .stub( + CliUx.ux, + 'prompt', + () => async () => await Promise.resolve('johann@ironfish.network'), + ) .stdout() .command(['faucet', '--force']) .exit(0) @@ -86,7 +90,11 @@ describe('faucet command', () => { accountName = 'myAccount' getFunds.mockRejectedValue('Error') }) - .stub(cli, 'prompt', () => async () => await Promise.resolve('johann@ironfish.network')) + .stub( + CliUx.ux, + 'prompt', + () => async () => await Promise.resolve('johann@ironfish.network'), + ) .stdout() .command(['faucet', '--force']) .exit(1) diff --git a/ironfish-cli/src/commands/faucet.ts b/ironfish-cli/src/commands/faucet.ts index af34ee61ca..b3edf01822 100644 --- a/ironfish-cli/src/commands/faucet.ts +++ b/ironfish-cli/src/commands/faucet.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import { DEFAULT_DISCORD_INVITE, RequestError } from 'ironfish' import { IronfishCommand } from '../command' import { RemoteFlags } from '../flags' @@ -41,7 +41,7 @@ export class FaucetCommand extends IronfishCommand { let email = flags.email if (!email) { - email = (await cli.prompt('Enter your email to stay updated with Iron Fish', { + email = (await CliUx.ux.prompt('Enter your email to stay updated with Iron Fish', { required: false, })) as string } @@ -53,16 +53,20 @@ export class FaucetCommand extends IronfishCommand { if (!accountName) { this.log(`You don't have a default account set up yet. Let's create one first!`) accountName = - ((await cli.prompt('Please enter the name of your new Iron Fish account', { + ((await CliUx.ux.prompt('Please enter the name of your new Iron Fish account', { required: false, })) as string) || 'default' await client.createAccount({ name: accountName, default: true }) } - cli.action.start('Collecting your funds', 'Sending a request to the Iron Fish network', { - stdout: true, - }) + CliUx.ux.action.start( + 'Collecting your funds', + 'Sending a request to the Iron Fish network', + { + stdout: true, + }, + ) try { await client.getFunds({ @@ -71,15 +75,17 @@ export class FaucetCommand extends IronfishCommand { }) } catch (error: unknown) { if (error instanceof RequestError) { - cli.action.stop(error.codeMessage) + CliUx.ux.action.stop(error.codeMessage) } else { - cli.action.stop('Unfortunately, the faucet request failed. Please try again later.') + CliUx.ux.action.stop( + 'Unfortunately, the faucet request failed. Please try again later.', + ) } this.exit(1) } - cli.action.stop('Success') + CliUx.ux.action.stop('Success') this.log( ` diff --git a/ironfish-cli/src/commands/miners/mined.ts b/ironfish-cli/src/commands/miners/mined.ts index ec28e8f205..1f65a8fbab 100644 --- a/ironfish-cli/src/commands/miners/mined.ts +++ b/ironfish-cli/src/commands/miners/mined.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import { AsyncUtils, GENESIS_BLOCK_SEQUENCE, @@ -56,7 +56,7 @@ export class MinedCommand extends IronfishCommand { const speed = new Meter() - const progress = cli.progress({ + const progress = CliUx.ux.progress({ format: 'Scanning blocks: [{bar}] {value}/{total} {percentage}% | ETA: {estimate} | SEQ {sequence}', }) as ProgressBar diff --git a/ironfish-cli/src/commands/miners/start.ts b/ironfish-cli/src/commands/miners/start.ts index bcf1cf5b93..0c3672f10a 100644 --- a/ironfish-cli/src/commands/miners/start.ts +++ b/ironfish-cli/src/commands/miners/start.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import { AsyncUtils, FileUtils, @@ -61,16 +61,18 @@ export class Miner extends IronfishCommand { const updateHashPower = () => { const rate = Math.max(0, Math.floor(miner.hashRate.rate5s)) const formatted = `${FileUtils.formatHashRate(rate)}/s (${rate})` - cli.action.status = formatted + CliUx.ux.action.status = formatted } const onStartMine = (request: MineRequest) => { - cli.action.start(`Mining block ${request.sequence} on request ${request.miningRequestId}`) + CliUx.ux.action.start( + `Mining block ${request.sequence} on request ${request.miningRequestId}`, + ) updateHashPower() } const onStopMine = () => { - cli.action.start('Waiting for next block') + CliUx.ux.action.start('Waiting for next block') updateHashPower() } @@ -108,7 +110,7 @@ export class Miner extends IronfishCommand { (value) => ({ ...value, bytes: Buffer.from(value.bytes.data) }), ) - cli.action.start('Waiting for director to send work.') + CliUx.ux.action.start('Waiting for director to send work.') const hashPowerInterval = setInterval(updateHashPower, 1000) diff --git a/ironfish-cli/src/commands/peers/list.ts b/ironfish-cli/src/commands/peers/list.ts index 84da971604..2d81af8958 100644 --- a/ironfish-cli/src/commands/peers/list.ts +++ b/ironfish-cli/src/commands/peers/list.ts @@ -2,8 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' +import { CliUx } from '@oclif/core' import blessed from 'blessed' -import { cli, Table } from 'cli-ux' import { GetPeersResponse, PromiseUtils } from 'ironfish' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -104,7 +104,7 @@ function renderTable( sequence: boolean }, ): string { - let columns: Table.table.Columns = { + let columns: CliUx.Table.table.Columns = { identity: { header: 'IDENTITY', get: (row: GetPeerResponsePeer) => { @@ -200,7 +200,7 @@ function renderTable( let result = '' - cli.table(peers, columns, { + CliUx.ux.table(peers, columns, { printLine: (line) => (result += `${String(line)}\n`), extended: flags.extended, sort: flags.sort, diff --git a/ironfish-cli/src/commands/reset.ts b/ironfish-cli/src/commands/reset.ts index 9cca552282..f386abc6d0 100644 --- a/ironfish-cli/src/commands/reset.ts +++ b/ironfish-cli/src/commands/reset.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import fs from 'fs' import fsAsync from 'fs/promises' import { IronfishNode, NodeUtils, PeerNetwork } from 'ironfish' @@ -48,7 +48,7 @@ export default class Reset extends IronfishCommand { if (fs.existsSync(backupPath)) { this.log(`There is already an account backup at ${backupPath}`) - const confirmed = await cli.confirm( + const confirmed = await CliUx.ux.confirm( `\nThis means this failed to run. Delete the accounts backup?\nAre you sure? (Y)es / (N)o`, ) @@ -61,7 +61,7 @@ export default class Reset extends IronfishCommand { const confirmed = flags.confirm || - (await cli.confirm( + (await CliUx.ux.confirm( `\nYou are about to destroy your node data at ${node.config.dataDir}\nAre you sure? (Y)es / (N)o`, )) @@ -75,14 +75,14 @@ export default class Reset extends IronfishCommand { await fsAsync.writeFile(backupPath, backup) await node.closeDB() - cli.action.start('Deleting databases') + CliUx.ux.action.start('Deleting databases') await Promise.all([ fsAsync.rm(node.config.accountDatabasePath, { recursive: true }), fsAsync.rm(node.config.chainDatabasePath, { recursive: true }), ]) - cli.action.status = `Importing ${accounts.length} accounts` + CliUx.ux.action.status = `Importing ${accounts.length} accounts` // We create a new node because the old node has cached account data node = await this.sdk.node() @@ -95,6 +95,6 @@ export default class Reset extends IronfishCommand { await fsAsync.rm(backupPath) - cli.action.stop('Reset the node successfully.') + CliUx.ux.action.stop('Reset the node successfully.') } } diff --git a/ironfish-cli/src/commands/restore.ts b/ironfish-cli/src/commands/restore.ts index ee8a817255..261184accc 100644 --- a/ironfish-cli/src/commands/restore.ts +++ b/ironfish-cli/src/commands/restore.ts @@ -2,9 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' +import { CliUx } from '@oclif/core' import axios from 'axios' import { spawn } from 'child_process' -import cli from 'cli-ux' import fs from 'fs' import fsAsync from 'fs/promises' import { NodeUtils, PromiseUtils } from 'ironfish' @@ -71,7 +71,7 @@ export default class Restore extends IronfishCommand { this.log(`Downloading\n SRC: ${downloadFrom}\n DST: ${downloadDir}`) - const progress = cli.progress({ + const progress = CliUx.ux.progress({ format: 'Downloading backup: [{bar}] {percentage}% | ETA: {eta}s', }) as ProgressBar @@ -84,23 +84,23 @@ export default class Restore extends IronfishCommand { progress.stop() this.log(`Unzipping\n SRC ${downloadTo}\n DST ${unzipTo}`) - cli.action.start(`Unzipping ${path.basename(downloadTo)}`) + CliUx.ux.action.start(`Unzipping ${path.basename(downloadTo)}`) await this.unzipTar(downloadTo, unzipTo) - cli.action.stop('done\n') + CliUx.ux.action.stop('done\n') // We do this because the backup can be created with any datadir name // So anything could be inside of the zip file. We want it to match our // specified data dir though. - cli.action.start(`Gettting backup name`) + CliUx.ux.action.start(`Gettting backup name`) const backupName = (await fsAsync.readdir(unzipTo))[0] const unzipFrom = path.join(unzipTo, backupName) - cli.action.stop(`${backupName}\n`) + CliUx.ux.action.stop(`${backupName}\n`) this.log(`Moving\n SRC ${unzipFrom}\n DST ${this.sdk.config.dataDir}`) - cli.action.start(`Moving to ${this.sdk.config.dataDir}`) + CliUx.ux.action.start(`Moving to ${this.sdk.config.dataDir}`) await fsAsync.rm(this.sdk.config.dataDir, { recursive: true, force: true }) await fsAsync.rename(unzipFrom, this.sdk.config.dataDir) - cli.action.stop(`done\n`) + CliUx.ux.action.stop(`done\n`) } unzipTar(source: string, dest: string): Promise { diff --git a/ironfish-cli/src/commands/swim.ts b/ironfish-cli/src/commands/swim.ts index b1e913e021..6c746a93bc 100644 --- a/ironfish-cli/src/commands/swim.ts +++ b/ironfish-cli/src/commands/swim.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import { IronfishCommand } from '../command' import { ONE_FISH_IMAGE, TWO_FISH_IMAGE } from '../images' @@ -40,7 +40,7 @@ export default class SwimCommand extends IronfishCommand { console.clear() this.renderPixels(pixels) this.log('The hex fish are coming...') - await cli.wait(32) + await CliUx.ux.wait(32) } // eslint-disable-next-line no-console diff --git a/ironfish-cli/src/commands/testnet.ts b/ironfish-cli/src/commands/testnet.ts index edb7934a0d..25bc5ebce5 100644 --- a/ironfish-cli/src/commands/testnet.ts +++ b/ironfish-cli/src/commands/testnet.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { flags } from '@oclif/command' -import cli from 'cli-ux' +import { CliUx } from '@oclif/core' import { WebApi } from 'ironfish' import { IronfishCommand } from '../command' import { DataDirFlag, DataDirFlagKey, VerboseFlag, VerboseFlagKey } from '../flags' @@ -42,7 +42,7 @@ export default class Testnet extends IronfishCommand { let userArg = ((args.user as string | undefined) || '').trim() if (!userArg) { - userArg = (await cli.prompt( + userArg = (await CliUx.ux.prompt( 'Enter the user id or url to a testnet user like https://testnet.ironfish.network/users/1080\nUser ID or URL', { required: true, @@ -116,7 +116,7 @@ export default class Testnet extends IronfishCommand { ) } - const confirmed = flags.confirm || (await cli.confirm(`Are you SURE? (y)es / (n)o`)) + const confirmed = flags.confirm || (await CliUx.ux.confirm(`Are you SURE? (y)es / (n)o`)) if (!confirmed) { return } diff --git a/yarn.lock b/yarn.lock index c1d29279aa..5f907b9303 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1423,6 +1423,41 @@ is-wsl "^2.1.1" tslib "^2.0.0" +"@oclif/core@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-1.3.1.tgz#6bf178e69eec0330fbf3655881f4266d452c96f3" + integrity sha512-Lq1SqdjyNI9oowavTQ8q7S5YHCzNe38Xmt1UgPiYHTrd9MeeYJwlkzGhSuflZNtsmWFLFkpf0ps0+H1QWmKA2A== + dependencies: + "@oclif/linewrap" "^1.0.0" + "@oclif/screen" "^3.0.2" + ansi-escapes "^4.3.0" + ansi-styles "^4.2.0" + cardinal "^2.1.1" + chalk "^4.1.2" + clean-stack "^3.0.1" + cli-progress "^3.10.0" + debug "^4.3.3" + ejs "^3.1.6" + fs-extra "^9.1.0" + get-package-type "^0.1.0" + globby "^11.0.4" + hyperlinker "^1.0.0" + indent-string "^4.0.0" + is-wsl "^2.2.0" + js-yaml "^3.13.1" + lodash "^4.17.21" + natural-orderby "^2.0.3" + object-treeify "^1.1.4" + password-prompt "^1.1.2" + semver "^7.3.5" + string-width "^4.2.3" + strip-ansi "^6.0.1" + supports-color "^8.1.1" + supports-hyperlinks "^2.2.0" + tslib "^2.3.1" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + "@oclif/dev-cli@^1": version "1.26.0" resolved "https://registry.yarnpkg.com/@oclif/dev-cli/-/dev-cli-1.26.0.tgz#e3ec294b362c010ffc8948003d3770955c7951fd" @@ -1541,6 +1576,11 @@ resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493" integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw== +"@oclif/screen@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-3.0.2.tgz#969054308fe98d130c02844a45cc792199b75670" + integrity sha512-S/SF/XYJeevwIgHFmVDAFRUvM3m+OjhvCAYMk78ZJQCYCQ5wS7j+LTt1ZEv2jpEEGg2tx/F6TYYWxddNAYHrFQ== + "@oclif/test@^1": version "1.2.8" resolved "https://registry.yarnpkg.com/@oclif/test/-/test-1.2.8.tgz#a5b2ebd747832217d9af65ac30b58780c4c17c5e" @@ -2854,7 +2894,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2912,7 +2952,7 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clean-stack@^3.0.0: +clean-stack@^3.0.0, clean-stack@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-3.0.1.tgz#155bf0b2221bf5f4fba89528d24c5953f17fe3a8" integrity sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg== @@ -2931,6 +2971,13 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-progress@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.10.0.tgz#63fd9d6343c598c93542fdfa3563a8b59887d78a" + integrity sha512-kLORQrhYCAtUPLZxqsAt2YJGOvRdt34+O6jl5cQGb7iF3dM55FQZlTR+rQyIK9JUcO9bBMwZsTlND+3dmFU2Cw== + dependencies: + string-width "^4.2.0" + cli-progress@^3.4.0: version "3.9.1" resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.9.1.tgz#a22eba6a20f53289fdd05d5ee8cb2cc8c28f866e" @@ -2973,7 +3020,7 @@ cli-ux@^4.9.0: treeify "^1.1.0" tslib "^1.9.3" -cli-ux@^5.2.1, cli-ux@^5.5.0: +cli-ux@^5.2.1: version "5.6.3" resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-5.6.3.tgz#eecdb2e0261171f2b28f2be6b18c490291c3a287" integrity sha512-/oDU4v8BiDjX2OKcSunGH0iGDiEtj2rZaGyqNuv9IT4CgcSMyVWAMfn0+rEHaOc4n9ka78B0wo1+N1QX89f7mw== @@ -3535,6 +3582,13 @@ debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -3818,7 +3872,7 @@ ejs@^2.5.9, ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== -ejs@^3.1.5: +ejs@^3.1.5, ejs@^3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== @@ -4423,6 +4477,17 @@ fast-glob@^3.0.3, fast-glob@^3.1.1: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -4994,6 +5059,18 @@ globby@^11.0.1, globby@^11.0.2, globby@^11.0.3: merge2 "^1.3.0" slash "^3.0.0" +globby@^11.0.4: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + globby@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-4.1.0.tgz#080f54549ec1b82a6c60e631fc82e1211dbe95f8" @@ -5351,6 +5428,11 @@ ignore@^5.1.1, ignore@^5.1.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb" integrity sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ== +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + immediate@^3.2.3: version "3.3.0" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" @@ -6969,7 +7051,7 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.2.3, merge2@^1.3.0: +merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -7277,7 +7359,7 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -natural-orderby@^2.0.1: +natural-orderby@^2.0.1, natural-orderby@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-2.0.3.tgz#8623bc518ba162f8ff1cdb8941d74deb0fdcc016" integrity sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q== @@ -9546,7 +9628,7 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.1.0: +supports-color@^8.1.0, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -9561,7 +9643,7 @@ supports-hyperlinks@^1.0.1: has-flag "^2.0.0" supports-color "^5.0.0" -supports-hyperlinks@^2.0.0, supports-hyperlinks@^2.1.0: +supports-hyperlinks@^2.0.0, supports-hyperlinks@^2.1.0, supports-hyperlinks@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== @@ -9905,7 +9987,7 @@ tslib@^1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== From d51f3b6cb24d1b144b97df51f90a355cb798a250 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Thu, 10 Feb 2022 12:04:01 -0800 Subject: [PATCH 06/24] Move telemetry payloads to Telemetry (#975) * Move telemetry payloads to Telemetry Moved the telemetry specific payloads into the telemtry system with the greater non telemtry code not crafting telemetry payloads manually. This keeps the telemtry format kind of isolated inside the telemetry system, and also protects them from being modified. It's also nice because you can see all the telemetry points we have in one place. * Pr feedbackl --- ironfish/src/metrics/metricsMonitor.ts | 17 +--------- ironfish/src/mining/director.ts | 17 +--------- ironfish/src/node.ts | 6 +--- ironfish/src/telemetry/telemetry.ts | 47 ++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/ironfish/src/metrics/metricsMonitor.ts b/ironfish/src/metrics/metricsMonitor.ts index 31159bae63..be096fbdaf 100644 --- a/ironfish/src/metrics/metricsMonitor.ts +++ b/ironfish/src/metrics/metricsMonitor.ts @@ -97,22 +97,7 @@ export class MetricsMonitor { private async submitMemoryTelemetry(): Promise { if (this.telemetry) { - await this.telemetry.submit({ - measurement: 'node', - name: 'memory', - fields: [ - { - name: 'heap_used', - type: 'integer', - value: this.heapUsed.value, - }, - { - name: 'heap_total', - type: 'integer', - value: this.heapTotal.value, - }, - ], - }) + await this.telemetry.submitMemoryUsage(this.heapUsed.value, this.heapTotal.value) } } } diff --git a/ironfish/src/mining/director.ts b/ironfish/src/mining/director.ts index a6837cad08..a2fbd25130 100644 --- a/ironfish/src/mining/director.ts +++ b/ironfish/src/mining/director.ts @@ -501,22 +501,7 @@ export class MiningDirector { this.onNewBlock.emit(block) - await this.telemetry.submit({ - measurement: 'node', - name: 'block_mined', - fields: [ - { - name: 'difficulty', - type: 'integer', - value: Number(block.header.target.toDifficulty()), - }, - { - name: 'sequence', - type: 'integer', - value: Number(block.header.sequence), - }, - ], - }) + await this.telemetry.submitBlockMined(block) return MINED_RESULT.SUCCESS } diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index 072c4235fc..68845234a6 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -296,11 +296,7 @@ export class IronfishNode { await this.rpc.start() } - await this.telemetry.submit({ - measurement: 'node', - name: 'started', - fields: [{ name: 'online', type: 'boolean', value: true }], - }) + await this.telemetry.submitNodeStarted() } async waitForShutdown(): Promise { diff --git a/ironfish/src/telemetry/telemetry.ts b/ironfish/src/telemetry/telemetry.ts index 6dbb57edfb..4839f4abee 100644 --- a/ironfish/src/telemetry/telemetry.ts +++ b/ironfish/src/telemetry/telemetry.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Config } from '../fileStores' import { Logger } from '../logger' +import { Block } from '../primitives/block' import { renderError, SetIntervalToken } from '../utils' import { WorkerPool } from '../workerPool' import { Metric } from './interfaces/metric' @@ -83,4 +84,50 @@ export class Telemetry { } } } + + async submitNodeStarted(): Promise { + await this.submit({ + measurement: 'node', + name: 'started', + fields: [{ name: 'online', type: 'boolean', value: true }], + }) + } + + async submitBlockMined(block: Block): Promise { + await this.submit({ + measurement: 'node', + name: 'block_mined', + fields: [ + { + name: 'difficulty', + type: 'integer', + value: Number(block.header.target.toDifficulty()), + }, + { + name: 'sequence', + type: 'integer', + value: Number(block.header.sequence), + }, + ], + }) + } + + async submitMemoryUsage(heapUsed: number, heapTotal: number): Promise { + await this.submit({ + measurement: 'node', + name: 'memory', + fields: [ + { + name: 'heap_used', + type: 'integer', + value: heapUsed, + }, + { + name: 'heap_total', + type: 'integer', + value: heapTotal, + }, + ], + }) + } } From 2dfb7b5b8e23070b25731faa665531f6b6f4993c Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 10 Feb 2022 15:38:50 -0500 Subject: [PATCH 07/24] feat(ironfish): Submit offline point and flush when stopping telemetry (#978) * feat(ironfish): Submit offline point and flush when stopping telemetry * Disable telemetry in node test * Check for enabled --- ironfish/src/node.ts | 2 +- ironfish/src/telemetry/telemetry.test.ts | 10 ++++++++++ ironfish/src/telemetry/telemetry.ts | 15 ++++++++++++++- ironfish/src/testUtilities/nodeTest.ts | 1 + 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index 68845234a6..c26bd56e30 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -354,7 +354,7 @@ export class IronfishNode { if (newValue) { this.telemetry.start() } else { - this.telemetry.stop() + await this.telemetry.stop() } break } diff --git a/ironfish/src/telemetry/telemetry.test.ts b/ironfish/src/telemetry/telemetry.test.ts index ffc9531f72..a7212b331b 100644 --- a/ironfish/src/telemetry/telemetry.test.ts +++ b/ironfish/src/telemetry/telemetry.test.ts @@ -42,6 +42,16 @@ describe('Telemetry', () => { telemetry = mockTelemetry() }) + describe('stop', () => { + it('sends a message for the node to stop and flushes remaining points', async () => { + const flush = jest.spyOn(telemetry, 'flush') + const submitNodeStopped = jest.spyOn(telemetry, 'submitNodeStopped') + await telemetry.stop() + expect(flush).toHaveBeenCalledTimes(1) + expect(submitNodeStopped).toHaveBeenCalledTimes(1) + }) + }) + describe('submit', () => { describe('when disabled', () => { it('does nothing', async () => { diff --git a/ironfish/src/telemetry/telemetry.ts b/ironfish/src/telemetry/telemetry.ts index 4839f4abee..eed42c38c3 100644 --- a/ironfish/src/telemetry/telemetry.ts +++ b/ironfish/src/telemetry/telemetry.ts @@ -37,7 +37,12 @@ export class Telemetry { } } - stop(): void { + async stop(): Promise { + if (this.enabled) { + await this.submitNodeStopped() + await this.flush() + } + if (this.flushInterval) { clearTimeout(this.flushInterval) } @@ -93,6 +98,14 @@ export class Telemetry { }) } + async submitNodeStopped(): Promise { + await this.submit({ + measurement: 'node', + name: 'started', + fields: [{ name: 'online', type: 'boolean', value: false }], + }) + } + async submitBlockMined(block: Block): Promise { await this.submit({ measurement: 'node', diff --git a/ironfish/src/testUtilities/nodeTest.ts b/ironfish/src/testUtilities/nodeTest.ts index fc543518ba..6922d7a3dc 100644 --- a/ironfish/src/testUtilities/nodeTest.ts +++ b/ironfish/src/testUtilities/nodeTest.ts @@ -81,6 +81,7 @@ export class NodeTest { sdk.config.setOverride('bootstrapNodes', ['']) sdk.config.setOverride('enableListenP2P', false) + sdk.config.setOverride('enableTelemetry', false) // Allow tests to override default settings if (options?.config) { From 99ac48720a1cec34f317233c55528d12c46e9ec5 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Thu, 10 Feb 2022 12:46:54 -0800 Subject: [PATCH 08/24] Make Telemetry.submit() non async (#979) We now defer all the flushing to the event loop itself, so that the event loop just handles flushing. --- ironfish/src/metrics/metricsMonitor.ts | 6 ++-- ironfish/src/mining/director.ts | 2 +- ironfish/src/node.ts | 2 +- ironfish/src/telemetry/telemetry.test.ts | 27 +++++------------ ironfish/src/telemetry/telemetry.ts | 38 ++++++++++++++---------- 5 files changed, 35 insertions(+), 40 deletions(-) diff --git a/ironfish/src/metrics/metricsMonitor.ts b/ironfish/src/metrics/metricsMonitor.ts index be096fbdaf..a172a772fc 100644 --- a/ironfish/src/metrics/metricsMonitor.ts +++ b/ironfish/src/metrics/metricsMonitor.ts @@ -60,7 +60,7 @@ export class MetricsMonitor { this.memoryInterval = setInterval(() => this.refreshMemory(), this.memoryRefreshPeriodMs) if (this.telemetry) { this.memoryTelemetryInterval = setInterval( - () => void this.submitMemoryTelemetry(), + () => this.submitMemoryTelemetry(), this.memoryTelemetryPeriodMs, ) } @@ -95,9 +95,9 @@ export class MetricsMonitor { this.rss.value = memoryUsage.rss } - private async submitMemoryTelemetry(): Promise { + private submitMemoryTelemetry(): void { if (this.telemetry) { - await this.telemetry.submitMemoryUsage(this.heapUsed.value, this.heapTotal.value) + this.telemetry.submitMemoryUsage(this.heapUsed.value, this.heapTotal.value) } } } diff --git a/ironfish/src/mining/director.ts b/ironfish/src/mining/director.ts index a2fbd25130..03655e5434 100644 --- a/ironfish/src/mining/director.ts +++ b/ironfish/src/mining/director.ts @@ -501,7 +501,7 @@ export class MiningDirector { this.onNewBlock.emit(block) - await this.telemetry.submitBlockMined(block) + this.telemetry.submitBlockMined(block) return MINED_RESULT.SUCCESS } diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index c26bd56e30..14586dc419 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -296,7 +296,7 @@ export class IronfishNode { await this.rpc.start() } - await this.telemetry.submitNodeStarted() + this.telemetry.submitNodeStarted() } async waitForShutdown(): Promise { diff --git a/ironfish/src/telemetry/telemetry.test.ts b/ironfish/src/telemetry/telemetry.test.ts index a7212b331b..c8c7ca91f7 100644 --- a/ironfish/src/telemetry/telemetry.test.ts +++ b/ironfish/src/telemetry/telemetry.test.ts @@ -54,42 +54,29 @@ describe('Telemetry', () => { describe('submit', () => { describe('when disabled', () => { - it('does nothing', async () => { + it('does nothing', () => { const disabledTelemetry = mockTelemetry(false) const currentPoints = disabledTelemetry['points'] - await disabledTelemetry.submit(mockMetric) + disabledTelemetry.submit(mockMetric) expect(disabledTelemetry['points']).toEqual(currentPoints) }) }) describe('when submitting a metric without fields', () => { - it('throws an error', async () => { + it('throws an error', () => { const metric: Metric = { measurement: 'node', name: 'memory', fields: [], } - await expect(telemetry.submit(metric)).rejects.toThrowError() - }) - }) - - describe('when the queue max size has been reached', () => { - it('flushes the queue', async () => { - const flush = jest.spyOn(telemetry, 'flush') - const points = [] - for (let i = 0; i < telemetry['MAX_QUEUE_SIZE']; i++) { - points.push(mockMetric) - } - telemetry['points'] = points - await telemetry.submit(mockMetric) - expect(flush).toHaveBeenCalled() + expect(() => telemetry.submit(metric)).toThrowError() }) }) - it('stores the metric', async () => { + it('stores the metric', () => { const currentPointsLength = telemetry['points'].length - await telemetry.submit(mockMetric) + telemetry.submit(mockMetric) const points = telemetry['points'] expect(points).toHaveLength(currentPointsLength + 1) @@ -119,7 +106,7 @@ describe('Telemetry', () => { it('submits telemetry to the pool', async () => { const submitTelemetry = jest.spyOn(telemetry['pool'], 'submitTelemetry') - await telemetry.submit(mockMetric) + telemetry.submit(mockMetric) await telemetry.flush() expect(submitTelemetry).toHaveBeenCalled() diff --git a/ironfish/src/telemetry/telemetry.ts b/ironfish/src/telemetry/telemetry.ts index eed42c38c3..3dcc34ed4b 100644 --- a/ironfish/src/telemetry/telemetry.ts +++ b/ironfish/src/telemetry/telemetry.ts @@ -33,13 +33,13 @@ export class Telemetry { start(): void { if (this.enabled) { - this.flushInterval = setInterval(() => void this.flush(), this.FLUSH_INTERVAL) + void this.flushLoop() } } async stop(): Promise { if (this.enabled) { - await this.submitNodeStopped() + this.submitNodeStopped() await this.flush() } @@ -48,7 +48,15 @@ export class Telemetry { } } - async submit(metric: Metric): Promise { + async flushLoop(): Promise { + await this.flush() + + this.flushInterval = setTimeout(() => { + void this.flushLoop() + }, this.FLUSH_INTERVAL) + } + + submit(metric: Metric): void { if (!this.enabled) { return } @@ -67,16 +75,16 @@ export class Telemetry { timestamp: metric.timestamp || new Date(), tags, }) - - if (this.points.length >= this.MAX_QUEUE_SIZE) { - await this.flush() - } } async flush(): Promise { const points = this.points this.points = [] + if (points.length === 0) { + return + } + try { await this.pool.submitTelemetry(points) this.logger.debug(`Submitted ${points.length} telemetry points`) @@ -90,24 +98,24 @@ export class Telemetry { } } - async submitNodeStarted(): Promise { - await this.submit({ + submitNodeStarted(): void { + this.submit({ measurement: 'node', name: 'started', fields: [{ name: 'online', type: 'boolean', value: true }], }) } - async submitNodeStopped(): Promise { - await this.submit({ + submitNodeStopped(): void { + this.submit({ measurement: 'node', name: 'started', fields: [{ name: 'online', type: 'boolean', value: false }], }) } - async submitBlockMined(block: Block): Promise { - await this.submit({ + submitBlockMined(block: Block): void { + this.submit({ measurement: 'node', name: 'block_mined', fields: [ @@ -125,8 +133,8 @@ export class Telemetry { }) } - async submitMemoryUsage(heapUsed: number, heapTotal: number): Promise { - await this.submit({ + submitMemoryUsage(heapUsed: number, heapTotal: number): void { + this.submit({ measurement: 'node', name: 'memory', fields: [ From 19866deebe6a3e0dc4d4dde9c38d8997f6ed7dde Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Thu, 10 Feb 2022 13:05:08 -0800 Subject: [PATCH 09/24] Remove enabled from telemetry (#980) * Remove enabled from telemetry We already don't start the telemetry system it's not enabled so we can just simplify this to operate based off of the telemetry system is running or not. * Fixed tests * Lint --- ironfish/src/node.ts | 2 +- ironfish/src/telemetry/telemetry.test.ts | 26 ++++++++---------------- ironfish/src/telemetry/telemetry.ts | 26 +++++++++++++++--------- ironfish/src/testUtilities/mocks.ts | 10 ++++++++- 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index 14586dc419..d042c5e011 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -188,7 +188,7 @@ export class IronfishNode { strategyClass = strategyClass || Strategy const strategy = new strategyClass(workerPool) - const telemetry = new Telemetry(config, workerPool, logger, [ + const telemetry = new Telemetry(workerPool, logger, [ { name: 'node_id', value: internal.get('telemetryNodeId') }, { name: 'session_id', value: uuid() }, { name: 'version', value: pkg.version }, diff --git a/ironfish/src/telemetry/telemetry.test.ts b/ironfish/src/telemetry/telemetry.test.ts index c8c7ca91f7..d49d4badd7 100644 --- a/ironfish/src/telemetry/telemetry.test.ts +++ b/ironfish/src/telemetry/telemetry.test.ts @@ -1,6 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { mockLogger, mockWorkerPool } from '../testUtilities/mocks' import { Metric } from './interfaces/metric' import { Telemetry } from './telemetry' @@ -10,22 +11,6 @@ import { Telemetry } from './telemetry' describe('Telemetry', () => { let telemetry: Telemetry - const mockTelemetry = (enabled = true): Telemetry => { - /* eslint-disable @typescript-eslint/no-explicit-any */ - const mockConfig: any = { - get: jest.fn().mockResolvedValueOnce(enabled), - } - const mockPool: any = { - submitTelemetry: jest.fn(), - } - const mockLogger: any = { - debug: jest.fn(), - error: jest.fn(), - } - /* eslint-enable @typescript-eslint/no-explicit-any */ - return new Telemetry(mockConfig, mockPool, mockLogger, []) - } - const mockMetric: Metric = { measurement: 'node', name: 'memory', @@ -39,7 +24,12 @@ describe('Telemetry', () => { } beforeEach(() => { - telemetry = mockTelemetry() + telemetry = new Telemetry(mockWorkerPool(), mockLogger(), []) + telemetry.start() + }) + + afterEach(() => { + telemetry?.stop() }) describe('stop', () => { @@ -55,7 +45,7 @@ describe('Telemetry', () => { describe('submit', () => { describe('when disabled', () => { it('does nothing', () => { - const disabledTelemetry = mockTelemetry(false) + const disabledTelemetry = new Telemetry(mockWorkerPool(), mockLogger(), []) const currentPoints = disabledTelemetry['points'] disabledTelemetry.submit(mockMetric) expect(disabledTelemetry['points']).toEqual(currentPoints) diff --git a/ironfish/src/telemetry/telemetry.ts b/ironfish/src/telemetry/telemetry.ts index 3dcc34ed4b..6c2efd1003 100644 --- a/ironfish/src/telemetry/telemetry.ts +++ b/ironfish/src/telemetry/telemetry.ts @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Config } from '../fileStores' import { Logger } from '../logger' import { Block } from '../primitives/block' import { renderError, SetIntervalToken } from '../utils' @@ -13,39 +12,46 @@ export class Telemetry { private readonly FLUSH_INTERVAL = 5000 private readonly MAX_QUEUE_SIZE = 1000 - private readonly enabled: boolean private readonly defaultTags: Tag[] private readonly logger: Logger private readonly pool: WorkerPool + private started: boolean private flushInterval: SetIntervalToken | null private points: Metric[] - constructor(config: Config, pool: WorkerPool, logger: Logger, defaultTags: Tag[]) { - this.enabled = config.get('enableTelemetry') + constructor(pool: WorkerPool, logger: Logger, defaultTags: Tag[]) { this.logger = logger this.pool = pool this.defaultTags = defaultTags + this.started = false this.flushInterval = null this.points = [] } start(): void { - if (this.enabled) { - void this.flushLoop() + if (this.started) { + return } + + this.started = true + void this.flushLoop() } async stop(): Promise { - if (this.enabled) { - this.submitNodeStopped() - await this.flush() + if (!this.started) { + return } + this.started = false + if (this.flushInterval) { clearTimeout(this.flushInterval) } + + this.submitNodeStopped() + await this.flush() } async flushLoop(): Promise { @@ -57,7 +63,7 @@ export class Telemetry { } submit(metric: Metric): void { - if (!this.enabled) { + if (!this.started) { return } diff --git a/ironfish/src/testUtilities/mocks.ts b/ironfish/src/testUtilities/mocks.ts index 929c57c090..692cb94e26 100644 --- a/ironfish/src/testUtilities/mocks.ts +++ b/ironfish/src/testUtilities/mocks.ts @@ -67,8 +67,16 @@ export function mockSyncer(): any { } } -function mockWorkerPool(): unknown { +export function mockLogger(): any { + return { + debug: jest.fn(), + error: jest.fn(), + } +} + +export function mockWorkerPool(): any { return { saturated: jest.fn(), + submitTelemetry: jest.fn(), } } From 2222ea94e1300ca2fb751563541f90ae6ec5de1b Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Thu, 10 Feb 2022 13:53:25 -0800 Subject: [PATCH 10/24] Convert telemetry constructor to object (#981) This helps scalability for adding more things in the future --- ironfish/src/node.ts | 14 +++++++++----- ironfish/src/telemetry/telemetry.test.ts | 13 ++++++++----- ironfish/src/telemetry/telemetry.ts | 14 +++++++------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index d042c5e011..267928c60b 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -188,11 +188,15 @@ export class IronfishNode { strategyClass = strategyClass || Strategy const strategy = new strategyClass(workerPool) - const telemetry = new Telemetry(workerPool, logger, [ - { name: 'node_id', value: internal.get('telemetryNodeId') }, - { name: 'session_id', value: uuid() }, - { name: 'version', value: pkg.version }, - ]) + const telemetry = new Telemetry({ + workerPool, + logger, + defaultTags: [ + { name: 'node_id', value: internal.get('telemetryNodeId') }, + { name: 'session_id', value: uuid() }, + { name: 'version', value: pkg.version }, + ], + }) metrics = metrics || new MetricsMonitor({ telemetry, logger }) diff --git a/ironfish/src/telemetry/telemetry.test.ts b/ironfish/src/telemetry/telemetry.test.ts index d49d4badd7..c0ad02acd0 100644 --- a/ironfish/src/telemetry/telemetry.test.ts +++ b/ironfish/src/telemetry/telemetry.test.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { mockLogger, mockWorkerPool } from '../testUtilities/mocks' +import { mockWorkerPool } from '../testUtilities/mocks' import { Metric } from './interfaces/metric' import { Telemetry } from './telemetry' @@ -24,7 +24,10 @@ describe('Telemetry', () => { } beforeEach(() => { - telemetry = new Telemetry(mockWorkerPool(), mockLogger(), []) + telemetry = new Telemetry({ + workerPool: mockWorkerPool(), + }) + telemetry.start() }) @@ -45,7 +48,7 @@ describe('Telemetry', () => { describe('submit', () => { describe('when disabled', () => { it('does nothing', () => { - const disabledTelemetry = new Telemetry(mockWorkerPool(), mockLogger(), []) + const disabledTelemetry = new Telemetry({ workerPool: mockWorkerPool() }) const currentPoints = disabledTelemetry['points'] disabledTelemetry.submit(mockMetric) expect(disabledTelemetry['points']).toEqual(currentPoints) @@ -77,7 +80,7 @@ describe('Telemetry', () => { describe('flush', () => { describe('when the pool throws an error and the queue is not saturated', () => { it('retries the points and logs an error', async () => { - jest.spyOn(telemetry['pool'], 'submitTelemetry').mockImplementationOnce(() => { + jest.spyOn(telemetry['workerPool'], 'submitTelemetry').mockImplementationOnce(() => { throw new Error() }) const error = jest.spyOn(telemetry['logger'], 'error') @@ -95,7 +98,7 @@ describe('Telemetry', () => { }) it('submits telemetry to the pool', async () => { - const submitTelemetry = jest.spyOn(telemetry['pool'], 'submitTelemetry') + const submitTelemetry = jest.spyOn(telemetry['workerPool'], 'submitTelemetry') telemetry.submit(mockMetric) await telemetry.flush() diff --git a/ironfish/src/telemetry/telemetry.ts b/ironfish/src/telemetry/telemetry.ts index 6c2efd1003..734ebdbb98 100644 --- a/ironfish/src/telemetry/telemetry.ts +++ b/ironfish/src/telemetry/telemetry.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Logger } from '../logger' +import { createRootLogger, Logger } from '../logger' import { Block } from '../primitives/block' import { renderError, SetIntervalToken } from '../utils' import { WorkerPool } from '../workerPool' @@ -14,16 +14,16 @@ export class Telemetry { private readonly defaultTags: Tag[] private readonly logger: Logger - private readonly pool: WorkerPool + private readonly workerPool: WorkerPool private started: boolean private flushInterval: SetIntervalToken | null private points: Metric[] - constructor(pool: WorkerPool, logger: Logger, defaultTags: Tag[]) { - this.logger = logger - this.pool = pool - this.defaultTags = defaultTags + constructor(options: { workerPool: WorkerPool; logger?: Logger; defaultTags?: Tag[] }) { + this.logger = options.logger ?? createRootLogger() + this.workerPool = options.workerPool + this.defaultTags = options.defaultTags ?? [] this.started = false this.flushInterval = null @@ -92,7 +92,7 @@ export class Telemetry { } try { - await this.pool.submitTelemetry(points) + await this.workerPool.submitTelemetry(points) this.logger.debug(`Submitted ${points.length} telemetry points`) } catch (error: unknown) { this.logger.error(`Error submitting telemetry to API: ${renderError(error)}`) From 207edc72012d0dcfec439acc85e99bdfc4368d45 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Thu, 10 Feb 2022 16:48:49 -0800 Subject: [PATCH 11/24] Remove use of napi-rs factory functions (#977) napi-rs has a concurrency bug when constructing objects using factory functions in worker threads. This change removes the use of factory functions in favor of constructors. --- ironfish-rust-nodejs/index.d.ts | 16 +++++++--- ironfish-rust-nodejs/index.js | 3 +- ironfish-rust-nodejs/src/structs/note.rs | 32 +++++++++++++++---- .../src/structs/note_encrypted.rs | 4 +-- .../src/structs/transaction.rs | 4 +-- ironfish/src/genesis/makeGenesisBlock.ts | 17 +++++++--- ironfish/src/primitives/note.ts | 2 +- ironfish/src/primitives/noteEncrypted.ts | 2 +- ironfish/src/primitives/transaction.ts | 2 +- ironfish/src/strategy.test.slow.ts | 22 +++++++++---- .../src/workerPool/tasks/createMinersFee.ts | 4 +-- .../src/workerPool/tasks/createTransaction.ts | 6 ++-- .../src/workerPool/tasks/getUnspentNotes.ts | 4 +-- .../src/workerPool/tasks/transactionFee.ts | 2 +- .../src/workerPool/tasks/verifyTransaction.ts | 2 +- 15 files changed, 83 insertions(+), 39 deletions(-) diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 6616993c31..0e5d5b4907 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -24,7 +24,7 @@ export interface MineHeaderNapiResult { export function mineHeaderBatch(headerBytes: Buffer, initialRandomness: number, targetBuffer: Buffer, batchSize: number): MineHeaderNapiResult export type NativeNoteEncrypted = NoteEncrypted export class NoteEncrypted { - static deserialize(bytes: Buffer): NativeNoteEncrypted + constructor(bytes: Buffer) serialize(): Buffer equals(other: NoteEncrypted): boolean merkleHash(): Buffer @@ -38,10 +38,18 @@ export class NoteEncrypted { /** Returns undefined if the note was unable to be decrypted with the given key. */ decryptNoteForSpender(outgoingHexKey: string): NativeNote | undefined | null } +export type NativeNoteBuilder = NoteBuilder +export class NoteBuilder { + /** + * TODO: This works around a concurrency bug when using #[napi(factory)] + * in worker threads. It can be merged into NativeNote once the bug is fixed. + */ + constructor(owner: string, value: bigint, memo: string) + serialize(): Buffer +} export type NativeNote = Note export class Note { - constructor(owner: string, value: bigint, memo: string) - static deserialize(bytes: Buffer): NativeNote + constructor(bytes: Buffer) serialize(): Buffer /** Value this note represents. */ value(): bigint @@ -68,7 +76,7 @@ export class NativeSpendProof { } export type NativeTransactionPosted = TransactionPosted export class TransactionPosted { - static deserialize(bytes: Buffer): NativeTransactionPosted + constructor(bytes: Buffer) serialize(): Buffer verify(): boolean notesLength(): number diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index b08a5ab897..a25ce3f5e2 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -236,9 +236,10 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { NoteEncrypted, Note, NativeSpendProof, TransactionPosted, Transaction, SimpleTransaction, generateKey, generateNewPublicAddress, mineHeaderBatch } = nativeBinding +const { NoteEncrypted, NoteBuilder, Note, NativeSpendProof, TransactionPosted, Transaction, SimpleTransaction, generateKey, generateNewPublicAddress, mineHeaderBatch } = nativeBinding module.exports.NoteEncrypted = NoteEncrypted +module.exports.NoteBuilder = NoteBuilder module.exports.Note = Note module.exports.NativeSpendProof = NativeSpendProof module.exports.TransactionPosted = TransactionPosted diff --git a/ironfish-rust-nodejs/src/structs/note.rs b/ironfish-rust-nodejs/src/structs/note.rs index 5eeff1e941..257e67082d 100644 --- a/ironfish-rust-nodejs/src/structs/note.rs +++ b/ironfish-rust-nodejs/src/structs/note.rs @@ -9,26 +9,46 @@ use napi_derive::napi; use ironfish_rust::note::Memo; use ironfish_rust::sapling_bls12::{Key, Note, SAPLING}; -#[napi(js_name = "Note")] -pub struct NativeNote { +#[napi(js_name = "NoteBuilder")] +pub struct NativeNoteBuilder { pub(crate) note: Note, } #[napi] -impl NativeNote { +impl NativeNoteBuilder { + /// TODO: This works around a concurrency bug when using #[napi(factory)] + /// in worker threads. It can be merged into NativeNote once the bug is fixed. #[napi(constructor)] pub fn new(owner: String, value: JsBigInt, memo: String) -> Result { let value_u64 = value.get_u64()?.0; let owner_address = ironfish_rust::PublicAddress::from_hex(SAPLING.clone(), &owner) .map_err(|err| Error::from_reason(err.to_string()))?; - Ok(NativeNote { + Ok(NativeNoteBuilder { note: Note::new(SAPLING.clone(), owner_address, value_u64, Memo::from(memo)), }) } - #[napi(factory)] - pub fn deserialize(bytes: Buffer) -> Result { + #[napi] + pub fn serialize(&self) -> Result { + let mut arr: Vec = vec![]; + self.note + .write(&mut arr) + .map_err(|err| Error::from_reason(err.to_string()))?; + + Ok(Buffer::from(arr)) + } +} + +#[napi(js_name = "Note")] +pub struct NativeNote { + pub(crate) note: Note, +} + +#[napi] +impl NativeNote { + #[napi(constructor)] + pub fn new(bytes: Buffer) -> Result { let hasher = SAPLING.clone(); let note = Note::read(bytes.as_ref(), hasher) .map_err(|err| Error::from_reason(err.to_string()))?; diff --git a/ironfish-rust-nodejs/src/structs/note_encrypted.rs b/ironfish-rust-nodejs/src/structs/note_encrypted.rs index 9c9af0c1c8..d223fd8e1c 100644 --- a/ironfish-rust-nodejs/src/structs/note_encrypted.rs +++ b/ironfish-rust-nodejs/src/structs/note_encrypted.rs @@ -16,8 +16,8 @@ pub struct NativeNoteEncrypted { #[napi] impl NativeNoteEncrypted { - #[napi(factory)] - pub fn deserialize(bytes: Buffer) -> Result { + #[napi(constructor)] + pub fn new(bytes: Buffer) -> Result { let hasher = sapling_bls12::SAPLING.clone(); let note = MerkleNote::read(bytes.as_ref(), hasher) diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index 984e6f2fd1..85b5f1b925 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -24,8 +24,8 @@ pub struct NativeTransactionPosted { #[napi] impl NativeTransactionPosted { - #[napi(factory)] - pub fn deserialize(bytes: Buffer) -> Result { + #[napi(constructor)] + pub fn new(bytes: Buffer) -> Result { let mut cursor = std::io::Cursor::new(bytes); let transaction = Transaction::read(SAPLING.clone(), &mut cursor) diff --git a/ironfish/src/genesis/makeGenesisBlock.ts b/ironfish/src/genesis/makeGenesisBlock.ts index cee2ba4af6..e96d58280b 100644 --- a/ironfish/src/genesis/makeGenesisBlock.ts +++ b/ironfish/src/genesis/makeGenesisBlock.ts @@ -6,6 +6,7 @@ import type { Account } from '../account' import { generateKey, Note as NativeNote, + NoteBuilder as NativeNoteBuilder, Transaction as NativeTransaction, } from 'ironfish-rust-nodejs' import { Blockchain } from '../blockchain' @@ -52,9 +53,11 @@ export async function makeGenesisBlock( const genesisKey = generateKey() // Create a genesis note granting the genesisKey allocationSum coins. const genesisNote = new NativeNote( - genesisKey.public_address, - BigInt(allocationSum), - info.memo, + new NativeNoteBuilder( + genesisKey.public_address, + BigInt(allocationSum), + info.memo, + ).serialize(), ) // Create a miner's fee transaction for the block. @@ -64,7 +67,9 @@ export async function makeGenesisBlock( // This transaction will cause block.verify to fail, but we skip block verification // throughout the code when the block header's previousBlockHash is GENESIS_BLOCK_PREVIOUS. logger.info(`Generating a miner's fee transaction for the block...`) - const note = new NativeNote(account.publicAddress, BigInt(0), '') + const note = new NativeNote( + new NativeNoteBuilder(account.publicAddress, BigInt(0), '').serialize(), + ) const minersFeeTransaction = new NativeTransaction() minersFeeTransaction.receive(account.spendingKey, note) const postedMinersFeeTransaction = new Transaction( @@ -126,7 +131,9 @@ export async function makeGenesisBlock( logger.info( ` Generating a receipt for ${alloc.amount} coins for ${alloc.publicAddress}...`, ) - const note = new NativeNote(alloc.publicAddress, BigInt(alloc.amount), info.memo) + const note = new NativeNote( + new NativeNoteBuilder(alloc.publicAddress, BigInt(alloc.amount), info.memo).serialize(), + ) transaction.receive(genesisKey.spending_key, note) } diff --git a/ironfish/src/primitives/note.ts b/ironfish/src/primitives/note.ts index e060f427a9..86f8b643fa 100644 --- a/ironfish/src/primitives/note.ts +++ b/ironfish/src/primitives/note.ts @@ -20,7 +20,7 @@ export class Note { takeReference(): NativeNote { this.referenceCount++ if (this.note === null) { - this.note = NativeNote.deserialize(this.noteSerialized) + this.note = new NativeNote(this.noteSerialized) } return this.note } diff --git a/ironfish/src/primitives/noteEncrypted.ts b/ironfish/src/primitives/noteEncrypted.ts index f7eba40a72..cff2062169 100644 --- a/ironfish/src/primitives/noteEncrypted.ts +++ b/ironfish/src/primitives/noteEncrypted.ts @@ -26,7 +26,7 @@ export class NoteEncrypted { takeReference(): NativeNoteEncrypted { this.referenceCount++ if (this.noteEncrypted === null) { - this.noteEncrypted = NativeNoteEncrypted.deserialize(this.noteEncryptedSerialized) + this.noteEncrypted = new NativeNoteEncrypted(this.noteEncryptedSerialized) } return this.noteEncrypted } diff --git a/ironfish/src/primitives/transaction.ts b/ironfish/src/primitives/transaction.ts index 50ec7863d7..5e686d8d1e 100644 --- a/ironfish/src/primitives/transaction.ts +++ b/ironfish/src/primitives/transaction.ts @@ -36,7 +36,7 @@ export class Transaction { takeReference(): TransactionPosted { this.referenceCount++ if (this.transactionPosted === null) { - this.transactionPosted = TransactionPosted.deserialize(this.transactionPostedSerialized) + this.transactionPosted = new TransactionPosted(this.transactionPostedSerialized) } return this.transactionPosted } diff --git a/ironfish/src/strategy.test.slow.ts b/ironfish/src/strategy.test.slow.ts index b6f86d11d8..5882843393 100644 --- a/ironfish/src/strategy.test.slow.ts +++ b/ironfish/src/strategy.test.slow.ts @@ -7,6 +7,7 @@ import { generateNewPublicAddress, Key, Note as NativeNote, + NoteBuilder as NativeNoteBuilder, SimpleTransaction as NativeSimpleTransaction, Transaction as NativeTransaction, TransactionPosted as NativeTransactionPosted, @@ -99,7 +100,8 @@ describe('Demonstrate the Sapling API', () => { it('Can create a miner reward', () => { const owner = generateNewPublicAddress(spenderKey.spending_key).public_address - minerNote = new NativeNote(owner, BigInt(42), '') + minerNote = new NativeNote(new NativeNoteBuilder(owner, BigInt(42), '').serialize()) + const transaction = new NativeTransaction() expect(transaction.receive(spenderKey.spending_key, minerNote)).toBe('') minerTransaction = transaction.post_miners_fee() @@ -109,7 +111,7 @@ describe('Demonstrate the Sapling API', () => { it('Can verify the miner transaction', () => { const serializedTransaction = minerTransaction.serialize() - const deserializedTransaction = NativeTransactionPosted.deserialize(serializedTransaction) + const deserializedTransaction = new NativeTransactionPosted(serializedTransaction) expect(deserializedTransaction.verify()).toBeTruthy() }) @@ -138,7 +140,9 @@ describe('Demonstrate the Sapling API', () => { it('Can add a receive to the transaction', () => { receiverKey = generateKey() - const receivingNote = new NativeNote(receiverKey.public_address, BigInt(40), '') + const receivingNote = new NativeNote( + new NativeNoteBuilder(receiverKey.public_address, BigInt(40), '').serialize(), + ) const result = simpleTransaction.receive(receivingNote) expect(result).toEqual('') }) @@ -270,11 +274,15 @@ describe('Demonstrate the Sapling API', () => { expect(transaction.spend(receiverKey.spending_key, note, witness)).toBe('') receiverNote.returnReference() - const noteForSpender = new NativeNote(spenderKey.public_address, BigInt(10), '') + const noteForSpender = new NativeNote( + new NativeNoteBuilder(spenderKey.public_address, BigInt(10), '').serialize(), + ) const receiverNoteToSelf = new NativeNote( - generateNewPublicAddress(receiverKey.spending_key).public_address, - BigInt(29), - '', + new NativeNoteBuilder( + generateNewPublicAddress(receiverKey.spending_key).public_address, + BigInt(29), + '', + ).serialize(), ) expect(transaction.receive(receiverKey.spending_key, noteForSpender)).toBe('') diff --git a/ironfish/src/workerPool/tasks/createMinersFee.ts b/ironfish/src/workerPool/tasks/createMinersFee.ts index 54b7137af2..d65b3d042b 100644 --- a/ironfish/src/workerPool/tasks/createMinersFee.ts +++ b/ironfish/src/workerPool/tasks/createMinersFee.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { generateNewPublicAddress, Note, Transaction } from 'ironfish-rust-nodejs' +import { generateNewPublicAddress, Note, NoteBuilder, Transaction } from 'ironfish-rust-nodejs' export type CreateMinersFeeRequest = { type: 'createMinersFee' @@ -24,7 +24,7 @@ export function handleCreateMinersFee({ // Generate a public address from the miner's spending key const minerPublicAddress = generateNewPublicAddress(spendKey).public_address - const minerNote = new Note(minerPublicAddress, amount, memo) + const minerNote = new Note(new NoteBuilder(minerPublicAddress, amount, memo).serialize()) const transaction = new Transaction() transaction.receive(spendKey, minerNote) diff --git a/ironfish/src/workerPool/tasks/createTransaction.ts b/ironfish/src/workerPool/tasks/createTransaction.ts index 1167aafa2b..f7d4f3954a 100644 --- a/ironfish/src/workerPool/tasks/createTransaction.ts +++ b/ironfish/src/workerPool/tasks/createTransaction.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Note, Transaction } from 'ironfish-rust-nodejs' +import { Note, NoteBuilder, Transaction } from 'ironfish-rust-nodejs' import { Witness } from '../../merkletree' import { NoteHasher } from '../../merkletree/hasher' import { Side } from '../../merkletree/merkletree' @@ -43,7 +43,7 @@ export function handleCreateTransaction({ transaction.setExpirationSequence(expirationSequence) for (const spend of spends) { - const note = Note.deserialize(spend.note) + const note = new Note(spend.note) transaction.spend( spendKey, note, @@ -52,7 +52,7 @@ export function handleCreateTransaction({ } for (const { publicAddress, amount, memo } of receives) { - const note = new Note(publicAddress, amount, memo) + const note = new Note(new NoteBuilder(publicAddress, amount, memo).serialize()) transaction.receive(spendKey, note) } diff --git a/ironfish/src/workerPool/tasks/getUnspentNotes.ts b/ironfish/src/workerPool/tasks/getUnspentNotes.ts index d855d86019..5b0e30b65a 100644 --- a/ironfish/src/workerPool/tasks/getUnspentNotes.ts +++ b/ironfish/src/workerPool/tasks/getUnspentNotes.ts @@ -23,13 +23,13 @@ export function handleGetUnspentNotes({ accounts, serializedTransactionPosted, }: GetUnspentNotesRequest): GetUnspentNotesResponse { - const transaction = TransactionPosted.deserialize(serializedTransactionPosted) + const transaction = new TransactionPosted(serializedTransactionPosted) const results: GetUnspentNotesResponse['notes'] = [] for (let i = 0; i < transaction.notesLength(); i++) { const serializedNote = transaction.getNote(i) - const note = NoteEncrypted.deserialize(serializedNote) + const note = new NoteEncrypted(serializedNote) // Notes can be spent and received by the same Account. // Try decrypting the note as its owner diff --git a/ironfish/src/workerPool/tasks/transactionFee.ts b/ironfish/src/workerPool/tasks/transactionFee.ts index 88b73dd9b7..a4da80e090 100644 --- a/ironfish/src/workerPool/tasks/transactionFee.ts +++ b/ironfish/src/workerPool/tasks/transactionFee.ts @@ -17,7 +17,7 @@ export type TransactionFeeResponse = { export function handleTransactionFee({ serializedTransactionPosted, }: TransactionFeeRequest): TransactionFeeResponse { - const transaction = TransactionPosted.deserialize(serializedTransactionPosted) + const transaction = new TransactionPosted(serializedTransactionPosted) const fee = transaction.fee() return { type: 'transactionFee', transactionFee: fee } diff --git a/ironfish/src/workerPool/tasks/verifyTransaction.ts b/ironfish/src/workerPool/tasks/verifyTransaction.ts index 139c14c32f..2e7ba0f8cc 100644 --- a/ironfish/src/workerPool/tasks/verifyTransaction.ts +++ b/ironfish/src/workerPool/tasks/verifyTransaction.ts @@ -27,7 +27,7 @@ export function handleVerifyTransaction({ let verified = false try { - transaction = TransactionPosted.deserialize(serializedTransactionPosted) + transaction = new TransactionPosted(serializedTransactionPosted) if (verifyFees && transaction.fee() < BigInt(0)) { throw new Error('Transaction has negative fees') From fee68e382e20a6dd6c4e8edc5a5939d3f6e3e8ff Mon Sep 17 00:00:00 2001 From: wd021 Date: Sat, 12 Feb 2022 05:14:49 +0000 Subject: [PATCH 12/24] readme update for python version error (#985) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5d64bea31c..8b62aa6eb8 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ See https://ironfish.network 1. On Ubuntu: `apt install build-essential` 1. On Amazon Linux: `sudo yum groupinstall "Development Tools"` + - If `yarn install` fails with an error that includes "Error: Could not find any Python installation to use", you may need to install Python3 (required by node-gyp). on MacOS: + 1. Run `brew install python` + ## Usage Once your environment is setup - you can run the CLI by following [these directions](https://github.com/iron-fish/ironfish/tree/master/ironfish-cli). From 366a42981b15dbe0d213f1e3638e1ed51ed7eac1 Mon Sep 17 00:00:00 2001 From: Kupuyc Date: Sat, 12 Feb 2022 08:28:50 +0300 Subject: [PATCH 13/24] Migrate to @oclif/core (#984) * Migrate version hook from @oclif/config to @oclif/core * Migrate all commands from @oclif/config and @oclif/command to @oclif/core * Migrate entry point of CLI application from @oclif/command to @oclif/core * Migrate bin file of CLI application from @oclif/command to @oclif/core * Remove @oclif/config and @oclif/command from project * Add return clause to make condition full value type guard * Remove unused import * Create alias to make definition fo flags shorter * Fix of modules import order * Fix of "unsafe" assigment * Fix of forgotten await clause * Fix wrong declaration of parse function * Fix of linter @typescript-eslint/require-await error * Balancing linter requirements and quality of code: don't use unnecessary async and specify return type * Remove dead code --- ironfish-cli/bin/run | 6 +-- ironfish-cli/package.json | 2 - ironfish-cli/src/command.ts | 8 +-- ironfish-cli/src/commands/accounts/balance.ts | 4 +- ironfish-cli/src/commands/accounts/create.ts | 4 +- ironfish-cli/src/commands/accounts/export.ts | 11 ++-- ironfish-cli/src/commands/accounts/import.ts | 11 ++-- ironfish-cli/src/commands/accounts/list.ts | 6 +-- ironfish-cli/src/commands/accounts/pay.ts | 19 ++++--- .../src/commands/accounts/publickey.ts | 8 +-- ironfish-cli/src/commands/accounts/remove.ts | 7 ++- ironfish-cli/src/commands/accounts/rescan.ts | 12 ++--- ironfish-cli/src/commands/accounts/use.ts | 2 +- ironfish-cli/src/commands/accounts/which.ts | 6 +-- ironfish-cli/src/commands/backup.ts | 9 ++-- ironfish-cli/src/commands/blocks/show.ts | 4 +- ironfish-cli/src/commands/chain/export.ts | 13 +++-- ironfish-cli/src/commands/chain/forks.ts | 2 +- .../src/commands/chain/genesisblock.ts | 10 ++-- ironfish-cli/src/commands/chain/readdblock.ts | 6 +-- ironfish-cli/src/commands/chain/repair.ts | 11 ++-- ironfish-cli/src/commands/chain/show.ts | 6 +-- ironfish-cli/src/commands/config/edit.ts | 6 +-- ironfish-cli/src/commands/config/get.ts | 12 ++--- ironfish-cli/src/commands/config/set.ts | 10 ++-- ironfish-cli/src/commands/config/show.ts | 8 +-- ironfish-cli/src/commands/faucet.ts | 9 ++-- ironfish-cli/src/commands/logs.ts | 2 +- ironfish-cli/src/commands/miners/mined.ts | 6 +-- ironfish-cli/src/commands/miners/start.ts | 7 ++- ironfish-cli/src/commands/peers/list.ts | 19 ++++--- ironfish-cli/src/commands/peers/show.ts | 4 +- ironfish-cli/src/commands/reset.ts | 7 ++- ironfish-cli/src/commands/restore.ts | 7 ++- ironfish-cli/src/commands/service/faucet.ts | 12 ++--- ironfish-cli/src/commands/service/sync.ts | 12 ++--- ironfish-cli/src/commands/start.ts | 22 ++++---- ironfish-cli/src/commands/status.ts | 6 +-- ironfish-cli/src/commands/stop.ts | 2 +- ironfish-cli/src/commands/swim.ts | 2 +- ironfish-cli/src/commands/testnet.ts | 15 +++--- ironfish-cli/src/commands/workers/status.ts | 6 +-- ironfish-cli/src/flags.ts | 53 ++++++++++--------- ironfish-cli/src/hooks/version.ts | 5 +- ironfish-cli/src/index.ts | 2 +- yarn.lock | 4 +- 46 files changed, 196 insertions(+), 209 deletions(-) diff --git a/ironfish-cli/bin/run b/ironfish-cli/bin/run index b96cf1244d..76329c7749 100755 --- a/ironfish-cli/bin/run +++ b/ironfish-cli/bin/run @@ -7,6 +7,6 @@ if (process.platform !== 'win32') { require('segfault-handler').registerHandler('segfault.log') } -require('@oclif/command').run() -.then(require('@oclif/command/flush')) -.catch(require('@oclif/errors/handle')) +require('@oclif/core').run() +.then(require('@oclif/core/flush')) +.catch(require('@oclif/core/handle')) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 2a140954d0..1998e5080f 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -42,8 +42,6 @@ }, "license": "MPL-2.0", "dependencies": { - "@oclif/command": "1.8.0", - "@oclif/config": "1.17.0", "@oclif/core": "1.3.1", "@oclif/plugin-help": "3.2.2", "@oclif/plugin-not-found": "1.2.4", diff --git a/ironfish-cli/src/command.ts b/ironfish-cli/src/command.ts index 1042b73510..dcff9e8b16 100644 --- a/ironfish-cli/src/command.ts +++ b/ironfish-cli/src/command.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Command } from '@oclif/command' -import { IConfig } from '@oclif/config' +import { Command, Config } from '@oclif/core' import { ConfigOptions, ConnectionError, createRootLogger, IronfishSdk, Logger } from 'ironfish' import { ConfigFlagKey, @@ -53,7 +52,7 @@ export abstract class IronfishCommand extends Command { */ closing = false - constructor(argv: string[], config: IConfig) { + constructor(argv: string[], config: Config) { super(argv, config) this.logger = createRootLogger().withTag(this.ctor.id) } @@ -79,7 +78,8 @@ export abstract class IronfishCommand extends Command { async init(): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any const commandClass = this.constructor as any - const { flags } = this.parse(commandClass) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { flags } = await this.parse(commandClass) // Get the flags from the flag object which is unknown const dataDirFlag = getFlag(flags, DataDirFlagKey) diff --git a/ironfish-cli/src/commands/accounts/balance.ts b/ironfish-cli/src/commands/accounts/balance.ts index 0bc7e6b858..c4aa4b350c 100644 --- a/ironfish-cli/src/commands/accounts/balance.ts +++ b/ironfish-cli/src/commands/accounts/balance.ts @@ -15,14 +15,14 @@ export class BalanceCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'name of the account to get balance for', }, ] async start(): Promise { - const { args } = this.parse(BalanceCommand) + const { args } = await this.parse(BalanceCommand) const account = args.account as string | undefined const client = await this.sdk.connectRpc() diff --git a/ironfish-cli/src/commands/accounts/create.ts b/ironfish-cli/src/commands/accounts/create.ts index f7db29d359..3891b38957 100644 --- a/ironfish-cli/src/commands/accounts/create.ts +++ b/ironfish-cli/src/commands/accounts/create.ts @@ -12,7 +12,7 @@ export class CreateCommand extends IronfishCommand { static args = [ { name: 'name', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'name of the account', }, @@ -23,7 +23,7 @@ export class CreateCommand extends IronfishCommand { } async start(): Promise { - const { args } = this.parse(CreateCommand) + const { args } = await this.parse(CreateCommand) let name = args.name as string if (!name) { diff --git a/ironfish-cli/src/commands/accounts/export.ts b/ironfish-cli/src/commands/accounts/export.ts index 3d4bdf6571..99fb3ee70f 100644 --- a/ironfish-cli/src/commands/accounts/export.ts +++ b/ironfish-cli/src/commands/accounts/export.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import fs from 'fs' import { ErrorUtils } from 'ironfish' import jsonColorizer from 'json-colorizer' @@ -16,7 +15,7 @@ export class ExportCommand extends IronfishCommand { static flags = { ...RemoteFlags, [ColorFlagKey]: ColorFlag, - local: flags.boolean({ + local: Flags.boolean({ default: false, description: 'Export an account without an online node', }), @@ -25,20 +24,20 @@ export class ExportCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'name of the account to export', }, { name: 'path', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'a path to export the account to', }, ] async start(): Promise { - const { flags, args } = this.parse(ExportCommand) + const { flags, args } = await this.parse(ExportCommand) const { color, local } = flags const account = args.account as string const exportPath = args.path as string | undefined diff --git a/ironfish-cli/src/commands/accounts/import.ts b/ironfish-cli/src/commands/accounts/import.ts index 57e9360ab7..5e3e9c7d81 100644 --- a/ironfish-cli/src/commands/accounts/import.ts +++ b/ironfish-cli/src/commands/accounts/import.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import { JSONUtils, PromiseUtils, SerializedAccount } from 'ironfish' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -12,7 +11,7 @@ export class ImportCommand extends IronfishCommand { static flags = { ...RemoteFlags, - rescan: flags.boolean({ + rescan: Flags.boolean({ allowNo: true, default: true, description: 'rescan the blockchain once the account is imported', @@ -22,14 +21,14 @@ export class ImportCommand extends IronfishCommand { static args = [ { name: 'path', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'a path to import the account from', }, ] async start(): Promise { - const { flags, args } = this.parse(ImportCommand) + const { flags, args } = await this.parse(ImportCommand) const importPath = args.path as string | undefined const client = await this.sdk.connectRpc() @@ -45,7 +44,7 @@ export class ImportCommand extends IronfishCommand { if (account === null) { this.log('No account to import provided') - this.exit(1) + return this.exit(1) } const result = await client.importAccount({ diff --git a/ironfish-cli/src/commands/accounts/list.ts b/ironfish-cli/src/commands/accounts/list.ts index 675ee8c8c7..c32c2e1397 100644 --- a/ironfish-cli/src/commands/accounts/list.ts +++ b/ironfish-cli/src/commands/accounts/list.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -10,14 +10,14 @@ export class ListCommand extends IronfishCommand { static flags = { ...RemoteFlags, - displayName: flags.boolean({ + displayName: Flags.boolean({ default: false, description: `Display a hash of the account's read-only keys along with the account name`, }), } async start(): Promise { - const { flags } = this.parse(ListCommand) + const { flags } = await this.parse(ListCommand) const client = await this.sdk.connectRpc() diff --git a/ironfish-cli/src/commands/accounts/pay.ts b/ironfish-cli/src/commands/accounts/pay.ts index 01cff613fe..b84de44a60 100644 --- a/ironfish-cli/src/commands/accounts/pay.ts +++ b/ironfish-cli/src/commands/accounts/pay.ts @@ -2,8 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import { displayIronAmountWithCurrency, ironToOre, @@ -27,31 +26,31 @@ export class Pay extends IronfishCommand { static flags = { ...RemoteFlags, - account: flags.string({ + account: Flags.string({ char: 'f', description: 'the account to send money from', }), - amount: flags.string({ + amount: Flags.string({ char: 'a', description: 'amount of coins to send', }), - to: flags.string({ + to: Flags.string({ char: 't', description: 'the public address of the recipient', }), - fee: flags.string({ + fee: Flags.string({ char: 'o', description: 'the fee amount in IRON', }), - memo: flags.string({ + memo: Flags.string({ char: 'm', description: 'the memo of transaction', }), - confirm: flags.boolean({ + confirm: Flags.boolean({ default: false, description: 'confirm without asking', }), - expirationSequence: flags.integer({ + expirationSequence: Flags.integer({ char: 'e', description: 'The block sequence after which the transaction will be removed from the mempool. Set to 0 for no expiration.', @@ -59,7 +58,7 @@ export class Pay extends IronfishCommand { } async start(): Promise { - const { flags } = this.parse(Pay) + const { flags } = await this.parse(Pay) let amount = flags.amount as unknown as number let fee = flags.fee as unknown as number let to = flags.to diff --git a/ironfish-cli/src/commands/accounts/publickey.ts b/ironfish-cli/src/commands/accounts/publickey.ts index b8e9b1642b..8077187624 100644 --- a/ironfish-cli/src/commands/accounts/publickey.ts +++ b/ironfish-cli/src/commands/accounts/publickey.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -10,7 +10,7 @@ export class PublicKeyCommand extends IronfishCommand { static flags = { ...RemoteFlags, - generate: flags.boolean({ + generate: Flags.boolean({ char: 'g', default: false, description: 'generate the public key', @@ -20,14 +20,14 @@ export class PublicKeyCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'name of the account to get a public key', }, ] async start(): Promise { - const { args, flags } = this.parse(PublicKeyCommand) + const { args, flags } = await this.parse(PublicKeyCommand) const account = args.account as string | undefined const client = await this.sdk.connectRpc() diff --git a/ironfish-cli/src/commands/accounts/remove.ts b/ironfish-cli/src/commands/accounts/remove.ts index 7eb0bd0d8f..d81a817d33 100644 --- a/ironfish-cli/src/commands/accounts/remove.ts +++ b/ironfish-cli/src/commands/accounts/remove.ts @@ -2,8 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -20,13 +19,13 @@ export class RemoveCommand extends IronfishCommand { static flags = { ...RemoteFlags, - confirm: flags.boolean({ + confirm: Flags.boolean({ description: 'suppress the confirmation prompt', }), } async start(): Promise { - const { args, flags } = this.parse(RemoveCommand) + const { args, flags } = await this.parse(RemoveCommand) const confirm = flags.confirm const name = (args.name as string).trim() diff --git a/ironfish-cli/src/commands/accounts/rescan.ts b/ironfish-cli/src/commands/accounts/rescan.ts index e3eee1d940..82f90ea73d 100644 --- a/ironfish-cli/src/commands/accounts/rescan.ts +++ b/ironfish-cli/src/commands/accounts/rescan.ts @@ -1,8 +1,8 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' + +import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' import { hasUserResponseError } from '../../utils' @@ -12,23 +12,23 @@ export class RescanCommand extends IronfishCommand { static flags = { ...RemoteFlags, - detach: flags.boolean({ + detach: Flags.boolean({ default: false, description: 'if a scan is already happening, follow that scan instead', }), - reset: flags.boolean({ + reset: Flags.boolean({ default: false, description: 'clear the in-memory and disk caches before rescanning. note that this removes all pending transactions', }), - local: flags.boolean({ + local: Flags.boolean({ default: false, description: 'Force the rescan to not connect via RPC', }), } async start(): Promise { - const { flags } = this.parse(RescanCommand) + const { flags } = await this.parse(RescanCommand) const { detach, reset, local } = flags const client = await this.sdk.connectRpc(local) diff --git a/ironfish-cli/src/commands/accounts/use.ts b/ironfish-cli/src/commands/accounts/use.ts index 63c062747a..d743b3c64e 100644 --- a/ironfish-cli/src/commands/accounts/use.ts +++ b/ironfish-cli/src/commands/accounts/use.ts @@ -20,7 +20,7 @@ export class UseCommand extends IronfishCommand { } async start(): Promise { - const { args } = this.parse(UseCommand) + const { args } = await this.parse(UseCommand) const name = (args.name as string).trim() const client = await this.sdk.connectRpc() diff --git a/ironfish-cli/src/commands/accounts/which.ts b/ironfish-cli/src/commands/accounts/which.ts index d747de2cd7..84601762ae 100644 --- a/ironfish-cli/src/commands/accounts/which.ts +++ b/ironfish-cli/src/commands/accounts/which.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -14,14 +14,14 @@ export class WhichCommand extends IronfishCommand { static flags = { ...RemoteFlags, - displayName: flags.boolean({ + displayName: Flags.boolean({ default: false, description: `Display a hash of the account's read-only keys along with the account name`, }), } async start(): Promise { - const { flags } = this.parse(WhichCommand) + const { flags } = await this.parse(WhichCommand) const client = await this.sdk.connectRpc() diff --git a/ironfish-cli/src/commands/backup.ts b/ironfish-cli/src/commands/backup.ts index ab72e64f23..d2a00e159e 100644 --- a/ironfish-cli/src/commands/backup.ts +++ b/ironfish-cli/src/commands/backup.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import { spawn } from 'child_process' import fsAsync from 'fs/promises' import { FileUtils, NodeUtils } from 'ironfish' @@ -19,12 +18,12 @@ export default class Backup extends IronfishCommand { static flags = { [VerboseFlagKey]: VerboseFlag, [DataDirFlagKey]: DataDirFlag, - lock: flags.boolean({ + lock: Flags.boolean({ default: true, allowNo: true, description: 'wait for the database to stop being used', }), - accounts: flags.boolean({ + accounts: Flags.boolean({ default: false, allowNo: true, description: 'export the accounts', @@ -40,7 +39,7 @@ export default class Backup extends IronfishCommand { ] async start(): Promise { - const { flags, args } = this.parse(Backup) + const { flags, args } = await this.parse(Backup) const bucket = (args.bucket as string).trim() let id = uuid().slice(0, 5) diff --git a/ironfish-cli/src/commands/blocks/show.ts b/ironfish-cli/src/commands/blocks/show.ts index 85b0e198c0..ce99a42cf5 100644 --- a/ironfish-cli/src/commands/blocks/show.ts +++ b/ironfish-cli/src/commands/blocks/show.ts @@ -11,7 +11,7 @@ export default class ShowBlock extends IronfishCommand { static args = [ { name: 'search', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: true, description: 'the hash or sequence of the block to look at', }, @@ -22,7 +22,7 @@ export default class ShowBlock extends IronfishCommand { } async start(): Promise { - const { args } = this.parse(ShowBlock) + const { args } = await this.parse(ShowBlock) const search = args.search as string const client = await this.sdk.connectRpc() diff --git a/ironfish-cli/src/commands/chain/export.ts b/ironfish-cli/src/commands/chain/export.ts index 27d38fec12..1c35cc38c8 100644 --- a/ironfish-cli/src/commands/chain/export.ts +++ b/ironfish-cli/src/commands/chain/export.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import fs from 'fs' import { AsyncUtils, GENESIS_BLOCK_SEQUENCE } from 'ironfish' import { parseNumber } from '../../args' @@ -15,9 +14,9 @@ export default class Export extends IronfishCommand { static flags = { ...RemoteFlags, - path: flags.string({ + path: Flags.string({ char: 'p', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, default: '../ironfish-graph-explorer/src/data.json', description: 'a path to export the chain to', @@ -27,21 +26,21 @@ export default class Export extends IronfishCommand { static args = [ { name: 'start', - parse: parseNumber, + parse: (input: string): Promise => Promise.resolve(parseNumber(input)), default: Number(GENESIS_BLOCK_SEQUENCE), required: false, description: 'the sequence to start at (inclusive, genesis block is 1)', }, { name: 'stop', - parse: parseNumber, + parse: (input: string): Promise => Promise.resolve(parseNumber(input)), required: false, description: 'the sequence to end at (inclusive)', }, ] async start(): Promise { - const { flags, args } = this.parse(Export) + const { flags, args } = await this.parse(Export) const path = this.sdk.fileSystem.resolve(flags.path) const client = await this.sdk.connectRpc() diff --git a/ironfish-cli/src/commands/chain/forks.ts b/ironfish-cli/src/commands/chain/forks.ts index 3bd1a702d7..c62899ae7a 100644 --- a/ironfish-cli/src/commands/chain/forks.ts +++ b/ironfish-cli/src/commands/chain/forks.ts @@ -17,7 +17,7 @@ export default class ForksCommand extends IronfishCommand { } async start(): Promise { - this.parse(ForksCommand) + await this.parse(ForksCommand) this.logger.pauseLogs() let connected = false diff --git a/ironfish-cli/src/commands/chain/genesisblock.ts b/ironfish-cli/src/commands/chain/genesisblock.ts index beaa38aedf..90c3357564 100644 --- a/ironfish-cli/src/commands/chain/genesisblock.ts +++ b/ironfish-cli/src/commands/chain/genesisblock.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import { GenesisBlockInfo, IJSON, makeGenesisBlock } from 'ironfish' import { IronfishCommand } from '../../command' import { LocalFlags } from '../../flags' @@ -13,19 +13,19 @@ export default class GenesisBlockCommand extends IronfishCommand { static flags = { ...LocalFlags, - account: flags.string({ + account: Flags.string({ char: 'a', required: false, default: 'IronFishGenesisAccount', description: 'the name of the account to use for keys to assign the genesis block to', }), - coins: flags.integer({ + coins: Flags.integer({ char: 'c', required: false, default: 4200000000000000, description: 'The amount of coins to generate', }), - memo: flags.string({ + memo: Flags.string({ char: 'm', required: false, default: 'Genesis Block', @@ -34,7 +34,7 @@ export default class GenesisBlockCommand extends IronfishCommand { } async start(): Promise { - const { flags } = this.parse(GenesisBlockCommand) + const { flags } = await this.parse(GenesisBlockCommand) const node = await this.sdk.node({ autoSeed: false }) await node.openDB() diff --git a/ironfish-cli/src/commands/chain/readdblock.ts b/ironfish-cli/src/commands/chain/readdblock.ts index c9be5f3644..50adc7846c 100644 --- a/ironfish-cli/src/commands/chain/readdblock.ts +++ b/ironfish-cli/src/commands/chain/readdblock.ts @@ -18,14 +18,14 @@ export default class ReAddBlock extends IronfishCommand { static args = [ { name: 'hash', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: true, description: 'the hash of the block in hex format', }, ] async start(): Promise { - const { args } = this.parse(ReAddBlock) + const { args } = await this.parse(ReAddBlock) const hash = Buffer.from(args.hash as string, 'hex') CliUx.ux.action.start(`Opening node`) @@ -38,7 +38,7 @@ export default class ReAddBlock extends IronfishCommand { if (!block) { this.log(`No block found with hash ${hash.toString('hex')}`) - this.exit(0) + return this.exit(0) } await node.chain.removeBlock(hash) diff --git a/ironfish-cli/src/commands/chain/repair.ts b/ironfish-cli/src/commands/chain/repair.ts index 9f97e76966..9424a85bf9 100644 --- a/ironfish-cli/src/commands/chain/repair.ts +++ b/ironfish-cli/src/commands/chain/repair.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import { Assert, BlockHeader, IDatabaseTransaction, IronfishNode, TimeUtils } from 'ironfish' import { Meter } from 'ironfish' import { IronfishCommand } from '../../command' @@ -21,12 +20,12 @@ export default class RepairChain extends IronfishCommand { static flags = { ...LocalFlags, - confirm: flags.boolean({ + confirm: Flags.boolean({ char: 'c', default: false, description: 'force confirmation to repair', }), - force: flags.boolean({ + force: Flags.boolean({ char: 'f', default: false, description: 'force merkle tree reconstruction', @@ -34,7 +33,7 @@ export default class RepairChain extends IronfishCommand { } async start(): Promise { - const { flags } = this.parse(RepairChain) + const { flags } = await this.parse(RepairChain) const speed = new Meter() const progress = CliUx.ux.progress({ @@ -184,7 +183,7 @@ export default class RepairChain extends IronfishCommand { `\nDelete your database at ${node.config.chainDatabasePath}\n` this.log(error) - this.exit(1) + return this.exit(1) } done++ diff --git a/ironfish-cli/src/commands/chain/show.ts b/ironfish-cli/src/commands/chain/show.ts index 67487cac82..6d55604492 100644 --- a/ironfish-cli/src/commands/chain/show.ts +++ b/ironfish-cli/src/commands/chain/show.ts @@ -15,21 +15,21 @@ export default class Show extends IronfishCommand { static args = [ { name: 'start', - parse: parseNumber, + parse: (input: string): Promise => Promise.resolve(parseNumber(input)), default: -50, required: false, description: 'the sequence to start at (inclusive, genesis block is 1)', }, { name: 'stop', - parse: parseNumber, + parse: (input: string): Promise => Promise.resolve(parseNumber(input)), required: false, description: 'the sequence to end at (inclusive)', }, ] async start(): Promise { - const { args } = this.parse(Show) + const { args } = await this.parse(Show) const start = args.start as number | null const stop = args.stop as number | null diff --git a/ironfish-cli/src/commands/config/edit.ts b/ironfish-cli/src/commands/config/edit.ts index 970ef5db8c..ff0a4eb44d 100644 --- a/ironfish-cli/src/commands/config/edit.ts +++ b/ironfish-cli/src/commands/config/edit.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import { mkdtemp, readFile, writeFile } from 'fs' import { DEFAULT_CONFIG_NAME, JSONUtils } from 'ironfish' import os from 'os' @@ -23,14 +23,14 @@ export class EditCommand extends IronfishCommand { static flags = { [ConfigFlagKey]: ConfigFlag, [DataDirFlagKey]: DataDirFlag, - remote: flags.boolean({ + remote: Flags.boolean({ default: false, description: 'connect to the node when editing the config', }), } async start(): Promise { - const { flags } = this.parse(EditCommand) + const { flags } = await this.parse(EditCommand) if (!flags.remote) { const configPath = this.sdk.config.storage.configPath diff --git a/ironfish-cli/src/commands/config/get.ts b/ironfish-cli/src/commands/config/get.ts index 2b1def1cb0..ead388cee4 100644 --- a/ironfish-cli/src/commands/config/get.ts +++ b/ironfish-cli/src/commands/config/get.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import { ConfigOptions } from 'ironfish' import jsonColorizer from 'json-colorizer' import { IronfishCommand } from '../../command' @@ -13,7 +13,7 @@ export class GetCommand extends IronfishCommand { static args = [ { name: 'name', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: true, description: 'name of the config item', }, @@ -21,14 +21,14 @@ export class GetCommand extends IronfishCommand { static flags = { ...RemoteFlags, - user: flags.boolean({ + user: Flags.boolean({ description: 'only show config from the users datadir and not overrides', }), - local: flags.boolean({ + local: Flags.boolean({ default: false, description: 'dont connect to the node when displaying the config', }), - color: flags.boolean({ + color: Flags.boolean({ default: true, allowNo: true, description: 'should colorize the output', @@ -36,7 +36,7 @@ export class GetCommand extends IronfishCommand { } async start(): Promise { - const { args, flags } = this.parse(GetCommand) + const { args, flags } = await this.parse(GetCommand) const name = (args.name as string).trim() const client = await this.sdk.connectRpc(flags.local) diff --git a/ironfish-cli/src/commands/config/set.ts b/ironfish-cli/src/commands/config/set.ts index f646eeea75..1e4e9aa00f 100644 --- a/ironfish-cli/src/commands/config/set.ts +++ b/ironfish-cli/src/commands/config/set.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -11,13 +11,13 @@ export class SetCommand extends IronfishCommand { static args = [ { name: 'name', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: true, description: 'name of the config item', }, { name: 'value', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: true, description: 'value of the config item', }, @@ -25,7 +25,7 @@ export class SetCommand extends IronfishCommand { static flags = { ...RemoteFlags, - local: flags.boolean({ + local: Flags.boolean({ default: false, description: 'dont connect to the node when updating the config', }), @@ -36,7 +36,7 @@ export class SetCommand extends IronfishCommand { ] async start(): Promise { - const { args, flags } = this.parse(SetCommand) + const { args, flags } = await this.parse(SetCommand) const name = args.name as string const value = args.value as string diff --git a/ironfish-cli/src/commands/config/show.ts b/ironfish-cli/src/commands/config/show.ts index 84d8ee396f..4f0e4271f5 100644 --- a/ironfish-cli/src/commands/config/show.ts +++ b/ironfish-cli/src/commands/config/show.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import jsonColorizer from 'json-colorizer' import { IronfishCommand } from '../../command' import { ColorFlag, ColorFlagKey } from '../../flags' @@ -13,17 +13,17 @@ export class ShowCommand extends IronfishCommand { static flags = { ...RemoteFlags, [ColorFlagKey]: ColorFlag, - user: flags.boolean({ + user: Flags.boolean({ description: 'only show config from the users datadir and not overrides', }), - local: flags.boolean({ + local: Flags.boolean({ default: false, description: 'dont connect to the node when displaying the config', }), } async start(): Promise { - const { flags } = this.parse(ShowCommand) + const { flags } = await this.parse(ShowCommand) const client = await this.sdk.connectRpc(flags.local) const response = await client.getConfig({ user: flags.user }) diff --git a/ironfish-cli/src/commands/faucet.ts b/ironfish-cli/src/commands/faucet.ts index b3edf01822..bb051759be 100644 --- a/ironfish-cli/src/commands/faucet.ts +++ b/ironfish-cli/src/commands/faucet.ts @@ -2,8 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import { DEFAULT_DISCORD_INVITE, RequestError } from 'ironfish' import { IronfishCommand } from '../command' import { RemoteFlags } from '../flags' @@ -16,18 +15,18 @@ export class FaucetCommand extends IronfishCommand { static flags = { ...RemoteFlags, - force: flags.boolean({ + force: Flags.boolean({ default: false, description: 'Force the faucet to try to give you coins even if its disabled', }), - email: flags.string({ + email: Flags.string({ hidden: true, description: 'Email to use to get funds', }), } async start(): Promise { - const { flags } = this.parse(FaucetCommand) + const { flags } = await this.parse(FaucetCommand) if (FAUCET_DISABLED && !flags.force) { this.log(`❌ The faucet is currently disabled. Check ${DEFAULT_DISCORD_INVITE} ❌`) diff --git a/ironfish-cli/src/commands/logs.ts b/ironfish-cli/src/commands/logs.ts index 8659e5d04e..09a189093d 100644 --- a/ironfish-cli/src/commands/logs.ts +++ b/ironfish-cli/src/commands/logs.ts @@ -16,7 +16,7 @@ export default class LogsCommand extends IronfishCommand { node: IronfishNode | null = null async start(): Promise { - this.parse(LogsCommand) + await this.parse(LogsCommand) await this.sdk.client.connect() diff --git a/ironfish-cli/src/commands/miners/mined.ts b/ironfish-cli/src/commands/miners/mined.ts index 1f65a8fbab..697f2c8fdb 100644 --- a/ironfish-cli/src/commands/miners/mined.ts +++ b/ironfish-cli/src/commands/miners/mined.ts @@ -27,21 +27,21 @@ export class MinedCommand extends IronfishCommand { static args = [ { name: 'start', - parse: parseNumber, + parse: (input: string): Promise => Promise.resolve(parseNumber(input)), default: Number(GENESIS_BLOCK_SEQUENCE), required: false, description: 'the sequence to start at (inclusive, genesis block is 1)', }, { name: 'stop', - parse: parseNumber, + parse: (input: string): Promise => Promise.resolve(parseNumber(input)), required: false, description: 'the sequence to end at (inclusive)', }, ] async start(): Promise { - const { args } = this.parse(MinedCommand) + const { args } = await this.parse(MinedCommand) const client = await this.sdk.connectRpc() this.log('Scanning for mined blocks...') diff --git a/ironfish-cli/src/commands/miners/start.ts b/ironfish-cli/src/commands/miners/start.ts index 0c3672f10a..24dbb05d04 100644 --- a/ironfish-cli/src/commands/miners/start.ts +++ b/ironfish-cli/src/commands/miners/start.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import { AsyncUtils, FileUtils, @@ -20,7 +19,7 @@ export class Miner extends IronfishCommand { static flags = { ...RemoteFlags, - threads: flags.integer({ + threads: Flags.integer({ char: 't', default: 1, description: @@ -29,7 +28,7 @@ export class Miner extends IronfishCommand { } async start(): Promise { - const { flags } = this.parse(Miner) + const { flags } = await this.parse(Miner) if (flags.threads === 0 || flags.threads < -1) { throw new Error('--threads must be a positive integer or -1.') diff --git a/ironfish-cli/src/commands/peers/list.ts b/ironfish-cli/src/commands/peers/list.ts index 2d81af8958..392b5768ce 100644 --- a/ironfish-cli/src/commands/peers/list.ts +++ b/ironfish-cli/src/commands/peers/list.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import blessed from 'blessed' import { GetPeersResponse, PromiseUtils } from 'ironfish' import { IronfishCommand } from '../../command' @@ -17,36 +16,36 @@ export class ListCommand extends IronfishCommand { static flags = { ...RemoteFlags, - follow: flags.boolean({ + follow: Flags.boolean({ char: 'f', default: false, description: 'follow the peers list live', }), - all: flags.boolean({ + all: Flags.boolean({ default: false, description: 'show all peers, not just connected peers', }), - extended: flags.boolean({ + extended: Flags.boolean({ char: 'e', default: false, description: 'display all information', }), - sort: flags.string({ + sort: Flags.string({ char: 'o', default: STATE_COLUMN_HEADER, description: 'sort by column header', }), - agents: flags.boolean({ + agents: Flags.boolean({ char: 'a', default: false, description: 'display peer agents', }), - sequence: flags.boolean({ + sequence: Flags.boolean({ char: 's', default: false, description: 'display peer head sequence', }), - names: flags.boolean({ + names: Flags.boolean({ char: 'n', default: false, description: 'display node names', @@ -55,7 +54,7 @@ export class ListCommand extends IronfishCommand { } async start(): Promise { - const { flags } = this.parse(ListCommand) + const { flags } = await this.parse(ListCommand) if (!flags.follow) { await this.sdk.client.connect() diff --git a/ironfish-cli/src/commands/peers/show.ts b/ironfish-cli/src/commands/peers/show.ts index e7e9a32b9e..d49be81393 100644 --- a/ironfish-cli/src/commands/peers/show.ts +++ b/ironfish-cli/src/commands/peers/show.ts @@ -25,7 +25,7 @@ export class ShowCommand extends IronfishCommand { } async start(): Promise { - const { args } = this.parse(ShowCommand) + const { args } = await this.parse(ShowCommand) const identity = (args.identity as string).trim() @@ -37,7 +37,7 @@ export class ShowCommand extends IronfishCommand { if (peer.content.peer === null) { this.log(`No peer found containing identity '${identity}'.`) - this.exit(1) + return this.exit(1) } this.log(this.renderPeer(peer.content.peer)) diff --git a/ironfish-cli/src/commands/reset.ts b/ironfish-cli/src/commands/reset.ts index f386abc6d0..07e86a88cc 100644 --- a/ironfish-cli/src/commands/reset.ts +++ b/ironfish-cli/src/commands/reset.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import fs from 'fs' import fsAsync from 'fs/promises' import { IronfishNode, NodeUtils, PeerNetwork } from 'ironfish' @@ -27,7 +26,7 @@ export default class Reset extends IronfishCommand { [ConfigFlagKey]: ConfigFlag, [DataDirFlagKey]: DataDirFlag, [DatabaseFlagKey]: DatabaseFlag, - confirm: flags.boolean({ + confirm: Flags.boolean({ default: false, description: 'confirm without asking', }), @@ -38,7 +37,7 @@ export default class Reset extends IronfishCommand { peerNetwork: PeerNetwork | null = null async start(): Promise { - const { flags } = this.parse(Reset) + const { flags } = await this.parse(Reset) let node = await this.sdk.node({ autoSeed: false }) await NodeUtils.waitForOpen(node, null, { upgrade: false, load: false }) diff --git a/ironfish-cli/src/commands/restore.ts b/ironfish-cli/src/commands/restore.ts index 261184accc..3e9d5a9c15 100644 --- a/ironfish-cli/src/commands/restore.ts +++ b/ironfish-cli/src/commands/restore.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import axios from 'axios' import { spawn } from 'child_process' import fs from 'fs' @@ -23,7 +22,7 @@ export default class Restore extends IronfishCommand { static flags = { [VerboseFlagKey]: VerboseFlag, [DataDirFlagKey]: DataDirFlag, - lock: flags.boolean({ + lock: Flags.boolean({ default: true, allowNo: true, description: 'wait for the database to stop being used', @@ -44,7 +43,7 @@ export default class Restore extends IronfishCommand { ] async start(): Promise { - const { flags, args } = this.parse(Restore) + const { flags, args } = await this.parse(Restore) const bucket = (args.bucket as string).trim() let name = (args.name as string).trim() diff --git a/ironfish-cli/src/commands/service/faucet.ts b/ironfish-cli/src/commands/service/faucet.ts index 5001be599f..e66040e94d 100644 --- a/ironfish-cli/src/commands/service/faucet.ts +++ b/ironfish-cli/src/commands/service/faucet.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import { ConnectionError, IronfishIpcClient, Meter, PromiseUtils, WebApi } from 'ironfish' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -19,15 +19,15 @@ export default class Faucet extends IronfishCommand { static flags = { ...RemoteFlags, - api: flags.string({ + api: Flags.string({ char: 'a', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'API host to sync to', }), - token: flags.string({ + token: Flags.string({ char: 't', - parse: (input: string): string => input.trim(), + parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'API host token to authenticate with', }), @@ -36,7 +36,7 @@ export default class Faucet extends IronfishCommand { warnedFund = false async start(): Promise { - const { flags } = this.parse(Faucet) + const { flags } = await this.parse(Faucet) const apiHost = (flags.api || process.env.IRONFISH_API_HOST || '').trim() const apiToken = (flags.token || process.env.IRONFISH_API_TOKEN || '').trim() diff --git a/ironfish-cli/src/commands/service/sync.ts b/ironfish-cli/src/commands/service/sync.ts index 2ff0ad1728..bbd527ae93 100644 --- a/ironfish-cli/src/commands/service/sync.ts +++ b/ironfish-cli/src/commands/service/sync.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import { FollowChainStreamResponse, Meter, TimeUtils, WebApi } from 'ironfish' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -19,15 +19,15 @@ export default class Sync extends IronfishCommand { static flags = { ...RemoteFlags, - endpoint: flags.string({ + endpoint: Flags.string({ char: 'e', - parse: (input: string): string => input.trim(), + parse: (input: string) => Promise.resolve(input.trim()), required: false, description: 'API host to sync to', }), - token: flags.string({ + token: Flags.string({ char: 'e', - parse: (input: string): string => input.trim(), + parse: (input: string) => Promise.resolve(input.trim()), required: false, description: 'API host token to authenticate with', }), @@ -42,7 +42,7 @@ export default class Sync extends IronfishCommand { ] async start(): Promise { - const { flags, args } = this.parse(Sync) + const { flags, args } = await this.parse(Sync) const apiHost = (flags.endpoint || process.env.IRONFISH_API_HOST || '').trim() const apiToken = (flags.token || process.env.IRONFISH_API_TOKEN || '').trim() diff --git a/ironfish-cli/src/commands/start.ts b/ironfish-cli/src/commands/start.ts index 28b5a5291c..05a98ffe92 100644 --- a/ironfish-cli/src/commands/start.ts +++ b/ironfish-cli/src/commands/start.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import { Assert, IronfishNode, NodeUtils, PrivateIdentity, PromiseUtils } from 'ironfish' import tweetnacl from 'tweetnacl' import { v4 as uuid } from 'uuid' @@ -43,46 +43,46 @@ export default class Start extends IronfishCommand { [RpcTcpHostFlagKey]: RpcTcpHostFlag, [RpcTcpPortFlagKey]: RpcTcpPortFlag, [RpcTcpSecureFlagKey]: RpcTcpSecureFlag, - bootstrap: flags.string({ + bootstrap: Flags.string({ char: 'b', description: 'comma-separated addresses of bootstrap nodes to connect to', multiple: true, }), - port: flags.integer({ + port: Flags.integer({ char: 'p', description: 'port to run the local ws server on', }), - workers: flags.integer({ + workers: Flags.integer({ description: 'number of CPU workers to use for long-running operations. 0 disables (likely to cause performance issues), -1 auto-detects based on CPU cores', }), - graffiti: flags.string({ + graffiti: Flags.string({ char: 'g', default: undefined, description: 'Set the graffiti for the node', }), - name: flags.string({ + name: Flags.string({ char: 'n', description: 'name for the node', hidden: true, }), - listen: flags.boolean({ + listen: Flags.boolean({ allowNo: true, default: undefined, description: 'disable the web socket listen server', hidden: true, }), - forceMining: flags.boolean({ + forceMining: Flags.boolean({ default: undefined, description: 'force mining even if we are not synced', hidden: true, }), - logPeerMessages: flags.boolean({ + logPeerMessages: Flags.boolean({ default: undefined, description: 'track all messages sent and received by peers', hidden: true, }), - generateNewIdentity: flags.boolean({ + generateNewIdentity: Flags.boolean({ default: false, description: 'genereate new identity for each new start', hidden: true, @@ -103,7 +103,7 @@ export default class Start extends IronfishCommand { const [startDonePromise, startDoneResolve] = PromiseUtils.split() this.startDonePromise = startDonePromise - const { flags } = this.parse(Start) + const { flags } = await this.parse(Start) const { bootstrap, forceMining, diff --git a/ironfish-cli/src/commands/status.ts b/ironfish-cli/src/commands/status.ts index b31785c2b6..b9785ccbaa 100644 --- a/ironfish-cli/src/commands/status.ts +++ b/ironfish-cli/src/commands/status.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import blessed from 'blessed' import { FileUtils, GetStatusResponse, PromiseUtils } from 'ironfish' import { Assert } from 'ironfish' @@ -13,7 +13,7 @@ export default class Status extends IronfishCommand { static flags = { ...RemoteFlags, - follow: flags.boolean({ + follow: Flags.boolean({ char: 'f', default: false, description: 'follow the status of the node live', @@ -21,7 +21,7 @@ export default class Status extends IronfishCommand { } async start(): Promise { - const { flags } = this.parse(Status) + const { flags } = await this.parse(Status) if (!flags.follow) { const client = await this.sdk.connectRpc() diff --git a/ironfish-cli/src/commands/stop.ts b/ironfish-cli/src/commands/stop.ts index 58b1043863..229b1c0916 100644 --- a/ironfish-cli/src/commands/stop.ts +++ b/ironfish-cli/src/commands/stop.ts @@ -15,7 +15,7 @@ export default class StopCommand extends IronfishCommand { node: IronfishNode | null = null async start(): Promise { - this.parse(StopCommand) + await this.parse(StopCommand) await this.sdk.client.connect({ retryConnect: false }).catch((e) => { if (e instanceof ConnectionError) { diff --git a/ironfish-cli/src/commands/swim.ts b/ironfish-cli/src/commands/swim.ts index 6c746a93bc..5501ce6945 100644 --- a/ironfish-cli/src/commands/swim.ts +++ b/ironfish-cli/src/commands/swim.ts @@ -12,7 +12,7 @@ export default class SwimCommand extends IronfishCommand { static hidden = true async start(): Promise { - this.parse(SwimCommand) + await this.parse(SwimCommand) const images = [ONE_FISH_IMAGE, TWO_FISH_IMAGE] const image = images[Math.round(Math.random() * (images.length - 1))] diff --git a/ironfish-cli/src/commands/testnet.ts b/ironfish-cli/src/commands/testnet.ts index 25bc5ebce5..15b74f69c6 100644 --- a/ironfish-cli/src/commands/testnet.ts +++ b/ironfish-cli/src/commands/testnet.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { CliUx } from '@oclif/core' +import { CliUx, Flags } from '@oclif/core' import { WebApi } from 'ironfish' import { IronfishCommand } from '../command' import { DataDirFlag, DataDirFlagKey, VerboseFlag, VerboseFlagKey } from '../flags' @@ -14,15 +13,15 @@ export default class Testnet extends IronfishCommand { static flags = { [VerboseFlagKey]: VerboseFlag, [DataDirFlagKey]: DataDirFlag, - confirm: flags.boolean({ + confirm: Flags.boolean({ default: false, description: 'confirm without asking', }), - skipName: flags.boolean({ + skipName: Flags.boolean({ default: false, description: "Don't update your node name", }), - skipGraffiti: flags.boolean({ + skipGraffiti: Flags.boolean({ default: false, description: "Don't update your graffiti", }), @@ -38,7 +37,7 @@ export default class Testnet extends IronfishCommand { ] async start(): Promise { - const { flags, args } = this.parse(Testnet) + const { flags, args } = await this.parse(Testnet) let userArg = ((args.user as string | undefined) || '').trim() if (!userArg) { @@ -64,7 +63,7 @@ export default class Testnet extends IronfishCommand { if (userId === null) { this.log(`Could not figure out testnet user id from ${userArg}`) - this.exit(1) + return this.exit(1) } // request user from API @@ -75,7 +74,7 @@ export default class Testnet extends IronfishCommand { if (!user) { this.log(`Could not find a user with id ${userId}`) - this.exit(1) + return this.exit(1) } this.log('') diff --git a/ironfish-cli/src/commands/workers/status.ts b/ironfish-cli/src/commands/workers/status.ts index ebf6cc623c..c47766c986 100644 --- a/ironfish-cli/src/commands/workers/status.ts +++ b/ironfish-cli/src/commands/workers/status.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' +import { Flags } from '@oclif/core' import blessed from 'blessed' import { GetWorkersStatusResponse, PromiseUtils } from 'ironfish' import { IronfishCommand } from '../../command' @@ -12,7 +12,7 @@ export default class Status extends IronfishCommand { static flags = { ...RemoteFlags, - follow: flags.boolean({ + follow: Flags.boolean({ char: 'f', default: false, description: 'follow the status of the node live', @@ -20,7 +20,7 @@ export default class Status extends IronfishCommand { } async start(): Promise { - const { flags } = this.parse(Status) + const { flags } = await this.parse(Status) if (!flags.follow) { const client = await this.sdk.connectRpc() diff --git a/ironfish-cli/src/flags.ts b/ironfish-cli/src/flags.ts index ea7f7914a1..059be218ce 100644 --- a/ironfish-cli/src/flags.ts +++ b/ironfish-cli/src/flags.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { flags } from '@oclif/command' -import { IOptionFlag } from '@oclif/command/lib/flags' +import { Flags, Interfaces } from '@oclif/core' import { DEFAULT_CONFIG_NAME, DEFAULT_DATA_DIR, @@ -11,6 +10,8 @@ import { DEFAULT_USE_RPC_TCP, } from 'ironfish' +type CompletableOptionFlag = Interfaces.CompletableOptionFlag + export const VerboseFlagKey = 'verbose' export const ConfigFlagKey = 'config' export const ColorFlagKey = 'color' @@ -22,62 +23,62 @@ export const RpcTcpHostFlagKey = 'rpc.tcp.host' export const RpcTcpPortFlagKey = 'rpc.tcp.port' export const RpcTcpSecureFlagKey = 'rpc.tcp.secure' -export const VerboseFlag = flags.boolean({ +export const VerboseFlag = Flags.boolean({ char: 'v', default: false, description: 'set logging level to verbose', }) -export const ColorFlag = flags.boolean({ +export const ColorFlag = Flags.boolean({ default: true, allowNo: true, description: 'should colorize the output', }) -export const ConfigFlag = flags.string({ +export const ConfigFlag = Flags.string({ default: DEFAULT_CONFIG_NAME, description: 'the name of the config file to use', }) -export const DataDirFlag = flags.string({ +export const DataDirFlag = Flags.string({ default: DEFAULT_DATA_DIR, description: 'the path to the data dir', }) -export const DatabaseFlag = flags.string({ +export const DatabaseFlag = Flags.string({ char: 'd', default: DEFAULT_DATABASE_NAME, description: 'the name of the database to use', }) -export const RpcUseIpcFlag = flags.boolean({ +export const RpcUseIpcFlag = Flags.boolean({ default: DEFAULT_USE_RPC_IPC, description: 'connect to the RPC over IPC (default)', }) -export const RpcUseTcpFlag = flags.boolean({ +export const RpcUseTcpFlag = Flags.boolean({ default: DEFAULT_USE_RPC_TCP, description: 'connect to the RPC over TCP', }) -export const RpcTcpHostFlag = flags.string({ +export const RpcTcpHostFlag = Flags.string({ description: 'the TCP host to listen for connections on', }) -export const RpcTcpPortFlag = flags.integer({ +export const RpcTcpPortFlag = Flags.integer({ description: 'the TCP port to listen for connections on', }) -export const RpcTcpSecureFlag = flags.boolean({ +export const RpcTcpSecureFlag = Flags.boolean({ default: false, description: 'allow sensitive config to be changed over TCP', }) -const localFlags: Record> = {} -localFlags[VerboseFlagKey] = VerboseFlag as unknown as IOptionFlag -localFlags[ConfigFlagKey] = ConfigFlag as unknown as IOptionFlag -localFlags[DataDirFlagKey] = DataDirFlag as unknown as IOptionFlag -localFlags[DatabaseFlagKey] = DatabaseFlag as unknown as IOptionFlag +const localFlags: Record = {} +localFlags[VerboseFlagKey] = VerboseFlag as unknown as CompletableOptionFlag +localFlags[ConfigFlagKey] = ConfigFlag as unknown as CompletableOptionFlag +localFlags[DataDirFlagKey] = DataDirFlag as unknown as CompletableOptionFlag +localFlags[DatabaseFlagKey] = DatabaseFlag as unknown as CompletableOptionFlag /** * These flags should usually be used on any command that starts a node, @@ -85,15 +86,15 @@ localFlags[DatabaseFlagKey] = DatabaseFlag as unknown as IOptionFlag */ export const LocalFlags = localFlags -const remoteFlags: Record> = {} -remoteFlags[VerboseFlagKey] = VerboseFlag as unknown as IOptionFlag -remoteFlags[ConfigFlagKey] = ConfigFlag as unknown as IOptionFlag -remoteFlags[DataDirFlagKey] = DataDirFlag as unknown as IOptionFlag -remoteFlags[RpcUseTcpFlagKey] = RpcUseTcpFlag as unknown as IOptionFlag -remoteFlags[RpcUseIpcFlagKey] = RpcUseIpcFlag as unknown as IOptionFlag -remoteFlags[RpcTcpHostFlagKey] = RpcTcpHostFlag as unknown as IOptionFlag -remoteFlags[RpcTcpPortFlagKey] = RpcTcpPortFlag as unknown as IOptionFlag -remoteFlags[RpcTcpSecureFlagKey] = RpcTcpSecureFlag as unknown as IOptionFlag +const remoteFlags: Record = {} +remoteFlags[VerboseFlagKey] = VerboseFlag as unknown as CompletableOptionFlag +remoteFlags[ConfigFlagKey] = ConfigFlag as unknown as CompletableOptionFlag +remoteFlags[DataDirFlagKey] = DataDirFlag as unknown as CompletableOptionFlag +remoteFlags[RpcUseTcpFlagKey] = RpcUseTcpFlag as unknown as CompletableOptionFlag +remoteFlags[RpcUseIpcFlagKey] = RpcUseIpcFlag as unknown as CompletableOptionFlag +remoteFlags[RpcTcpHostFlagKey] = RpcTcpHostFlag as unknown as CompletableOptionFlag +remoteFlags[RpcTcpPortFlagKey] = RpcTcpPortFlag as unknown as CompletableOptionFlag +remoteFlags[RpcTcpSecureFlagKey] = RpcTcpSecureFlag as unknown as CompletableOptionFlag /** * These flags should usually be used on any command that uses an diff --git a/ironfish-cli/src/hooks/version.ts b/ironfish-cli/src/hooks/version.ts index dd83da866c..ca5825a19c 100644 --- a/ironfish-cli/src/hooks/version.ts +++ b/ironfish-cli/src/hooks/version.ts @@ -3,11 +3,12 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /* eslint-disable no-console */ -import { Hook } from '@oclif/config' +import { Hook } from '@oclif/core' import { Platform } from 'ironfish' import { IronfishCliPKG } from '../package' -const VersionHook: Hook<'init'> = (): void => { +// eslint-disable-next-line @typescript-eslint/require-await +const VersionHook: Hook<'init'> = async () => { const isVersionCmd = process.argv[2] === 'version' const hasDashVersion = process.argv.some((a) => a === '--version') const showVersion = isVersionCmd || hasDashVersion diff --git a/ironfish-cli/src/index.ts b/ironfish-cli/src/index.ts index 72a07e4759..4c9dba066e 100644 --- a/ironfish-cli/src/index.ts +++ b/ironfish-cli/src/index.ts @@ -1,4 +1,4 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -export { run } from '@oclif/command' +export { run } from '@oclif/core' diff --git a/yarn.lock b/yarn.lock index 5f907b9303..51b89f10be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1399,7 +1399,7 @@ supports-color "^5.4.0" tslib "^1" -"@oclif/command@1.8.0", "@oclif/command@^1.5.10", "@oclif/command@^1.5.20", "@oclif/command@^1.6", "@oclif/command@^1.6.0", "@oclif/command@^1.8.0": +"@oclif/command@^1.5.10", "@oclif/command@^1.5.20", "@oclif/command@^1.6", "@oclif/command@^1.6.0", "@oclif/command@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.8.0.tgz#c1a499b10d26e9d1a611190a81005589accbb339" integrity sha512-5vwpq6kbvwkQwKqAoOU3L72GZ3Ta8RRrewKj9OJRolx28KLJJ8Dg9Rf7obRwt5jQA9bkYd8gqzMTrI7H3xLfaw== @@ -1411,7 +1411,7 @@ debug "^4.1.1" semver "^7.3.2" -"@oclif/config@1.17.0", "@oclif/config@^1.12.6", "@oclif/config@^1.12.8", "@oclif/config@^1.15.1", "@oclif/config@^1.17.0": +"@oclif/config@^1.12.6", "@oclif/config@^1.12.8", "@oclif/config@^1.15.1", "@oclif/config@^1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.17.0.tgz#ba8639118633102a7e481760c50054623d09fcab" integrity sha512-Lmfuf6ubjQ4ifC/9bz1fSCHc6F6E653oyaRXxg+lgT4+bYf9bk+nqrUpAbrXyABkCqgIBiFr3J4zR/kiFdE1PA== From ed882a5dc959f85637c06eb3e28880548aee594f Mon Sep 17 00:00:00 2001 From: wd021 Date: Sat, 12 Feb 2022 06:15:47 +0000 Subject: [PATCH 14/24] Fix Issue #875 (#974) * add default en-US to fix localization error * add displayLocale() to currency utils test - need to use toLocaleString() in currency display tests. otherwise we'll run in to localization errors. * fix lint issue --- ironfish/src/utils/currency.test.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/ironfish/src/utils/currency.test.ts b/ironfish/src/utils/currency.test.ts index 4fcb49443d..2e2ae4bee8 100644 --- a/ironfish/src/utils/currency.test.ts +++ b/ironfish/src/utils/currency.test.ts @@ -5,16 +5,31 @@ import { displayIronAmountWithCurrency, ironToOre, isValidAmount, oreToIron } fr describe('Currency utils', () => { test('displayIronAmountWithCurrency returns the right string', () => { - expect(displayIronAmountWithCurrency(0.00000002, true)).toEqual('$IRON 0.00000002 ($ORE 2)') - expect(displayIronAmountWithCurrency(0.0000001, true)).toEqual('$IRON 0.00000010 ($ORE 10)') - expect(displayIronAmountWithCurrency(0, true)).toEqual('$IRON 0.00000000 ($ORE 0)') + const displayLocale = (value: string, decimals: number) => { + return parseFloat(value).toLocaleString(undefined, { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }) + } + + expect(displayIronAmountWithCurrency(0.00000002, true)).toEqual( + `$IRON ${displayLocale('0.00000002', 8)} ($ORE ${displayLocale('2', 0)})`, + ) + expect(displayIronAmountWithCurrency(0.0000001, true)).toEqual( + `$IRON ${displayLocale('0.00000010', 8)} ($ORE ${displayLocale('10', 0)})`, + ) + expect(displayIronAmountWithCurrency(0, true)).toEqual( + `$IRON ${displayLocale('0.00000000', 8)} ($ORE ${displayLocale('0', 0)})`, + ) expect(displayIronAmountWithCurrency(1, true)).toEqual( - '$IRON 1.00000000 ($ORE 100,000,000)', + `$IRON ${displayLocale('1.00000000', 8)} ($ORE ${displayLocale('100000000', 0)})`, ) expect(displayIronAmountWithCurrency(100, true)).toEqual( - '$IRON 100.00000000 ($ORE 10,000,000,000)', + `$IRON ${displayLocale('100.00000000', 8)} ($ORE ${displayLocale('10000000000', 0)})`, + ) + expect(displayIronAmountWithCurrency(100, false)).toEqual( + `$IRON ${displayLocale('100.00000000', 8)}`, ) - expect(displayIronAmountWithCurrency(100, false)).toEqual('$IRON 100.00000000') }) test('isValidAmount returns the right value', () => { From 239f9a49d1925a2f372681e0eb720794aef530d6 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 11 Feb 2022 22:48:35 -0800 Subject: [PATCH 15/24] Improve usability around telemetry (#987) * Improve usability around telemetry - Properly prints the right command to enable telemetry - Ties the code to a variable so that there will be a build error if the key changes - Optimize first run so it doesn't need to spin up the RPC layer just to set commands - Make testnet command ask user to opt into telemetry * Fix test * Rebase on staging Co-authored-by: Derek Guenther --- ironfish-cli/src/commands/start.test.ts | 13 +++++++++- ironfish-cli/src/commands/start.ts | 34 +++++++++++++------------ ironfish-cli/src/commands/testnet.ts | 22 ++++++++++++++++ ironfish-cli/src/images.ts | 8 ------ 4 files changed, 52 insertions(+), 25 deletions(-) diff --git a/ironfish-cli/src/commands/start.test.ts b/ironfish-cli/src/commands/start.test.ts index 452d9bcbb8..60849b6ad2 100644 --- a/ironfish-cli/src/commands/start.test.ts +++ b/ironfish-cli/src/commands/start.test.ts @@ -88,6 +88,17 @@ describe('start command', () => { const accounts = { accountExists: jest.fn(), getDefaultAccount: jest.fn(), + createAccount: jest.fn().mockImplementation( + (name: string) => + new ironfishmodule.Account({ + incomingViewKey: '', + outgoingViewKey: '', + publicAddress: '', + rescan: null, + spendingKey: '', + name, + }), + ), } const peerNetwork = { @@ -138,7 +149,7 @@ describe('start command', () => { expectCli(ctx.stdout).include(`Peer Identity`) // telemetry expectCli(ctx.stdout).include( - `To help improve Ironfish, opt in to collecting telemetry`, + `To help improve Iron Fish, opt in to collecting telemetry`, ) expect(setConfig).toHaveBeenCalledWith('isFirstRun', false) expect(setConfig).toHaveBeenCalledWith('telemetryNodeId', expect.any(String)) diff --git a/ironfish-cli/src/commands/start.ts b/ironfish-cli/src/commands/start.ts index 05a98ffe92..138d76b7a4 100644 --- a/ironfish-cli/src/commands/start.ts +++ b/ironfish-cli/src/commands/start.ts @@ -26,8 +26,9 @@ import { VerboseFlag, VerboseFlagKey, } from '../flags' -import { ONE_FISH_IMAGE, TELEMETRY_BANNER } from '../images' +import { ONE_FISH_IMAGE } from '../images' +export const ENABLE_TELEMETRY_CONFIG_KEY = 'enableTelemetry' const DEFAULT_ACCOUNT_NAME = 'default' export default class Start extends IronfishCommand { @@ -229,29 +230,30 @@ export default class Start extends IronfishCommand { * Information displayed the first time a node is running */ async firstRun(node: IronfishNode): Promise { - // Try to get the user to display telementry - if (!node.config.get('enableTelemetry')) { - this.log(TELEMETRY_BANNER) + this.log('') + this.log('Thank you for installing the Iron Fish Node.') + + if (!node.config.get(ENABLE_TELEMETRY_CONFIG_KEY)) { + this.log('') + this.log('To help improve Iron Fish, opt in to collecting telemetry by running') + this.log(` > ironfish config:set ${ENABLE_TELEMETRY_CONFIG_KEY} true`) } - // Create a default account on startup if (!node.accounts.getDefaultAccount()) { - if (node.accounts.accountExists(DEFAULT_ACCOUNT_NAME)) { - await node.accounts.setDefaultAccount(DEFAULT_ACCOUNT_NAME) - this.log(`The default account is now: ${DEFAULT_ACCOUNT_NAME}\n`) - } else { - await this.sdk.clientMemory.connect(node) + this.log('') - const result = await this.sdk.clientMemory.createAccount({ - name: DEFAULT_ACCOUNT_NAME, - }) + if (!node.accounts.accountExists(DEFAULT_ACCOUNT_NAME)) { + const account = await node.accounts.createAccount(DEFAULT_ACCOUNT_NAME, true) - this.log( - `New default account created: ${DEFAULT_ACCOUNT_NAME} \nAccount's public address: ${result?.content.publicAddress}\n`, - ) + this.log(`New default account created: ${account.name}`) + this.log(`Account's public address: ${account.publicAddress}`) + } else { + this.log(`The default account is now: ${DEFAULT_ACCOUNT_NAME}`) + await node.accounts.setDefaultAccount(DEFAULT_ACCOUNT_NAME) } } + this.log('') node.internal.set('isFirstRun', false) node.internal.set('telemetryNodeId', uuid()) await node.internal.save() diff --git a/ironfish-cli/src/commands/testnet.ts b/ironfish-cli/src/commands/testnet.ts index 15b74f69c6..36ad3b3dfb 100644 --- a/ironfish-cli/src/commands/testnet.ts +++ b/ironfish-cli/src/commands/testnet.ts @@ -5,6 +5,7 @@ import { CliUx, Flags } from '@oclif/core' import { WebApi } from 'ironfish' import { IronfishCommand } from '../command' import { DataDirFlag, DataDirFlagKey, VerboseFlag, VerboseFlagKey } from '../flags' +import { ENABLE_TELEMETRY_CONFIG_KEY } from './start' export default class Testnet extends IronfishCommand { static hidden = false @@ -25,6 +26,10 @@ export default class Testnet extends IronfishCommand { default: false, description: "Don't update your graffiti", }), + skipTelemetry: Flags.boolean({ + default: false, + description: "Don't update your telemetry", + }), } static args = [ @@ -88,11 +93,15 @@ export default class Testnet extends IronfishCommand { const existingNodeName = (await node.getConfig({ name: 'nodeName' })).content.nodeName const existingGraffiti = (await node.getConfig({ name: 'blockGraffiti' })).content .blockGraffiti + const telemetryEnabled = (await node.getConfig({ name: ENABLE_TELEMETRY_CONFIG_KEY })) + .content.enableTelemetry const updateNodeName = existingNodeName !== user.graffiti && !flags.skipName const updateGraffiti = existingGraffiti !== user.graffiti && !flags.skipGraffiti const needsUpdate = updateNodeName || updateGraffiti + let updateTelemetry = !telemetryEnabled && !flags.skipTelemetry + if (!needsUpdate) { this.log('Your node is already up to date!') this.exit(0) @@ -123,6 +132,14 @@ export default class Testnet extends IronfishCommand { this.log('') } + if (!flags.confirm && updateTelemetry) { + updateTelemetry = await CliUx.ux.confirm( + 'Do you want to help improve Iron Fish by enabling Telemetry? (y)es / (n)o', + ) + + this.log('') + } + if (updateNodeName) { await node.setConfig({ name: 'nodeName', value: user.graffiti }) this.log( @@ -136,5 +153,10 @@ export default class Testnet extends IronfishCommand { `✅ Updated GRAFFITI from ${existingGraffiti || '{NOT SET}'} to ${user.graffiti}`, ) } + + if (updateTelemetry) { + await node.setConfig({ name: ENABLE_TELEMETRY_CONFIG_KEY, value: true }) + this.log('✅ Telemetry Enabled 🙏') + } } } diff --git a/ironfish-cli/src/images.ts b/ironfish-cli/src/images.ts index 76ee00958f..0630480d9b 100644 --- a/ironfish-cli/src/images.ts +++ b/ironfish-cli/src/images.ts @@ -31,11 +31,3 @@ export const TWO_FISH_IMAGE = ::::::::::::: ::::::::::::::::::::: ::::::::::::: ::::::::::::::::::::: \n\ :::::::::::: ::::::::::::::::::: :::::::::::: ::::::::::::::::::: \n\ :::::::::: :::::::::::::::: :::::::::: :::::::::::::::: ' - -export const TELEMETRY_BANNER = ` -################################################################# -# Thank you for installing the Iron Fish Node. # -# To help improve Ironfish, opt in to collecting telemetry # -# by setting telemetry=true in your configuration file # -################################################################# -` From 3a17bba3fa4bc87fe8698c6e3dea90d67a29cfa4 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 14 Feb 2022 10:33:09 -0500 Subject: [PATCH 16/24] feat(ironfish): Add retry logic and slice submission to telemetry service (#991) * feat(ironfish): Add slice submissions and retry logic to telemetry service * Update telemetry test --- ironfish/src/telemetry/telemetry.test.ts | 59 +++++++++++++++++------- ironfish/src/telemetry/telemetry.ts | 21 ++++++--- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/ironfish/src/telemetry/telemetry.test.ts b/ironfish/src/telemetry/telemetry.test.ts index c0ad02acd0..ffeb250064 100644 --- a/ironfish/src/telemetry/telemetry.test.ts +++ b/ironfish/src/telemetry/telemetry.test.ts @@ -78,31 +78,58 @@ describe('Telemetry', () => { }) describe('flush', () => { - describe('when the pool throws an error and the queue is not saturated', () => { - it('retries the points and logs an error', async () => { - jest.spyOn(telemetry['workerPool'], 'submitTelemetry').mockImplementationOnce(() => { - throw new Error() + describe('when the pool throws an error', () => { + describe('when max retries have not been hit', () => { + it('retries the points and logs an error', async () => { + jest.spyOn(telemetry['workerPool'], 'submitTelemetry').mockImplementationOnce(() => { + throw new Error() + }) + const error = jest.spyOn(telemetry['logger'], 'error') + + const points = [mockMetric] + const retries = telemetry['retries'] + telemetry['points'] = points + + await telemetry.flush() + expect(error).toHaveBeenCalled() + expect(telemetry['points']).toEqual(points) + expect(telemetry['retries']).toBe(retries + 1) }) - const error = jest.spyOn(telemetry['logger'], 'error') + }) - const points = [] - for (let i = 0; i < telemetry['MAX_QUEUE_SIZE'] - 1; i++) { - points.push(mockMetric) - } - telemetry['points'] = points + describe('when max retries have been hit', () => { + it('clears the points and logs an error', async () => { + jest.spyOn(telemetry['workerPool'], 'submitTelemetry').mockImplementationOnce(() => { + throw new Error() + }) + const error = jest.spyOn(telemetry['logger'], 'error') + + telemetry['retries'] = telemetry['MAX_RETRIES'] + telemetry['points'] = [mockMetric] - await telemetry.flush() - expect(telemetry['points']).toEqual(points) - expect(error).toHaveBeenCalled() + await telemetry.flush() + expect(error).toHaveBeenCalled() + expect(telemetry['points']).toEqual([]) + expect(telemetry['retries']).toBe(0) + }) }) }) - it('submits telemetry to the pool', async () => { + it('submits a slice of telemetry points to the pool', async () => { const submitTelemetry = jest.spyOn(telemetry['workerPool'], 'submitTelemetry') - telemetry.submit(mockMetric) + const points = Array(telemetry['MAX_POINTS_TO_SUBMIT'] + 1).fill(mockMetric) + telemetry['points'] = points + await telemetry.flush() - expect(submitTelemetry).toHaveBeenCalled() + expect(submitTelemetry).toHaveBeenCalledWith( + points.slice(0, telemetry['MAX_POINTS_TO_SUBMIT']), + ) + expect(telemetry['points']).toEqual(points.slice(telemetry['MAX_POINTS_TO_SUBMIT'])) + expect(telemetry['points']).toHaveLength( + points.slice(telemetry['MAX_POINTS_TO_SUBMIT']).length, + ) + expect(telemetry['retries']).toBe(0) }) }) }) diff --git a/ironfish/src/telemetry/telemetry.ts b/ironfish/src/telemetry/telemetry.ts index 734ebdbb98..668cabfe6b 100644 --- a/ironfish/src/telemetry/telemetry.ts +++ b/ironfish/src/telemetry/telemetry.ts @@ -10,7 +10,8 @@ import { Tag } from './interfaces/tag' export class Telemetry { private readonly FLUSH_INTERVAL = 5000 - private readonly MAX_QUEUE_SIZE = 1000 + private readonly MAX_POINTS_TO_SUBMIT = 1000 + private readonly MAX_RETRIES = 5 private readonly defaultTags: Tag[] private readonly logger: Logger @@ -19,15 +20,17 @@ export class Telemetry { private started: boolean private flushInterval: SetIntervalToken | null private points: Metric[] + private retries: number constructor(options: { workerPool: WorkerPool; logger?: Logger; defaultTags?: Tag[] }) { this.logger = options.logger ?? createRootLogger() this.workerPool = options.workerPool this.defaultTags = options.defaultTags ?? [] - this.started = false this.flushInterval = null this.points = [] + this.retries = 0 + this.started = false } start(): void { @@ -84,8 +87,8 @@ export class Telemetry { } async flush(): Promise { - const points = this.points - this.points = [] + const points = this.points.slice(0, this.MAX_POINTS_TO_SUBMIT) + this.points = this.points.slice(this.MAX_POINTS_TO_SUBMIT) if (points.length === 0) { return @@ -94,12 +97,18 @@ export class Telemetry { try { await this.workerPool.submitTelemetry(points) this.logger.debug(`Submitted ${points.length} telemetry points`) + this.retries = 0 } catch (error: unknown) { this.logger.error(`Error submitting telemetry to API: ${renderError(error)}`) - if (points.length < this.MAX_QUEUE_SIZE) { + if (this.retries < this.MAX_RETRIES) { this.logger.debug('Retrying telemetry submission') - this.points = points + this.retries++ + this.points = points.concat(this.points) + } else { + this.logger.debug('Max retries reached. Resetting telemetry points') + this.retries = 0 + this.points = [] } } } From 5c8592f68f458c4f7d6220d2db0a7106c3d374b6 Mon Sep 17 00:00:00 2001 From: Ben Holden-Crowther <4144334+blrhc@users.noreply.github.com> Date: Mon, 14 Feb 2022 21:20:00 +0000 Subject: [PATCH 17/24] Github -> GitHub (#1001) Very minor change, but looks a bit weird without the capitalization. --- CONTRIBUTING.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 905f36ad1f..fc4f45fc28 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,14 +15,14 @@ We welcome contributions from anyone on the internet, and are grateful for even Thanks in advance for your help. -## We develop with Github +## We develop with GitHub -We use Github to host code, to track issues and feature requests, as well as accept pull requests. +We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. ## Pull Requests Guidelines -Pull requests are the best way to propose a new change to the codebase (we use the classic [Github Flow](https://guides.github.com/introduction/flow/index.html)). +Pull requests are the best way to propose a new change to the codebase (we use the classic [GitHub Flow](https://guides.github.com/introduction/flow/index.html)). To create a new pull request: 1. Fork the repo and check out a new branch from `master`. @@ -30,7 +30,7 @@ To create a new pull request: 3. Update the documentation - Especially if you've changed APIs or created new functions. 4. Ensure the test suite passes by running `yarn test`. 5. Make sure your code lints by running `yarn lint`. -6. Once 4 & 5 are passing, create a new pull request on Github. +6. Once 4 & 5 are passing, create a new pull request on GitHub. 7. Add the right label to your PR `documentation`, `bug`, `security-issue`, or `enhancement`. 8. Add a description of what the PR is changing: * What problem is the PR solving @@ -39,7 +39,7 @@ To create a new pull request: Once the PR is created, one of the maintainers will review it and merge it into the master branch. -If you are thinking of working on a complex change, do not hesitate to discuss the change you wish to make via a Github Issue. You can also request feedback early, by opening a WIP pull request or discuss with a maintainer to ensure your work is in line with the philosophy and roadmap of Iron Fish. +If you are thinking of working on a complex change, do not hesitate to discuss the change you wish to make via a GitHub Issue. You can also request feedback early, by opening a WIP pull request or discuss with a maintainer to ensure your work is in line with the philosophy and roadmap of Iron Fish. ## Where to start @@ -62,7 +62,7 @@ For our Rust codebase, you can run the test suites for each project by running ` ## Continuous integration -After creating a PR on Github, the code will be tested automatically by GitHub Action. The tests can take up to 15 minutes to pass. We ask you to test your code on your machine before submitting a PR. +After creating a PR on GitHub, the code will be tested automatically by GitHub Action. The tests can take up to 15 minutes to pass. We ask you to test your code on your machine before submitting a PR. ## Style Guide From 3ed863022ceb6cdb76868e9c35c24d2efa916b24 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Mon, 14 Feb 2022 14:22:22 -0700 Subject: [PATCH 18/24] Debug command will now show multiple CPU models (#1003) This is not perfect, as it only checks for unique CPU model names, but it's an improvement over just grabbing the first CPU model name --- ironfish-cli/src/commands/debug.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/debug.ts b/ironfish-cli/src/commands/debug.ts index 0486758ff7..e3b8e37fdf 100644 --- a/ironfish-cli/src/commands/debug.ts +++ b/ironfish-cli/src/commands/debug.ts @@ -26,7 +26,7 @@ export default class Debug extends IronfishCommand { const accountsHeadSequence = accountsBlockHeader?.sequence || 'null' const cpus = os.cpus() - const cpuName = cpus[0].model + const cpuNames = [...new Set(cpus.map((c) => c.model))] const cpuThreads = cpus.length const memTotal = FileUtils.formatMemorySize(os.totalmem()) @@ -37,7 +37,7 @@ export default class Debug extends IronfishCommand { Iron Fish version ${node.pkg.version} @ ${node.pkg.git} Iron Fish library ${IronfishPKG.version} @ ${IronfishPKG.git} Operating system ${os.type()} ${process.arch} -CPU model ${cpuName} +CPU model(s) ${cpuNames.toString()} CPU threads ${cpuThreads} RAM total ${memTotal} Node version ${process.version} From 6ceebdc6c9d806e63371b574b5a4dcfb97cc2195 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 15 Feb 2022 12:05:42 -0800 Subject: [PATCH 19/24] Submit new telemetry for block seen (#1005) This submits a new telemetry if the block has been successfully added. I decided to only submit this telemtry so if invalid blocks are sent, they aren't added to our data points. We also add data points for forks and not forks, so that we can measure properly. --- ironfish/src/syncer.ts | 9 +++++++ ironfish/src/telemetry/interfaces/metric.ts | 2 +- ironfish/src/telemetry/telemetry.ts | 26 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/ironfish/src/syncer.ts b/ironfish/src/syncer.ts index a2a596bfe3..21359bbb94 100644 --- a/ironfish/src/syncer.ts +++ b/ironfish/src/syncer.ts @@ -31,6 +31,7 @@ export class Syncer { readonly chain: Blockchain readonly strategy: Strategy readonly metrics: MetricsMonitor + readonly telemetry: Telemetry readonly logger: Logger readonly speed: Meter @@ -57,6 +58,8 @@ export class Syncer { this.chain = options.chain this.strategy = options.strategy this.logger = logger.withTag('syncer') + this.telemetry = options.telemetry + this.metrics = options.metrics || new MetricsMonitor({ telemetry: options.telemetry, logger: this.logger }) @@ -484,6 +487,8 @@ export class Syncer { return false } + const seenAt = new Date() + const { added, block } = await this.addBlock(peer, newBlock) if (!peer.sequence || block.header.sequence > peer.sequence) { @@ -492,6 +497,10 @@ export class Syncer { this.onGossip.emit(block) + if (added) { + this.telemetry.submitNewBlockSeen(block, seenAt) + } + return added } diff --git a/ironfish/src/telemetry/interfaces/metric.ts b/ironfish/src/telemetry/interfaces/metric.ts index 2a09dbfe0b..89b192aefc 100644 --- a/ironfish/src/telemetry/interfaces/metric.ts +++ b/ironfish/src/telemetry/interfaces/metric.ts @@ -12,7 +12,7 @@ export interface Metric { * A description for the container that the fields measure. Defaults to * 'node' because all metrics are submitted from an Iron Fish node. */ - measurement: 'node' + measurement: string /** * The name of whatever is being measured. diff --git a/ironfish/src/telemetry/telemetry.ts b/ironfish/src/telemetry/telemetry.ts index 668cabfe6b..b480e77249 100644 --- a/ironfish/src/telemetry/telemetry.ts +++ b/ironfish/src/telemetry/telemetry.ts @@ -166,4 +166,30 @@ export class Telemetry { ], }) } + + submitNewBlockSeen(block: Block, seenAt: Date): void { + this.submit({ + measurement: 'propagation', + name: 'propagation', + timestamp: seenAt, + tags: [ + { + name: 'block_hash', + value: block.header.hash.toString('hex'), + }, + ], + fields: [ + { + name: 'block_timestamp', + type: 'integer', + value: block.header.timestamp.valueOf(), + }, + { + name: 'block_sequence', + type: 'integer', + value: block.header.sequence, + }, + ], + }) + } } From 818eadf14a4110f5f48aec55dd0a3d346f6033a3 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 15 Feb 2022 12:12:26 -0800 Subject: [PATCH 20/24] Convert telemetry types to strict union types (#1006) * Convert telemetry types to strict union types The interfaces before did not ensure that if you put "string" that you then passed a string in the value. These union types now ensure that the value matches the type that is specified. * Revert tag type --- ironfish/src/telemetry/interfaces/field.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/ironfish/src/telemetry/interfaces/field.ts b/ironfish/src/telemetry/interfaces/field.ts index 96a07a2402..7249e967fb 100644 --- a/ironfish/src/telemetry/interfaces/field.ts +++ b/ironfish/src/telemetry/interfaces/field.ts @@ -1,8 +1,19 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -export interface Field { - name: string - type: 'string' | 'boolean' | 'float' | 'integer' - value: string | boolean | number -} +export type Field = + | { + name: string + type: 'string' + value: string + } + | { + name: string + type: 'boolean' + value: boolean + } + | { + name: string + type: 'float' | 'integer' + value: number + } From 266e82b9fbff16072d855ce3282520b4a84bc275 Mon Sep 17 00:00:00 2001 From: wd021 Date: Tue, 15 Feb 2022 20:34:10 +0000 Subject: [PATCH 21/24] readme typo fix (#1009) --- ironfish/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironfish/README.md b/ironfish/README.md index 1158a977c2..eb6675c85b 100644 --- a/ironfish/README.md +++ b/ironfish/README.md @@ -39,7 +39,7 @@ An adapter exists to represent a single transport layer. For example, in an HTTP ### Logs By default the log level is set to only display info. -Change the `logLevel` in the config file, from `*:info` to `*debug` if you want verbose logs. +Change the `logLevel` in the config file, from `*:info` to `*:debug` if you want verbose logs. ### IronfishSDK This project contains the IronfishSdk, which is just a simple wrapper around the ironfish components like Accounts, Config, and IronfishNode. You can use the individual components whenever you feel like it, though the SDK is aimed at making usage easier. From 040b1ca49464207b71d22aacf7b2e0c1b0daa932 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 15 Feb 2022 12:37:58 -0800 Subject: [PATCH 22/24] Remove captain from readme docs (#1017) --- ironfish/README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ironfish/README.md b/ironfish/README.md index eb6675c85b..7cef65f165 100644 --- a/ironfish/README.md +++ b/ironfish/README.md @@ -2,13 +2,10 @@ [![codecov](https://codecov.io/gh/iron-fish/ironfish/branch/master/graph/badge.svg?token=PCSVEVEW5V&flag=ironfish)](https://codecov.io/gh/iron-fish/ironfish) -Ironfish SDK wraps all of the generic components of [Captain](./src/captain/README.md) into a project that is specific to Ironfish. +Ironfish contains the implementation of the Ironfish node and all relavent components that run it including the Blockchain, MemPool, RPC layer, PeerNetwork, and more. ## Components -### Strategy -It also contains a strategy, which is a collection of implementations that [Captain](./src/captain/README.md) uses to implement coin specific logic. - ### Accounts An account store used to manage, create, and update Ironfish accounts. @@ -33,16 +30,16 @@ This is the server that handles clients connecting and making requests against t When the RpcServer starts, so do the transports. They accept messages from clients, construct Requests, and route them into the routing layer which executes the proper route. -#### Adapter +#### RpcAdapter An adapter exists to represent a single transport layer. For example, in an HTTP adapter you might listen on port 80 for requests, construct RPC layer Request objects, and feed them into the routing layer, then render the RPC responses as HTTP responses. See IPCAdapter for an example of how to implement an adapter. -### Logs +### Logger By default the log level is set to only display info. Change the `logLevel` in the config file, from `*:info` to `*:debug` if you want verbose logs. ### IronfishSDK -This project contains the IronfishSdk, which is just a simple wrapper around the ironfish components like Accounts, Config, and IronfishNode. You can use the individual components whenever you feel like it, though the SDK is aimed at making usage easier. +This project contains the IronfishSdk, which is just a simple wrapper around the ironfish components like IronfishNode, Blockchain, Config, Accounts. You can use the individual components whenever you feel like it, though the SDK is aimed at making usage easier. #### SDK Example From cc51d7840e957cb170a2cade089d71e177d2a70e Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 15 Feb 2022 12:41:17 -0800 Subject: [PATCH 23/24] Make separate measuresments for each telemetry (#1016) * Make separate measuresments for each telemetry Also standardize on values not duplicating their measurement prefix. * Fix tests --- ironfish/src/telemetry/interfaces/metric.ts | 9 ++------- ironfish/src/telemetry/telemetry.test.ts | 2 -- ironfish/src/telemetry/telemetry.ts | 21 ++++++++------------- 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/ironfish/src/telemetry/interfaces/metric.ts b/ironfish/src/telemetry/interfaces/metric.ts index 89b192aefc..86c31fe15d 100644 --- a/ironfish/src/telemetry/interfaces/metric.ts +++ b/ironfish/src/telemetry/interfaces/metric.ts @@ -9,16 +9,11 @@ import { Tag } from './tag' */ export interface Metric { /** - * A description for the container that the fields measure. Defaults to - * 'node' because all metrics are submitted from an Iron Fish node. + * A description for the container that the fields measure. This is equivilent + * to a SQL table. */ measurement: string - /** - * The name of whatever is being measured. - */ - name: string - /** * The exact time at which the metric was recorded. * JS gives us millisecond accuracy here. diff --git a/ironfish/src/telemetry/telemetry.test.ts b/ironfish/src/telemetry/telemetry.test.ts index ffeb250064..249165e8fc 100644 --- a/ironfish/src/telemetry/telemetry.test.ts +++ b/ironfish/src/telemetry/telemetry.test.ts @@ -13,7 +13,6 @@ describe('Telemetry', () => { const mockMetric: Metric = { measurement: 'node', - name: 'memory', fields: [ { name: 'heap_used', @@ -59,7 +58,6 @@ describe('Telemetry', () => { it('throws an error', () => { const metric: Metric = { measurement: 'node', - name: 'memory', fields: [], } diff --git a/ironfish/src/telemetry/telemetry.ts b/ironfish/src/telemetry/telemetry.ts index b480e77249..a3ee983645 100644 --- a/ironfish/src/telemetry/telemetry.ts +++ b/ironfish/src/telemetry/telemetry.ts @@ -115,24 +115,21 @@ export class Telemetry { submitNodeStarted(): void { this.submit({ - measurement: 'node', - name: 'started', + measurement: 'node_started', fields: [{ name: 'online', type: 'boolean', value: true }], }) } submitNodeStopped(): void { this.submit({ - measurement: 'node', - name: 'started', + measurement: 'node_started', fields: [{ name: 'online', type: 'boolean', value: false }], }) } submitBlockMined(block: Block): void { this.submit({ - measurement: 'node', - name: 'block_mined', + measurement: 'block_mined', fields: [ { name: 'difficulty', @@ -150,8 +147,7 @@ export class Telemetry { submitMemoryUsage(heapUsed: number, heapTotal: number): void { this.submit({ - measurement: 'node', - name: 'memory', + measurement: 'node_memory', fields: [ { name: 'heap_used', @@ -169,23 +165,22 @@ export class Telemetry { submitNewBlockSeen(block: Block, seenAt: Date): void { this.submit({ - measurement: 'propagation', - name: 'propagation', + measurement: 'block_propagation', timestamp: seenAt, tags: [ { - name: 'block_hash', + name: 'hash', value: block.header.hash.toString('hex'), }, ], fields: [ { - name: 'block_timestamp', + name: 'timestamp', type: 'integer', value: block.header.timestamp.valueOf(), }, { - name: 'block_sequence', + name: 'sequence', type: 'integer', value: block.header.sequence, }, From 1ca28c2c792835dadb9287e04119ffdf55cc7385 Mon Sep 17 00:00:00 2001 From: NullSoldier Date: Tue, 15 Feb 2022 12:56:18 -0800 Subject: [PATCH 24/24] Bump ironfish-cli to 0.1.22 --- ironfish-cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 1998e5080f..816e4c1694 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish-cli", - "version": "0.1.21", + "version": "0.1.22", "description": "Command line Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "engines": {