diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd60eb386f..062ab904a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,13 +39,9 @@ jobs: - name: Lint run: yarn lint - test-matrix: - name: Test Matrix + test: + name: Test runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18, 20] - test-type: ["test", "test:slow"] steps: - name: Check out Git repository @@ -54,7 +50,8 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: ${{ matrix.version }} + # Tests will only run on Node v20 due to https://github.com/nodejs/node/issues/35889 + node-version: 20 cache: 'yarn' - name: Cache Rust @@ -66,7 +63,7 @@ jobs: run: yarn --non-interactive --frozen-lockfile - name: Run tests - run: yarn ${{ matrix.test-type}}:coverage --maxWorkers=2 --workerIdleMemoryLimit=2000MB + run: yarn test:coverage --maxWorkers=2 --workerIdleMemoryLimit=2000MB - name: Check for missing fixtures run: | @@ -79,13 +76,39 @@ jobs: if: github.repository == 'iron-fish/ironfish' run: CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }} ROOT_PATH=$GITHUB_WORKSPACE/ yarn coverage:upload - # This is a workaround to have status checks on jobs that use a matrix. - # See: https://github.com/orgs/community/discussions/26822 - test: - if: ${{ always() }} + testslow: + name: Slow Tests runs-on: ubuntu-latest - name: Test - needs: [test-matrix] + steps: - - run: exit 1 - if: ${{ !contains(needs.*.result, 'success')}} + - name: Check out Git repository + uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + # Tests will only run on Node v20 due to https://github.com/nodejs/node/issues/35889 + node-version: 20 + cache: 'yarn' + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + shared-key: nodejs + + - name: Install packages + run: yarn --non-interactive --frozen-lockfile + + - name: Run slow tests & coverage + run: yarn test:slow:coverage --maxWorkers=2 --workerIdleMemoryLimit=2000MB + + - name: Check for missing fixtures + run: | + if [[ $(git status | grep fixture) ]]; then + echo "New test fixtures have not been checked in, please check them in." + exit 1 + fi + + - name: Upload coverage + if: github.repository == 'iron-fish/ironfish' + run: CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }} ROOT_PATH=$GITHUB_WORKSPACE/ yarn coverage:upload diff --git a/config/tsconfig.base.json b/config/tsconfig.base.json index 14f0c9b61e..0d4d1e91d6 100644 --- a/config/tsconfig.base.json +++ b/config/tsconfig.base.json @@ -1,15 +1,15 @@ { "compilerOptions": { - "target": "ES2020", - "lib": ["dom", "dom.iterable", "ES2020"], + "target": "es2020", + "lib": ["DOM", "es2020"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, - "module": "commonjs", - "moduleResolution": "node", + "module": "Node16", + "moduleResolution": "Node16", "sourceMap": true, "resolveJsonModule": true, "composite": true, diff --git a/ironfish-cli/Dockerfile b/ironfish-cli/Dockerfile index 2e1124d876..7f49291dd0 100644 --- a/ironfish-cli/Dockerfile +++ b/ironfish-cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-bullseye as build +FROM node:20-bookworm as build ENV PATH="/root/.cargo/bin:${PATH}" RUN \ @@ -12,7 +12,7 @@ COPY ./ /usr/src/ironfish RUN /usr/src/ironfish/ironfish-cli/scripts/build.sh -FROM node:18-bullseye-slim +FROM node:20-bookworm-slim EXPOSE 8020:8020 EXPOSE 9033:9033 diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 43b976955a..5a5e6e0f2a 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "1.12.0", + "version": "1.13.0", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -32,12 +32,13 @@ "cross-env": "7.0.3", "eslint-config-ironfish": "*", "eslint-plugin-deprecation": "2.0.0", - "jest": "29.3.1", - "jest-jasmine2": "29.3.1", + "jest": "29.7.0", + "jest-jasmine2": "29.7.0", "oclif": "2.6.0", "rimraf": "^3.0.2", + "ts-jest": "29.1.1", "tsc-watch": "4.2.9", - "typescript": "4.3.4", + "typescript": "5.0.4", "yarn": "^1.22.10" }, "scripts": { @@ -48,9 +49,9 @@ "start:dev": "node start", "start": "yarn build && yarn start:js", "start:js": "cross-env OCLIF_TS_NODE=0 IRONFISH_DEBUG=1 node --expose-gc --inspect=:0 --inspect-publish-uid=http --enable-source-maps bin/run", - "test": "yarn clean && tsc -b && tsc -b tsconfig.test.json && jest --passWithNoTests", - "test:coverage:html": "tsc -b tsconfig.test.json && jest --passWithNoTests --testPathIgnorePatterns --coverage --coverage-reporters html", - "test:watch": "tsc -b tsconfig.test.json && jest --watch --coverage false", + "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", + "test:coverage:html": "tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns --coverage --coverage-reporters html", + "test:watch": "yarn clean && tsc -b && tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch --coverage false", "postpack": "rimraf oclif.manifest.json", "clean": "rimraf build", "prepack": "rimraf build && yarn build && oclif manifest && oclif readme", @@ -61,8 +62,8 @@ "@aws-sdk/client-s3": "3", "@aws-sdk/client-secrets-manager": "3", "@aws-sdk/s3-request-presigner": "3", - "@ironfish/rust-nodejs": "1.10.0", - "@ironfish/sdk": "1.12.0", + "@ironfish/rust-nodejs": "1.11.0", + "@ironfish/sdk": "1.13.0", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", diff --git a/ironfish-cli/src/commands/ceremony/index.ts b/ironfish-cli/src/commands/ceremony/index.ts index ab24059f59..3f9451e952 100644 --- a/ironfish-cli/src/commands/ceremony/index.ts +++ b/ironfish-cli/src/commands/ceremony/index.ts @@ -93,7 +93,11 @@ export default class Ceremony extends IronfishCommand { try { response = await axios.get(downloadLink, { responseType: 'stream', - onDownloadProgress: (p: ProgressEvent) => { + onDownloadProgress: (p: { + readonly lengthComputable: boolean + readonly loaded: number + readonly total: number + }) => { this.log('loaded', p.loaded, 'total', p.total) }, }) diff --git a/ironfish-cli/src/commands/logs.ts b/ironfish-cli/src/commands/logs.ts index 0203ee875c..93fb0eb0d5 100644 --- a/ironfish-cli/src/commands/logs.ts +++ b/ironfish-cli/src/commands/logs.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 { ConsoleReporterInstance, IJSON, IronfishNode } from '@ironfish/sdk' +import { ConsoleReporterInstance, FullNode, IJSON } from '@ironfish/sdk' import { logType } from 'consola' import { IronfishCommand } from '../command' import { RemoteFlags } from '../flags' @@ -13,7 +13,7 @@ export default class LogsCommand extends IronfishCommand { ...RemoteFlags, } - node: IronfishNode | null = null + node: FullNode | null = null async start(): Promise { await this.parse(LogsCommand) diff --git a/ironfish-cli/src/commands/peers/index.ts b/ironfish-cli/src/commands/peers/index.ts index 527f294792..c5a746c3d3 100644 --- a/ironfish-cli/src/commands/peers/index.ts +++ b/ironfish-cli/src/commands/peers/index.ts @@ -11,18 +11,13 @@ import { CommandFlags } from '../../types' type GetPeerResponsePeer = GetPeersResponse['peers'][0] const STATE_COLUMN_HEADER = 'STATE' -const { sort, ...tableFlags } = CliUx.ux.table.flags() export class ListCommand extends IronfishCommand { static description = `List all connected peers` static flags = { ...RemoteFlags, - ...tableFlags, - sort: { - ...sort, - exclusive: ['follow'], - }, + ...CliUx.ux.table.flags(), follow: Flags.boolean({ char: 'f', default: false, @@ -58,10 +53,9 @@ export class ListCommand extends IronfishCommand { async start(): Promise { const { flags } = await this.parse(ListCommand) + flags.sort = flags.sort ?? STATE_COLUMN_HEADER if (!flags.follow) { - flags.sort = flags.sort ?? STATE_COLUMN_HEADER - await this.sdk.client.connect() const response = await this.sdk.client.peer.getPeers() this.log(renderTable(response.content, flags)) diff --git a/ironfish-cli/src/commands/reset.ts b/ironfish-cli/src/commands/reset.ts index a55d6cdaab..8ffcd953af 100644 --- a/ironfish-cli/src/commands/reset.ts +++ b/ironfish-cli/src/commands/reset.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 { IronfishNode, PEER_STORE_FILE_NAME } from '@ironfish/sdk' +import { FullNode, PEER_STORE_FILE_NAME } from '@ironfish/sdk' import { CliUx, Flags } from '@oclif/core' import fsAsync from 'fs/promises' import { IronfishCommand } from '../command' @@ -32,7 +32,7 @@ export default class Reset extends IronfishCommand { }), } - node: IronfishNode | null = null + node: FullNode | null = null async start(): Promise { const { flags } = await this.parse(Reset) diff --git a/ironfish-cli/src/commands/service/estimate-fee-rates.ts b/ironfish-cli/src/commands/service/estimate-fee-rates.ts deleted file mode 100644 index 1a338a9805..0000000000 --- a/ironfish-cli/src/commands/service/estimate-fee-rates.ts +++ /dev/null @@ -1,103 +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 { CurrencyUtils, PromiseUtils } from '@ironfish/sdk' -import { WebApi } from '@ironfish/sdk' -import { Flags } from '@oclif/core' -import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' -import { IronfishCliPKG } from '../../package' - -export default class EstimateFees extends IronfishCommand { - static hidden = true - - static description = ` - Measures fee rate estimates and submits them to telemetry API - ` - - static flags = { - ...RemoteFlags, - endpoint: Flags.string({ - char: 'e', - parse: (input: string) => Promise.resolve(input.trim()), - required: false, - description: 'API host to sync to', - }), - token: Flags.string({ - char: 't', - parse: (input: string) => Promise.resolve(input.trim()), - required: false, - description: 'API host token to authenticate with', - }), - delay: Flags.integer({ - required: false, - default: 60000, - description: 'Delay (in ms) to wait before uploading fee rate estimates', - }), - } - - async start(): Promise { - const { flags } = await this.parse(EstimateFees) - - const apiHost = ( - flags.endpoint || - process.env.IRONFISH_API_HOST || - 'https://api.ironfish.network' - ).trim() - - const apiToken = (flags.token || process.env.IRONFISH_API_TOKEN || '').trim() - - const api = new WebApi({ host: apiHost, token: apiToken }) - - let connected = false - - // eslint-disable-next-line no-constant-condition - while (true) { - connected = await this.sdk.client.tryConnect() - - if (!connected) { - await PromiseUtils.sleep(1000) - continue - } - - const response = await this.sdk.client.chain.estimateFeeRates() - - if (!(response.content.slow && response.content.average && response.content.fast)) { - this.log('Unexpected response') - } else { - const feeRateSlow = Number(CurrencyUtils.decode(response.content.slow)) - const feeRateAverage = Number(CurrencyUtils.decode(response.content.average)) - const feeRateFast = Number(CurrencyUtils.decode(response.content.fast)) - - await api.submitTelemetry({ - points: [ - { - measurement: 'fee_rate_estimate', - timestamp: new Date(), - fields: [ - { - name: `fee_rate_slow`, - type: 'integer', - value: feeRateSlow, - }, - { - name: `fee_rate_average`, - type: 'integer', - value: feeRateAverage, - }, - { - name: `fee_rate_fast`, - type: 'integer', - value: feeRateFast, - }, - ], - tags: [{ name: 'version', value: IronfishCliPKG.version }], - }, - ], - }) - } - - await PromiseUtils.sleep(flags.delay) - } - } -} diff --git a/ironfish-cli/src/commands/service/faucet.ts b/ironfish-cli/src/commands/service/faucet.ts deleted file mode 100644 index c9eb3960c7..0000000000 --- a/ironfish-cli/src/commands/service/faucet.ts +++ /dev/null @@ -1,248 +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 { Asset, isValidPublicAddress } from '@ironfish/rust-nodejs' -import { Meter, PromiseUtils, RpcConnectionError, RpcSocketClient, WebApi } from '@ironfish/sdk' -import { Flags } from '@oclif/core' -import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' - -const FAUCET_AMOUNT = 5 -const FAUCET_FEE = 1 -const MAX_RECIPIENTS_PER_TRANSACTION = 50 - -export default class Faucet extends IronfishCommand { - static hidden = true - - static description = ` - Create faucet transactions to an HTTP API using IronfishApi - ` - - static flags = { - ...RemoteFlags, - api: Flags.string({ - char: 'a', - parse: (input: string): Promise => Promise.resolve(input.trim()), - required: false, - description: 'API host to sync to', - }), - token: Flags.string({ - char: 't', - parse: (input: string): Promise => Promise.resolve(input.trim()), - required: false, - description: 'API host token to authenticate with', - }), - account: Flags.string({ - char: 'f', - parse: (input: string): Promise => Promise.resolve(input.trim()), - required: false, - description: 'Name of the account to send faucet transactions from', - }), - } - - warnedFund = false - - async start(): Promise { - 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() - - if (!apiHost) { - this.log( - `No api host found to read faucet requests from. You must set IRONFISH_API_HOST env variable or pass --api flag.`, - ) - this.exit(1) - } - - if (!apiToken) { - this.log( - `No api token found to auth with the API. You must set IRONFISH_API_TOKEN env variable or pass --token flag.`, - ) - this.exit(1) - } - - this.log(`Connecting to node and API ${apiHost}`) - - const client = this.sdk.client - const api = new WebApi({ host: apiHost, token: apiToken }) - const speed = new Meter() - - // eslint-disable-next-line no-constant-condition - while (true) { - try { - await this.startSyncing(client, api, speed, flags.account) - } catch (e) { - if (e instanceof RpcConnectionError) { - this.log('Connection error... retrying in 5 seconds') - await PromiseUtils.sleep(5000) - continue - } - - throw e - } - } - } - - async startSyncing( - client: RpcSocketClient, - api: WebApi, - speed: Meter, - account?: string, - ): Promise { - const connected = await client.tryConnect() - - if (!connected) { - this.log('Failed to connect, retrying in 5 seconds') - await PromiseUtils.sleep(5000) - return - } - - if (!account) { - this.log('Fetching faucet account') - - const response = await client.wallet.getDefaultAccount() - - if (!response.content.account) { - this.error('Faucet node has no account to use') - } - - account = response.content.account.name - } - - this.log(`Using account ${account}`) - - while (client.isConnected) { - speed.start() - speed.reset() - - await this.processNextTransaction(client, account, speed, api) - } - } - - async processNextTransaction( - client: RpcSocketClient, - account: string, - speed: Meter, - api: WebApi, - ): Promise { - const status = await client.node.getStatus() - - if (!status.content.blockchain.synced) { - this.log('Blockchain not synced, waiting 5s') - await PromiseUtils.sleep(5000) - return - } - - if (!status.content.peerNetwork.isReady) { - this.log('Peer network not ready, waiting 5s') - await PromiseUtils.sleep(5000) - return - } - - const unprocessedFaucetTransactions = await api.getNextFaucetTransactions( - MAX_RECIPIENTS_PER_TRANSACTION, - ) - - if (unprocessedFaucetTransactions.length === 0) { - this.log('No faucet jobs, waiting 5s') - await PromiseUtils.sleep(5000) - return - } - - const invalidFaucetTransactions = [] - let faucetTransactions = [] - - for (const transaction of unprocessedFaucetTransactions) { - if (isValidPublicAddress(transaction.public_key)) { - faucetTransactions.push(transaction) - } else { - invalidFaucetTransactions.push(transaction) - } - } - - const response = await client.wallet.getAccountBalance({ account }) - - if (BigInt(response.content.available) < BigInt(FAUCET_AMOUNT + FAUCET_FEE)) { - if (!this.warnedFund) { - this.log( - `Faucet has insufficient funds. Needs ${FAUCET_AMOUNT + FAUCET_FEE} but has ${ - response.content.available - } available to spend. Waiting on more funds.`, - ) - - this.warnedFund = true - } - - await PromiseUtils.sleep(5000) - return - } - - this.warnedFund = false - - const maxPossibleRecipients = Math.min( - Number(BigInt(response.content.available) / BigInt(FAUCET_AMOUNT + FAUCET_FEE)), - MAX_RECIPIENTS_PER_TRANSACTION, - ) - - faucetTransactions = faucetTransactions.slice(0, maxPossibleRecipients) - - this.log( - `Starting ${JSON.stringify( - faucetTransactions, - ['id', 'public_key', 'started_at'], - ' ', - )}`, - ) - - for (const faucetTransaction of faucetTransactions) { - await api.startFaucetTransaction(faucetTransaction.id) - } - - const outputs = faucetTransactions.map((ft) => { - return { - publicAddress: ft.public_key, - amount: BigInt(FAUCET_AMOUNT).toString(), - memo: `Faucet for ${ft.id}`, - assetId: Asset.nativeId().toString('hex'), - } - }) - - const tx = await client.wallet.sendTransaction({ - account, - outputs, - fee: BigInt(faucetTransactions.length * FAUCET_FEE).toString(), - }) - - speed.add(1) - - this.log( - `COMPLETING: ${JSON.stringify( - faucetTransactions, - ['id', 'public_key', 'started_at'], - ' ', - )} ${tx.content.hash} (5m avg ${speed.rate5m.toFixed(2)})`, - ) - - for (const faucetTransaction of faucetTransactions) { - await api.completeFaucetTransaction(faucetTransaction.id, tx.content.hash) - } - - if (invalidFaucetTransactions.length) { - this.log( - `INVALIDATING: ${JSON.stringify( - invalidFaucetTransactions, - ['id', 'public_key', 'started_at'], - ' ', - )}`, - ) - } - - for (const invalidFaucetTransaction of invalidFaucetTransactions) { - await api.completeFaucetTransaction( - invalidFaucetTransaction.id, - '0000000000000000000000000000000000000000000000000000000000000000', - ) - } - } -} diff --git a/ironfish-cli/src/commands/service/forks.ts b/ironfish-cli/src/commands/service/forks.ts deleted file mode 100644 index e3b26a1d26..0000000000 --- a/ironfish-cli/src/commands/service/forks.ts +++ /dev/null @@ -1,98 +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 { PromiseUtils } from '@ironfish/sdk' -import { WebApi } from '@ironfish/sdk' -import { Flags } from '@oclif/core' -import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' -import { IronfishCliPKG } from '../../package' -import { GossipForkCounter } from '../../utils/gossipForkCounter' - -export default class Forks extends IronfishCommand { - static hidden = true - - static description = ` - Detects forks being mined and submits count to telemetry API - ` - - static flags = { - ...RemoteFlags, - endpoint: Flags.string({ - char: 'e', - parse: (input: string) => Promise.resolve(input.trim()), - required: false, - description: 'API host to sync to', - }), - token: Flags.string({ - char: 't', - parse: (input: string) => Promise.resolve(input.trim()), - required: false, - description: 'API host token to authenticate with', - }), - delay: Flags.integer({ - required: false, - default: 60000, - description: 'Delay (in ms) to wait before uploading fork count', - }), - } - - async start(): Promise { - const { flags } = await this.parse(Forks) - - const apiHost = ( - flags.endpoint || - process.env.IRONFISH_API_HOST || - 'https://api.ironfish.network' - ).trim() - - const apiToken = (flags.token || process.env.IRONFISH_API_TOKEN || '').trim() - - const api = new WebApi({ host: apiHost, token: apiToken }) - - await this.sdk.client.connect() - - const targetBlockTimeInSeconds = (await this.sdk.client.chain.getConsensusParameters()) - .content.targetBlockTimeInSeconds - - const counter = new GossipForkCounter(targetBlockTimeInSeconds, { delayMs: flags.delay }) - counter.start() - - let connected = false - - setInterval(() => { - void api.submitTelemetry({ - points: [ - { - measurement: 'forks_count', - timestamp: new Date(), - fields: [ - { - name: 'forks', - type: 'integer', - value: counter.count, - }, - ], - tags: [{ name: 'version', value: IronfishCliPKG.version }], - }, - ], - }) - }, flags.delay) - - // eslint-disable-next-line no-constant-condition - while (true) { - connected = await this.sdk.client.tryConnect() - - if (!connected) { - await PromiseUtils.sleep(1000) - continue - } - - const response = this.sdk.client.event.onGossipStream() - - for await (const value of response.contentStream()) { - counter.add(value.blockHeader) - } - } - } -} diff --git a/ironfish-cli/src/commands/service/snapshot.ts b/ironfish-cli/src/commands/service/snapshot.ts deleted file mode 100644 index b7d0289fe4..0000000000 --- a/ironfish-cli/src/commands/service/snapshot.ts +++ /dev/null @@ -1,171 +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 { S3Client } from '@aws-sdk/client-s3' -import { FileUtils, NodeUtils } from '@ironfish/sdk' -import { CliUx, Flags } from '@oclif/core' -import axios from 'axios' -import crypto from 'crypto' -import fsAsync from 'fs/promises' -import path from 'path' -import { IronfishCommand } from '../../command' -import { LocalFlags } from '../../flags' -import { SnapshotManifest } from '../../snapshot' -import { S3Utils, TarUtils } from '../../utils' - -const SNAPSHOT_FILE_NAME = `ironfish_snapshot.tar.gz` - -export type R2Secret = { - r2AccessKeyId: string - r2SecretAccessKey: string -} - -export default class Snapshot extends IronfishCommand { - static hidden = true - - static description = `Upload chain snapshot to a public bucket` - - static flags = { - ...LocalFlags, - upload: Flags.boolean({ - default: false, - allowNo: true, - description: - 'Upload the snapshot to an S3 bucket. AWS credentials and region must be configured or set in the environment', - }), - bucket: Flags.string({ - char: 'b', - parse: (input: string) => Promise.resolve(input.trim()), - required: false, - description: 'S3 bucket to upload snapshot to', - default: 'ironfish-snapshots', - }), - path: Flags.string({ - char: 'p', - parse: (input: string): Promise => Promise.resolve(input.trim()), - required: false, - description: 'The local path where the snapshot will be saved', - }), - webhook: Flags.string({ - char: 'w', - parse: (input: string): Promise => Promise.resolve(input.trim()), - required: false, - description: 'Webhook to notify on successful snapshot upload', - }), - r2: Flags.boolean({ - default: false, - allowNo: true, - description: 'Upload the snapshot to Cloudflare R2.', - }), - } - - async start(): Promise { - const { flags } = await this.parse(Snapshot) - - const bucket = flags.bucket - - let exportDir - - if (flags.path) { - exportDir = this.sdk.fileSystem.resolve(flags.path) - } else { - exportDir = this.sdk.fileSystem.resolve(this.sdk.config.tempDir) - } - - await this.sdk.fileSystem.mkdir(exportDir, { recursive: true }) - - const node = await this.sdk.node() - await NodeUtils.waitForOpen(node) - - CliUx.ux.action.start(`Compacting chain database`) - await node.chain.blockchainDb.compact() - CliUx.ux.action.stop() - - const chainDatabasePath = this.sdk.fileSystem.resolve(this.sdk.config.chainDatabasePath) - - const timestamp = Date.now() - - const snapshotPath = this.sdk.fileSystem.join(exportDir, SNAPSHOT_FILE_NAME) - - this.log(`Zipping\n SRC ${chainDatabasePath}\n DST ${snapshotPath}\n`) - CliUx.ux.action.start(`Zipping ${chainDatabasePath}`) - await TarUtils.zipDir(chainDatabasePath + '/', snapshotPath) - const stat = await fsAsync.stat(snapshotPath) - const fileSize = stat.size - CliUx.ux.action.stop(`done (${FileUtils.formatFileSize(fileSize)})`) - - const hasher = crypto.createHash('sha256') - const fileHandle = await fsAsync.open(snapshotPath, 'r') - const stream = fileHandle.createReadStream() - - CliUx.ux.action.start(`Creating checksum for ${snapshotPath}`) - for await (const data of stream) { - hasher.update(data) - } - const checksum = hasher.digest().toString('hex') - CliUx.ux.action.stop(`done (${checksum})`) - - if (flags.upload) { - const snapshotBaseName = path.basename(SNAPSHOT_FILE_NAME, '.tar.gz') - const snapshotKeyName = `${snapshotBaseName}_${timestamp}.tar.gz` - - let s3 = new S3Client({}) - if (flags.r2) { - const r2Credentials = await S3Utils.getR2Credentials() - - if (r2Credentials === undefined) { - this.logger.log('Failed getting R2 credentials from AWS') - this.exit(1) - return - } - - s3 = S3Utils.getR2S3Client(r2Credentials) - } - - CliUx.ux.action.start(`Uploading to ${bucket}`) - await S3Utils.uploadToBucket( - s3, - snapshotPath, - 'application/x-compressed-tar', - bucket, - snapshotKeyName, - this.logger.withTag('s3'), - ) - CliUx.ux.action.stop(`done`) - - const manifestPath = this.sdk.fileSystem.join(exportDir, 'manifest.json') - const manifest: SnapshotManifest = { - block_sequence: node.chain.head.sequence, - checksum, - file_name: snapshotKeyName, - file_size: fileSize, - timestamp, - database_version: await node.chain.blockchainDb.getVersion(), - } - - await fsAsync - .writeFile(manifestPath, JSON.stringify(manifest, undefined, ' ')) - .then(async () => { - CliUx.ux.action.start(`Uploading latest snapshot information to ${bucket}`) - await S3Utils.uploadToBucket( - s3, - manifestPath, - 'application/json', - bucket, - 'manifest.json', - this.logger.withTag('s3'), - ) - CliUx.ux.action.stop(`done`) - }) - - this.log('Snapshot upload complete. Uploaded the following manifest:') - this.log(JSON.stringify(manifest, undefined, ' ')) - - if (flags.webhook) { - await axios.post(flags.webhook, { - content: `Successfully uploaded Iron Fish snapshot at block ${node.chain.head.sequence}. Use \`ironfish chain:download\` to download and import the snapshot.`, - }) - } - } - } -} diff --git a/ironfish-cli/src/commands/service/stats.ts b/ironfish-cli/src/commands/service/stats.ts deleted file mode 100644 index 186fe8c087..0000000000 --- a/ironfish-cli/src/commands/service/stats.ts +++ /dev/null @@ -1,253 +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 { CurrencyUtils, PromiseUtils } from '@ironfish/sdk' -import { WebApi } from '@ironfish/sdk' -import { Flags } from '@oclif/core' -import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' -import { IronfishCliPKG } from '../../package' -import { GossipForkCounter } from '../../utils/gossipForkCounter' - -export default class Stats extends IronfishCommand { - static hidden = true - - static description = `Submits stats to telemetry API` - - static flags = { - ...RemoteFlags, - endpoint: Flags.string({ - char: 'e', - parse: (input: string) => Promise.resolve(input.trim()), - required: false, - description: 'API host to sync to', - }), - token: Flags.string({ - char: 't', - parse: (input: string) => Promise.resolve(input.trim()), - required: false, - description: 'API host token to authenticate with', - }), - delay: Flags.integer({ - required: false, - default: 60000, - description: 'Delay (in ms) to wait before uploading stats', - }), - } - - async start(): Promise { - const { flags } = await this.parse(Stats) - - const apiHost = ( - flags.endpoint || - process.env.IRONFISH_API_HOST || - 'https://api.ironfish.network' - ).trim() - - const apiToken = (flags.token || process.env.IRONFISH_API_TOKEN || '').trim() - - const api = new WebApi({ host: apiHost, token: apiToken }) - await this.sdk.client.connect() - - // metric loops, must await last loop - void this.forks(api, flags.delay) - void this.chainDBSize(api, flags.delay) - void this.chainReorgs(api) - await this.feeRates(api, flags.delay) - } - - async forks(api: WebApi, delay: number): Promise { - let connected = false - const targetBlockTimeInSeconds = (await this.sdk.client.chain.getConsensusParameters()) - .content.targetBlockTimeInSeconds - const counter = new GossipForkCounter(targetBlockTimeInSeconds, { delayMs: delay }) - counter.start() - - setInterval(() => { - void api.submitTelemetry({ - points: [ - { - measurement: 'forks_count', - timestamp: new Date(), - fields: [ - { - name: 'forks', - type: 'integer', - value: counter.count, - }, - ], - tags: [{ name: 'version', value: IronfishCliPKG.version }], - }, - ], - }) - }, delay) - - // eslint-disable-next-line no-constant-condition - while (true) { - connected = await this.sdk.client.tryConnect() - - if (!connected) { - await PromiseUtils.sleep(1000) - continue - } - - const response = this.sdk.client.event.onGossipStream() - - for await (const value of response.contentStream()) { - counter.add(value.blockHeader) - } - } - } - - async feeRates(api: WebApi, delay: number): Promise { - // eslint-disable-next-line no-constant-condition - while (true) { - const connected = await this.sdk.client.tryConnect() - - if (!connected) { - await PromiseUtils.sleep(1000) - continue - } - - const response = await this.sdk.client.chain.estimateFeeRates() - - if (!(response.content.slow && response.content.average && response.content.fast)) { - this.log('Unexpected response') - } else { - const feeRateSlow = Number(CurrencyUtils.decode(response.content.slow)) - const feeRateAverage = Number(CurrencyUtils.decode(response.content.average)) - const feeRateFast = Number(CurrencyUtils.decode(response.content.fast)) - - await api.submitTelemetry({ - points: [ - { - measurement: 'fee_rate_estimate', - timestamp: new Date(), - fields: [ - { - name: `fee_rate_slow`, - type: 'integer', - value: feeRateSlow, - }, - { - name: `fee_rate_average`, - type: 'integer', - value: feeRateAverage, - }, - { - name: `fee_rate_fast`, - type: 'integer', - value: feeRateFast, - }, - ], - tags: [{ name: 'version', value: IronfishCliPKG.version }], - }, - ], - }) - } - - await PromiseUtils.sleep(delay) - } - } - - async chainDBSize(api: WebApi, delay: number): Promise { - // eslint-disable-next-line no-constant-condition - while (true) { - const connected = await this.sdk.client.tryConnect() - - if (!connected) { - await PromiseUtils.sleep(1000) - continue - } - - const response = await this.sdk.client.node.getStatus() - - if (!response.content.blockchain.dbSizeBytes) { - this.log(`chain DB size unexpected response`) - } else { - const chainSize = Number(response.content.blockchain.dbSizeBytes) - await api.submitTelemetry({ - points: [ - { - measurement: 'chain_db', - timestamp: new Date(), - fields: [ - { - name: `chain_db_size_bytes`, - type: 'integer', - value: chainSize, - }, - ], - tags: [{ name: 'version', value: IronfishCliPKG.version }], - }, - ], - }) - - this.log(`Chain DB size: ${chainSize}`) - } - - await PromiseUtils.sleep(delay) - } - } - - async chainReorgs(api: WebApi): Promise { - let connected = false - - // eslint-disable-next-line no-constant-condition - while (true) { - connected = await this.sdk.client.tryConnect() - - if (!connected) { - await PromiseUtils.sleep(1000) - continue - } - - const response = this.sdk.client.event.onReorganizeChainStream() - - for await (const value of response.contentStream()) { - const { oldHead, newHead, fork } = value - void api.submitTelemetry({ - points: [ - { - measurement: 'chain_reorg', - timestamp: new Date(), - fields: [ - { - name: 'old_sequence', - type: 'integer', - value: oldHead.sequence, - }, - { - name: 'old_hash', - type: 'string', - value: oldHead.hash, - }, - { - name: 'new_sequence', - type: 'integer', - value: newHead.sequence, - }, - { - name: 'new_hash', - type: 'string', - value: newHead.hash, - }, - { - name: 'fork_sequence', - type: 'integer', - value: fork.sequence, - }, - { - name: 'fork_hash', - type: 'string', - value: fork.hash, - }, - ], - tags: [{ name: 'version', value: IronfishCliPKG.version }], - }, - ], - }) - } - } - } -} diff --git a/ironfish-cli/src/commands/service/sync.ts b/ironfish-cli/src/commands/service/sync.ts deleted file mode 100644 index e28c325489..0000000000 --- a/ironfish-cli/src/commands/service/sync.ts +++ /dev/null @@ -1,170 +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 { - FollowChainStreamResponse, - Meter, - RpcClient, - TimeUtils, - Transaction, - WebApi, -} from '@ironfish/sdk' -import { Flags } from '@oclif/core' -import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' - -const NEAR_SYNC_THRESHOLD = 5 - -export default class Sync extends IronfishCommand { - static hidden = true - - static description = ` - Upload blocks and mempool transactions to an HTTP API using IronfishApi - ` - - static flags = { - ...RemoteFlags, - endpoint: Flags.string({ - char: 'e', - parse: (input: string) => Promise.resolve(input.trim()), - required: false, - description: 'API host to sync to', - }), - token: Flags.string({ - char: 't', - parse: (input: string) => Promise.resolve(input.trim()), - required: false, - description: 'API host token to authenticate with', - }), - maxUpload: Flags.integer({ - char: 'm', - required: false, - default: isNaN(Number(process.env.MAX_UPLOAD)) ? 20 : Number(process.env.MAX_UPLOAD), - description: 'The max number of blocks or transactions to sync in one batch', - }), - } - - static args = [ - { - name: 'head', - required: false, - description: 'The block hash to start following at', - }, - ] - - async start(): Promise { - 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() - - if (!apiHost) { - this.log( - `No api host found to upload blocks and transactions to. You must set IRONFISH_API_HOST env variable or pass --endpoint flag.`, - ) - this.exit(1) - } - - if (!apiToken) { - this.log( - `No api token found to auth with the API. You must set IRONFISH_API_TOKEN env variable or pass --token flag.`, - ) - this.exit(1) - } - - this.log('Connecting to node...') - - const client = await this.sdk.connectRpc() - - const api = new WebApi({ host: apiHost, token: apiToken }) - - const head = args.head as string | null - - void this.syncTransactionGossip(client, api, flags.maxUpload) - await this.syncBlocks(client, api, head, flags.maxUpload) - } - - async syncBlocks( - client: RpcClient, - api: WebApi, - head: string | null, - maxUpload: number, - ): Promise { - if (!head) { - this.log(`Fetching head from ${api.host}`) - head = await api.headBlocks() - } - - if (head) { - this.log(`Starting from head ${head}`) - } - - const response = client.chain.followChainStream(head ? { head } : undefined) - - const speed = new Meter() - speed.start() - - const buffer = new Array() - - async function commit(): Promise { - await api.blocks(buffer) - buffer.length = 0 - } - - for await (const content of response.contentStream()) { - buffer.push(content) - speed.add(1) - - // We're almost done syncing if we are within 5 sequence to the HEAD - const finishing = - Math.abs(content.head.sequence - content.block.sequence) < NEAR_SYNC_THRESHOLD - - // Should we commit the current batch? - const committing = buffer.length === maxUpload || finishing - - this.log( - `${content.type}: ${content.block.hash} - ${content.block.sequence}${ - committing - ? ' - ' + - TimeUtils.renderEstimate( - content.block.sequence, - content.head.sequence, - speed.rate5m, - ) - : '' - }`, - ) - - if (committing) { - await commit() - } - } - - await commit() - } - - async syncTransactionGossip( - client: RpcClient, - api: WebApi, - maxUpload: number, - ): Promise { - const response = client.event.onTransactionGossipStream({}) - - const buffer = new Array() - - async function commit(): Promise { - await api.transactions(buffer) - buffer.length = 0 - } - - for await (const content of response.contentStream()) { - buffer.push(new Transaction(Buffer.from(content.serializedTransaction, 'hex'))) - - if (buffer.length === maxUpload) { - await commit() - } - } - - await commit() - } -} diff --git a/ironfish-cli/src/commands/start.ts b/ironfish-cli/src/commands/start.ts index 10cb4aba04..0b2d63c0ed 100644 --- a/ironfish-cli/src/commands/start.ts +++ b/ironfish-cli/src/commands/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 { BoxKeyPair } from '@ironfish/rust-nodejs' -import { Assert, IronfishNode, NodeUtils, PrivateIdentity, PromiseUtils } from '@ironfish/sdk' +import { Assert, FullNode, NodeUtils, PromiseUtils } from '@ironfish/sdk' import { Flags } from '@oclif/core' import inspector from 'node:inspector' import { v4 as uuid } from 'uuid' @@ -120,7 +119,7 @@ export default class Start extends IronfishCommand { }), } - node: IronfishNode | null = null + node: FullNode | null = null /** * This promise is used to wait until start is finished beforer closeFromSignal continues @@ -214,9 +213,7 @@ export default class Start extends IronfishCommand { await this.sdk.internal.save() } - const privateIdentity = this.getPrivateIdentity() - - const node = await this.sdk.node({ privateIdentity: privateIdentity }) + const node = await this.sdk.node({ privateIdentity: this.sdk.getPrivateIdentity() }) const nodeName = this.sdk.config.get('nodeName').trim() || null const blockGraffiti = this.sdk.config.get('blockGraffiti').trim() || null @@ -285,7 +282,7 @@ export default class Start extends IronfishCommand { /** * Information displayed the first time a node is running */ - async firstRun(node: IronfishNode): Promise { + async firstRun(node: FullNode): Promise { this.log('') this.log('Thank you for installing the Iron Fish Node.') @@ -308,7 +305,7 @@ export default class Start extends IronfishCommand { /** * Information displayed if there is no default account for the node */ - async setDefaultAccount(node: IronfishNode): Promise { + async setDefaultAccount(node: FullNode): Promise { if (!node.wallet.accountExists(DEFAULT_ACCOUNT_NAME)) { const account = await node.wallet.createAccount(DEFAULT_ACCOUNT_NAME, { setDefault: true, @@ -324,15 +321,4 @@ export default class Start extends IronfishCommand { this.log('') await node.internal.save() } - - getPrivateIdentity(): PrivateIdentity | undefined { - const networkIdentity = this.sdk.internal.get('networkIdentity') - if ( - !this.sdk.config.get('generateNewIdentity') && - networkIdentity !== undefined && - networkIdentity.length > 31 - ) { - return BoxKeyPair.fromHex(networkIdentity) - } - } } diff --git a/ironfish-cli/src/commands/stop.ts b/ironfish-cli/src/commands/stop.ts index d78eae7d6e..224364f9e1 100644 --- a/ironfish-cli/src/commands/stop.ts +++ b/ironfish-cli/src/commands/stop.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 { IronfishNode } from '@ironfish/sdk' +import { FullNode } from '@ironfish/sdk' import { CliUx } from '@oclif/core' import { IronfishCommand } from '../command' import { RemoteFlags } from '../flags' @@ -13,7 +13,7 @@ export default class StopCommand extends IronfishCommand { ...RemoteFlags, } - node: IronfishNode | null = null + node: FullNode | null = null async start(): Promise { await this.parse(StopCommand) diff --git a/ironfish-cli/src/commands/wallet/address.ts b/ironfish-cli/src/commands/wallet/address.ts index 36334f7a16..04bfd6891d 100644 --- a/ironfish-cli/src/commands/wallet/address.ts +++ b/ironfish-cli/src/commands/wallet/address.ts @@ -16,7 +16,6 @@ export class AddressCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'Name of the account to get the address for', }, diff --git a/ironfish-cli/src/commands/wallet/assets.ts b/ironfish-cli/src/commands/wallet/assets.ts index 03b9f878ca..288721d983 100644 --- a/ironfish-cli/src/commands/wallet/assets.ts +++ b/ironfish-cli/src/commands/wallet/assets.ts @@ -34,7 +34,6 @@ export class AssetsCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'Name of the account', }, diff --git a/ironfish-cli/src/commands/wallet/balance.ts b/ironfish-cli/src/commands/wallet/balance.ts index de0548a00a..25e689d402 100644 --- a/ironfish-cli/src/commands/wallet/balance.ts +++ b/ironfish-cli/src/commands/wallet/balance.ts @@ -37,7 +37,6 @@ export class BalanceCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'Name of the account to get balance for', }, diff --git a/ironfish-cli/src/commands/wallet/balances.ts b/ironfish-cli/src/commands/wallet/balances.ts index 69214d748b..f94f6a54e5 100644 --- a/ironfish-cli/src/commands/wallet/balances.ts +++ b/ironfish-cli/src/commands/wallet/balances.ts @@ -28,7 +28,6 @@ export class BalancesCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'Name of the account to get balances for', }, diff --git a/ironfish-cli/src/commands/wallet/create.ts b/ironfish-cli/src/commands/wallet/create.ts index 734095f588..71b687b0da 100644 --- a/ironfish-cli/src/commands/wallet/create.ts +++ b/ironfish-cli/src/commands/wallet/create.ts @@ -12,7 +12,6 @@ export class CreateCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'Name of the account', }, diff --git a/ironfish-cli/src/commands/wallet/export.ts b/ironfish-cli/src/commands/wallet/export.ts index 4ae1a11bcf..fd47c1e7f0 100644 --- a/ironfish-cli/src/commands/wallet/export.ts +++ b/ironfish-cli/src/commands/wallet/export.ts @@ -45,7 +45,6 @@ export class ExportCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'Name of the account to export', }, diff --git a/ironfish-cli/src/commands/wallet/notes.ts b/ironfish-cli/src/commands/wallet/notes.ts index cf66042487..67b435cac2 100644 --- a/ironfish-cli/src/commands/wallet/notes.ts +++ b/ironfish-cli/src/commands/wallet/notes.ts @@ -19,7 +19,6 @@ export class NotesCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'Name of the account to get notes for', }, @@ -47,9 +46,6 @@ export class NotesCommand extends IronfishCommand { sender: { header: 'Sender', }, - noteHash: { - header: 'Note Hash', - }, transactionHash: { header: 'From Transaction', }, @@ -59,7 +55,7 @@ export class NotesCommand extends IronfishCommand { if (row.spent === undefined) { return '-' } else { - return row.spent ? `✔` : `` + return row.spent ? `✔` : `x` } }, }, @@ -69,6 +65,9 @@ export class NotesCommand extends IronfishCommand { get: (row) => CurrencyUtils.renderIron(row.value), minWidth: 16, }, + noteHash: { + header: 'Note Hash', + }, nullifier: { header: 'Nullifier', get: (row) => { diff --git a/ironfish-cli/src/commands/wallet/post.ts b/ironfish-cli/src/commands/wallet/post.ts index cc9f78d96f..7a0c1b2d5f 100644 --- a/ironfish-cli/src/commands/wallet/post.ts +++ b/ironfish-cli/src/commands/wallet/post.ts @@ -43,7 +43,6 @@ export class PostCommand extends IronfishCommand { { name: 'transaction', required: true, - parse: (input: string): Promise => Promise.resolve(input.trim()), description: 'The raw transaction in hex encoding', }, ] diff --git a/ironfish-cli/src/commands/wallet/prune.ts b/ironfish-cli/src/commands/wallet/prune.ts index d1611886e4..5e2e25e94a 100644 --- a/ironfish-cli/src/commands/wallet/prune.ts +++ b/ironfish-cli/src/commands/wallet/prune.ts @@ -35,7 +35,7 @@ export default class PruneCommand extends IronfishCommand { const { flags } = await this.parse(PruneCommand) CliUx.ux.action.start(`Opening node`) - const node = await this.sdk.walletNode({ connectNodeClient: false }) + const node = await this.sdk.node() await NodeUtils.waitForOpen(node) CliUx.ux.action.stop('Done.') diff --git a/ironfish-cli/src/commands/wallet/rename.ts b/ironfish-cli/src/commands/wallet/rename.ts index 4a7fa19ec7..986cea4db8 100644 --- a/ironfish-cli/src/commands/wallet/rename.ts +++ b/ironfish-cli/src/commands/wallet/rename.ts @@ -10,13 +10,11 @@ export class RenameCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: true, description: 'Name of the account to rename', }, { name: 'new-name', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: true, description: 'New name to assign to the account', }, diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index bfb675e753..8877381fce 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -90,6 +90,11 @@ export class Send extends IronfishCommand { default: false, description: 'Allow offline transaction creation', }), + note: Flags.string({ + char: 'n', + description: 'The note hashes to include in the transaction', + multiple: true, + }), } async start(): Promise { @@ -189,6 +194,7 @@ export class Send extends IronfishCommand { feeRate: flags.feeRate ? CurrencyUtils.encode(flags.feeRate) : null, expiration: flags.expiration, confirmations: flags.confirmations, + notes: flags.note, } let raw: RawTransaction diff --git a/ironfish-cli/src/commands/wallet/transaction/index.ts b/ironfish-cli/src/commands/wallet/transaction/index.ts index 671f330c3f..c494b58cc6 100644 --- a/ironfish-cli/src/commands/wallet/transaction/index.ts +++ b/ironfish-cli/src/commands/wallet/transaction/index.ts @@ -29,7 +29,6 @@ export class TransactionCommand extends IronfishCommand { }, { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'Name of the account', }, diff --git a/ironfish-cli/src/commands/wallet/transaction/watch.ts b/ironfish-cli/src/commands/wallet/transaction/watch.ts index 6b8663b9c9..f1726a7bff 100644 --- a/ironfish-cli/src/commands/wallet/transaction/watch.ts +++ b/ironfish-cli/src/commands/wallet/transaction/watch.ts @@ -26,7 +26,6 @@ export class WatchTxCommand extends IronfishCommand { }, { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'Name of the account', }, diff --git a/ironfish-cli/src/commands/wallet/transactions.ts b/ironfish-cli/src/commands/wallet/transactions.ts index 7f88f922a7..e5359a7ef2 100644 --- a/ironfish-cli/src/commands/wallet/transactions.ts +++ b/ironfish-cli/src/commands/wallet/transactions.ts @@ -49,7 +49,6 @@ export class TransactionsCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: false, description: 'Name of the account', }, diff --git a/ironfish-cli/src/commands/wallet/use.ts b/ironfish-cli/src/commands/wallet/use.ts index a77710963f..f1187affab 100644 --- a/ironfish-cli/src/commands/wallet/use.ts +++ b/ironfish-cli/src/commands/wallet/use.ts @@ -10,7 +10,6 @@ export class UseCommand extends IronfishCommand { static args = [ { name: 'account', - parse: (input: string): Promise => Promise.resolve(input.trim()), required: true, description: 'Name of the account', }, diff --git a/ironfish-cli/src/typedefs/blru.ts b/ironfish-cli/src/typedefs/blru.d.ts similarity index 100% rename from ironfish-cli/src/typedefs/blru.ts rename to ironfish-cli/src/typedefs/blru.d.ts diff --git a/ironfish-cli/src/types.ts b/ironfish-cli/src/types.ts index e6ec0dc287..ee091e2e78 100644 --- a/ironfish-cli/src/types.ts +++ b/ironfish-cli/src/types.ts @@ -5,11 +5,11 @@ import { Interfaces } from '@oclif/core' export interface ProgressBar { - progress: VoidFunction + progress: () => void getTotal(): number setTotal(totalValue: number): void start(totalValue?: number, startValue?: number, payload?: Record): void - stop: VoidFunction + stop: () => void update(currentValue?: number, payload?: Record): void update(payload?: Record): void increment(delta?: number, payload?: Record): void diff --git a/ironfish-cli/tsconfig.json b/ironfish-cli/tsconfig.json index 142cf4a4f0..ad0746eaa9 100644 --- a/ironfish-cli/tsconfig.json +++ b/ironfish-cli/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../config/tsconfig.base.json", "compilerOptions": { + "lib": ["es2020"], "outDir": "build", "rootDir": "./", "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo" diff --git a/ironfish-cli/tsconfig.test.json b/ironfish-cli/tsconfig.test.json index 33d207ff42..df85e8e540 100644 --- a/ironfish-cli/tsconfig.test.json +++ b/ironfish-cli/tsconfig.test.json @@ -1,6 +1,7 @@ { "extends": "../config/tsconfig.base.json", "compilerOptions": { + "lib": ["es2020"], "noEmit": true }, "include": [], diff --git a/ironfish-rust-nodejs/npm/darwin-arm64/package.json b/ironfish-rust-nodejs/npm/darwin-arm64/package.json index 4e86d4b890..14e66760d7 100644 --- a/ironfish-rust-nodejs/npm/darwin-arm64/package.json +++ b/ironfish-rust-nodejs/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-darwin-arm64", - "version": "1.10.0", + "version": "1.11.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/darwin-x64/package.json b/ironfish-rust-nodejs/npm/darwin-x64/package.json index 39483ca196..da58f558ae 100644 --- a/ironfish-rust-nodejs/npm/darwin-x64/package.json +++ b/ironfish-rust-nodejs/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-darwin-x64", - "version": "1.10.0", + "version": "1.11.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json b/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json index 186e162205..e71c3c13d9 100644 --- a/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json +++ b/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-arm64-gnu", - "version": "1.10.0", + "version": "1.11.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json b/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json index b68ba56bdf..c4d9ca989e 100644 --- a/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json +++ b/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-arm64-musl", - "version": "1.10.0", + "version": "1.11.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json b/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json index 78dd89e4f3..2d6a6d3a20 100644 --- a/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json +++ b/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-x64-gnu", - "version": "1.10.0", + "version": "1.11.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-x64-musl/package.json b/ironfish-rust-nodejs/npm/linux-x64-musl/package.json index 0096e2c23e..fd40bb84d6 100644 --- a/ironfish-rust-nodejs/npm/linux-x64-musl/package.json +++ b/ironfish-rust-nodejs/npm/linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-x64-musl", - "version": "1.10.0", + "version": "1.11.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json b/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json index f37069285f..6ab1b0ca93 100644 --- a/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json +++ b/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-win32-x64-msvc", - "version": "1.10.0", + "version": "1.11.0", "os": [ "win32" ], diff --git a/ironfish-rust-nodejs/package.json b/ironfish-rust-nodejs/package.json index d7e15bb17b..b3c2a867fe 100644 --- a/ironfish-rust-nodejs/package.json +++ b/ironfish-rust-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs", - "version": "1.10.0", + "version": "1.11.0", "description": "Node.js bindings for Rust code required by the Iron Fish SDK", "main": "index.js", "types": "index.d.ts", @@ -34,11 +34,10 @@ }, "devDependencies": { "@napi-rs/cli": "2.16.1", - "@types/jest": "29.2.4", - "jest": "29.3.1", - "jest-jasmine2": "29.3.1", + "@types/jest": "29.5.8", + "jest": "29.7.0", "rimraf": "3.0.2", - "ts-jest": "29.0.3", - "typescript": "4.3.4" + "ts-jest": "29.1.1", + "typescript": "5.0.4" } } diff --git a/ironfish/README.md b/ironfish/README.md index 67f0ef42c1..b620f3e67b 100644 --- a/ironfish/README.md +++ b/ironfish/README.md @@ -39,7 +39,7 @@ 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 IronfishNode, Blockchain, Config, Accounts. 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 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 diff --git a/ironfish/jest.setup.env.js b/ironfish/jest.setup.env.js index 47be447fea..af5c4d3d97 100644 --- a/ironfish/jest.setup.env.js +++ b/ironfish/jest.setup.env.js @@ -4,28 +4,6 @@ const consola = require('consola') const { initializeSapling } = require('@ironfish/rust-nodejs') -jest.mock('node-datachannel', () => { - return { - PeerConnection: class { - onLocalDescription() {} - onLocalCandidate() {} - onDataChannel() {} - createDataChannel() { - return { - onOpen: () => {}, - onError: () => {}, - onClosed: () => {}, - onMessage: () => {}, - close: () => {}, - isOpen: () => {}, - sendMessage: () => {}, - sendMessageBinary: (_buffer) => {}, - } - } - }, - } -}) - beforeAll(() => { // This causes Sapling to be initialized, which is 1 time 2 second cost for each test suite if (process.env.TEST_INIT_RUST) { diff --git a/ironfish/package.json b/ironfish/package.json index dbe91de743..1acaedb9f9 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "1.12.0", + "version": "1.13.0", "description": "SDK for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -22,7 +22,7 @@ "dependencies": { "@ethersproject/bignumber": "5.7.0", "@fast-csv/format": "4.3.5", - "@ironfish/rust-nodejs": "1.10.0", + "@ironfish/rust-nodejs": "1.11.0", "@napi-rs/blake-hash": "1.3.3", "axios": "0.21.4", "bech32": "2.0.0", @@ -41,7 +41,7 @@ "leveldown": "5.6.0", "levelup": "4.4.0", "lodash": "4.17.21", - "node-datachannel": "0.4.3", + "node-datachannel": "0.5.1", "node-forge": "1.3.1", "parse-json": "5.2.0", "sqlite": "4.0.23", @@ -57,21 +57,21 @@ "lint": "tsc -b && tsc -b tsconfig.test.json && eslint --ext .ts,.tsx,.js,.jsx src/", "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 --testTimeout=${JEST_TIMEOUT:-5000}", - "test:slow": "tsc -b && tsc -b tsconfig.test.json && cross-env TEST_INIT_RUST=true jest --testMatch \"**/*.test.slow.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-60000}", - "test:perf": "tsc -b && tsc -b tsconfig.test.json && cross-env TEST_INIT_RUST=true jest --testMatch \"**/*.test.perf.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-600000} --runInBand", - "test:perf:report": "tsc -b && tsc -b tsconfig.test.json && cross-env TEST_INIT_RUST=true GENERATE_TEST_REPORT=true jest --config jest.config.js --testMatch \"**/*.test.perf.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-600000} --ci", - "test:coverage:html": "tsc -b tsconfig.test.json && jest --testPathIgnorePatterns --coverage --coverage-reporters html", - "test:watch": "tsc -b tsconfig.test.json && jest --watch --coverage false", - "fixtures:regenerate": "find . -name \"__fixtures__\" | xargs rm -rf && JEST_TIMEOUT=1000000000 yarn run test && JEST_TIMEOUT=1000000000 yarn run test:slow && JEST_TIMEOUT=1000000000 yarn run test:perf" + "test": "tsc -b && tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules jest --testTimeout=${JEST_TIMEOUT:-5000}", + "test:slow": "tsc -b && tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules TEST_INIT_RUST=true jest --testMatch \"**/*.test.slow.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-60000}", + "test:perf": "tsc -b && tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules TEST_INIT_RUST=true jest --testMatch \"**/*.test.perf.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-600000} --runInBand", + "test:perf:report": "tsc -b && tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules TEST_INIT_RUST=true GENERATE_TEST_REPORT=true jest --config jest.config.js --testMatch \"**/*.test.perf.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-600000} --ci", + "test:coverage:html": "tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns --coverage --coverage-reporters html", + "test:watch": "tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch --coverage false", + "fixtures:regenerate": "find . -name \"__fixtures__\" | xargs rm -rf && NODE_OPTIONS=--experimental-vm-modules JEST_TIMEOUT=1000000000 yarn run test && NODE_OPTIONS=--experimental-vm-modules JEST_TIMEOUT=1000000000 yarn run test:slow && NODE_OPTIONS=--experimental-vm-modules JEST_TIMEOUT=1000000000 yarn run test:perf" }, "devDependencies": { "@jest/reporters": "29.3.1", - "@jest/types": "29.5.0", + "@jest/types": "29.6.3", "@types/buffer-json": "2.0.0", "@types/colors": "1.2.1", "@types/imurmurhash": "0.1.1", - "@types/jest": "29.5.2", + "@types/jest": "29.5.8", "@types/leveldown": "4.0.2", "@types/levelup": "4.3.0", "@types/lodash": "4.14.170", @@ -89,13 +89,13 @@ "eslint-plugin-jest": "27.1.6", "eslint-plugin-prettier": "3.4.0", "eslint-plugin-react-hooks": "4.2.0", - "jest": "29.5.0", - "jest-jasmine2": "29.3.1", + "jest": "29.7.0", + "jest-jasmine2": "29.7.0", "mitm": "1.7.2", "prettier": "2.3.2", - "ts-jest": "29.0.3", + "ts-jest": "29.1.1", "ts-node": "10.9.1", - "typescript": "4.3.5" + "typescript": "5.0.4" }, "bugs": { "url": "https://github.com/iron-fish/ironfish/issues" diff --git a/ironfish/src/assert.ts b/ironfish/src/assert.ts index c102e37a65..9d997298fa 100644 --- a/ironfish/src/assert.ts +++ b/ironfish/src/assert.ts @@ -100,4 +100,16 @@ export class Assert { throw new Error(message || 'Expected buffers to have the same contents') } } + + static hasKeys, TKeys extends keyof TObj>( + obj: TObj, + keys: TKeys[], + message?: string, + ): asserts obj is TKeys extends keyof TObj ? Required> : never { + const missing = keys.filter((key) => !(key in obj)) + + if (missing.length) { + throw new Error(message || `Expected value to have keys ${String(missing)}`) + } + } } diff --git a/ironfish/src/fileStores/config.ts b/ironfish/src/fileStores/config.ts index 71cfb86e2c..7177d082a9 100644 --- a/ironfish/src/fileStores/config.ts +++ b/ironfish/src/fileStores/config.ts @@ -305,19 +305,8 @@ export type ConfigOptions = { walletGossipTransactionsMaxQueueSize: number /** - * Enable standalone wallet process to connect to a node via IPC + * The max number of transactions to process at one time when syncing */ - walletNodeIpcEnabled: boolean - walletNodeIpcPath: string - - /** - * Enable standalone wallet process to connect to a node via TCP - */ - walletNodeTcpEnabled: boolean - walletNodeTcpHost: string - walletNodeTcpPort: number - walletNodeTlsEnabled: boolean - walletNodeRpcAuthToken: string walletSyncingMaxQueueSize: number } @@ -401,13 +390,6 @@ export const ConfigOptionsSchema: yup.ObjectSchema> = yup networkDefinitionPath: yup.string().trim(), incomingWebSocketWhitelist: yup.array(yup.string().trim().defined()), walletGossipTransactionsMaxQueueSize: yup.number(), - walletNodeIpcEnabled: yup.boolean(), - walletNodeIpcPath: yup.string(), - walletNodeTcpEnabled: yup.boolean(), - walletNodeTcpHost: yup.string(), - walletNodeTcpPort: yup.number(), - walletNodeTlsEnabled: yup.boolean(), - walletNodeRpcAuthToken: yup.string(), walletSyncingMaxQueueSize: yup.number(), }) .defined() @@ -504,14 +486,7 @@ export class Config extends KeyStore { memPoolRecentlyEvictedCacheSize: 60000, networkDefinitionPath: files.resolve(files.join(dataDir, 'network.json')), incomingWebSocketWhitelist: [], - walletNodeIpcEnabled: false, - walletNodeIpcPath: '', walletGossipTransactionsMaxQueueSize: 1000, - walletNodeTcpEnabled: false, - walletNodeTcpHost: '', - walletNodeTcpPort: 8020, - walletNodeTlsEnabled: true, - walletNodeRpcAuthToken: '', walletSyncingMaxQueueSize: 100, } } diff --git a/ironfish/src/fileStores/peerStore.ts b/ironfish/src/fileStores/peerStore.ts index 3078790612..5a59230010 100644 --- a/ironfish/src/fileStores/peerStore.ts +++ b/ironfish/src/fileStores/peerStore.ts @@ -3,10 +3,15 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { FileSystem } from '../fileSystems' import { createRootLogger, Logger } from '../logger' -import { PeerAddress } from '../network/peers/peerAddress' -import { ParseJsonError } from '../utils/json' import { KeyStore } from './keyStore' +export type PeerAddress = { + address: string + port: number + name: string | null + lastAddedTimestamp: number +} + export type PeerStoreOptions = { priorPeers: PeerAddress[] } @@ -28,18 +33,19 @@ export class PeerStore extends KeyStore { this.logger = createRootLogger() } - async load(): Promise { - try { - await super.load() - } catch (e) { - if (e instanceof ParseJsonError) { - this.logger.debug( - `Error: Could not parse JSON at ${this.storage.configPath}, overwriting file.`, - ) - await super.save() - } else { - throw e + getPriorPeers(): PeerAddress[] { + // Checking for null values in case the file is an older version + return this.getArray('priorPeers').flatMap((peer) => { + if (peer.address === null || peer.port === null) { + return [] + } + + return { + address: peer.address, + port: peer.port, + name: peer.name, + lastAddedTimestamp: peer.lastAddedTimestamp ?? 0, } - } + }) } } diff --git a/ironfish/src/index.ts b/ironfish/src/index.ts index efa7aa0697..de55be0f14 100644 --- a/ironfish/src/index.ts +++ b/ironfish/src/index.ts @@ -30,5 +30,4 @@ export * from './network' export * from './package' export * from './platform' export * from './primitives' -export * from './webApi' export { getFeeRate } from './memPool' diff --git a/ironfish/src/memPool/feeEstimator.ts b/ironfish/src/memPool/feeEstimator.ts index cd78ab4e66..2411e5e74a 100644 --- a/ironfish/src/memPool/feeEstimator.ts +++ b/ironfish/src/memPool/feeEstimator.ts @@ -49,7 +49,7 @@ export class FeeEstimator { logger?: Logger percentiles?: PriorityLevelPercentiles }) { - this.logger = options.logger || createRootLogger().withTag('recentFeeCache') + this.logger = (options.logger ?? createRootLogger()).withTag('recentFeeCache') this.maxBlockHistory = options.maxBlockHistory ?? this.maxBlockHistory this.consensus = options.consensus diff --git a/ironfish/src/memPool/recentlyEvictedCache.ts b/ironfish/src/memPool/recentlyEvictedCache.ts index c82bb4a4ff..f3d119b709 100644 --- a/ironfish/src/memPool/recentlyEvictedCache.ts +++ b/ironfish/src/memPool/recentlyEvictedCache.ts @@ -203,7 +203,7 @@ export class RecentlyEvictedCache { toFlush = this.removeAtSequenceQueue.peek() } - this.logger?.debug( + this.logger.debug( `Flushed ${flushCount} transactions from RecentlyEvictedCache after adding block ${sequence}`, ) diff --git a/ironfish/src/metrics/cpuMeter.ts b/ironfish/src/metrics/cpuMeter.ts index f546e8b98c..542fcd1e17 100644 --- a/ironfish/src/metrics/cpuMeter.ts +++ b/ironfish/src/metrics/cpuMeter.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 os from 'os' -import { HRTime } from '../utils' +import { HRTime, SetIntervalToken } from '../utils' import { RollingAverage } from './rollingAverage' /** @@ -16,7 +16,7 @@ export class CPUMeter { private _current = 0 private _intervalMs: number private _started = false - private _interval: NodeJS.Timer | null = null + private _interval: SetIntervalToken | null = null private _lastReading: { time: HRTime osCpu: os.CpuInfo[] diff --git a/ironfish/src/migrations/data/000-template.ts b/ironfish/src/migrations/data/000-template.ts index dc1a2bb21c..4ccdc5080b 100644 --- a/ironfish/src/migrations/data/000-template.ts +++ b/ironfish/src/migrations/data/000-template.ts @@ -5,23 +5,22 @@ import { generateKeyFromPrivateKey } from '@ironfish/rust-nodejs' import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' import { createDB } from '../../storage/utils' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetStores } from './000-template/stores' export class Migration000 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { + prepare(context: MigrationContext): IDatabase { /* replace line below with node.chain.location if applying migration to the blockchain * database */ - return createDB({ location: node.config.walletDatabasePath }) + return createDB({ location: context.config.walletDatabasePath }) } async forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, @@ -54,7 +53,7 @@ export class Migration000 extends Migration { * Writing a backwards migration is optional but suggested */ async backward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, diff --git a/ironfish/src/migrations/data/014-blockchain.ts b/ironfish/src/migrations/data/014-blockchain.ts index 1565fc63c3..1dc860aba0 100644 --- a/ironfish/src/migrations/data/014-blockchain.ts +++ b/ironfish/src/migrations/data/014-blockchain.ts @@ -6,17 +6,16 @@ import { Assert } from '../../assert' import { FullNode } from '../../node' import { IDatabase } from '../../storage' import { createDB } from '../../storage/utils' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' export class Migration014 extends Migration { path = __filename database = Database.BLOCKCHAIN - async prepare(node: IronfishNode): Promise { - Assert.isInstanceOf(node, FullNode) - await node.files.mkdir(node.chain.location, { recursive: true }) - return createDB({ location: node.chain.location }) + async prepare(context: MigrationContext): Promise { + Assert.isInstanceOf(context, FullNode) + await context.files.mkdir(context.chain.location, { recursive: true }) + return createDB({ location: context.chain.location }) } // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/ironfish/src/migrations/data/015-wallet.ts b/ironfish/src/migrations/data/015-wallet.ts index 48943219bc..54320f8d53 100644 --- a/ironfish/src/migrations/data/015-wallet.ts +++ b/ironfish/src/migrations/data/015-wallet.ts @@ -4,16 +4,15 @@ import { IDatabase } from '../../storage' import { createDB } from '../../storage/utils' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' export class Migration015 extends Migration { path = __filename database = Database.WALLET - async prepare(node: IronfishNode): Promise { - await node.files.mkdir(node.config.walletDatabasePath, { recursive: true }) - return createDB({ location: node.config.walletDatabasePath }) + async prepare(context: MigrationContext): Promise { + await context.files.mkdir(context.config.walletDatabasePath, { recursive: true }) + return createDB({ location: context.config.walletDatabasePath }) } // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/ironfish/src/migrations/data/016-sequence-to-tx.ts b/ironfish/src/migrations/data/016-sequence-to-tx.ts index 7295027039..b85a4a46c5 100644 --- a/ironfish/src/migrations/data/016-sequence-to-tx.ts +++ b/ironfish/src/migrations/data/016-sequence-to-tx.ts @@ -4,25 +4,24 @@ import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetOldAccounts } from './021-add-version-to-accounts/schemaOld' export class Migration016 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return node.wallet.walletDb.db + prepare(context: MigrationContext): IDatabase { + return context.wallet.walletDb.db } async forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, ): Promise { - const accounts = await GetOldAccounts(node, db, tx) + const accounts = await GetOldAccounts(context, db, tx) logger.info(`Indexing on-chain transactions for ${accounts.length} accounts`) @@ -37,7 +36,7 @@ export class Migration016 extends Migration { continue } - await node.wallet.walletDb.saveSequenceToTransactionHash( + await context.wallet.walletDb.saveSequenceToTransactionHash( account, transaction.sequence, transaction.transaction.hash(), @@ -50,7 +49,7 @@ export class Migration016 extends Migration { } } - async backward(node: IronfishNode): Promise { - await node.wallet.walletDb.sequenceToTransactionHash.clear() + async backward(context: MigrationContext): Promise { + await context.wallet.walletDb.sequenceToTransactionHash.clear() } } diff --git a/ironfish/src/migrations/data/017-sequence-encoding.ts b/ironfish/src/migrations/data/017-sequence-encoding.ts index 464d09793d..899110cd36 100644 --- a/ironfish/src/migrations/data/017-sequence-encoding.ts +++ b/ironfish/src/migrations/data/017-sequence-encoding.ts @@ -13,25 +13,24 @@ import { PrefixEncoding, U32_ENCODING_BE, } from '../../storage' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetOldAccounts } from './021-add-version-to-accounts/schemaOld' export class Migration017 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return node.wallet.walletDb.db + prepare(context: MigrationContext): IDatabase { + return context.wallet.walletDb.db } async forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, ): Promise { - const accounts = await GetOldAccounts(node, db, tx) + const accounts = await GetOldAccounts(context, db, tx) logger.info(`Re-indexing transactions for ${accounts.length} accounts`) logger.info('') @@ -41,7 +40,7 @@ export class Migration017 extends Migration { logger.info(`Indexing on-chain transactions for account ${account.name}`) for await (const transactionValue of account.getTransactions()) { - await node.wallet.walletDb.saveTransaction( + await context.wallet.walletDb.saveTransaction( account, transactionValue.transaction.hash(), transactionValue, @@ -63,8 +62,8 @@ export class Migration017 extends Migration { await pendingTransactionHashes.clear() } - async backward(node: IronfishNode, db: IDatabase): Promise { - const accounts = await GetOldAccounts(node, db) + async backward(context: MigrationContext, db: IDatabase): Promise { + const accounts = await GetOldAccounts(context, db) const { sequenceToNoteHash, sequenceToTransactionHash, pendingTransactionHashes } = this.getOldStores(db) @@ -99,9 +98,9 @@ export class Migration017 extends Migration { } } - await node.wallet.walletDb.sequenceToNoteHash.clear() - await node.wallet.walletDb.sequenceToTransactionHash.clear() - await node.wallet.walletDb.pendingTransactionHashes.clear() + await context.wallet.walletDb.sequenceToNoteHash.clear() + await context.wallet.walletDb.sequenceToTransactionHash.clear() + await context.wallet.walletDb.pendingTransactionHashes.clear() } getOldStores(db: IDatabase): { diff --git a/ironfish/src/migrations/data/018-backfill-wallet-assets.ts b/ironfish/src/migrations/data/018-backfill-wallet-assets.ts index 82a8f7a122..17b95f59ad 100644 --- a/ironfish/src/migrations/data/018-backfill-wallet-assets.ts +++ b/ironfish/src/migrations/data/018-backfill-wallet-assets.ts @@ -3,29 +3,28 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetOldAccounts } from './021-add-version-to-accounts/schemaOld' export class Migration018 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return node.wallet.walletDb.db + prepare(context: MigrationContext): IDatabase { + return context.wallet.walletDb.db } async forward( - node: IronfishNode, + context: MigrationContext, _db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, ): Promise { // Ensure there are no corrupted records for users who might have failed // running this migration - await node.wallet.walletDb.assets.clear() + await context.wallet.walletDb.assets.clear() - const accounts = await GetOldAccounts(node, _db, tx) + const accounts = await GetOldAccounts(context, _db, tx) logger.info(`Backfilling assets for ${accounts.length} accounts`) @@ -50,7 +49,7 @@ export class Migration018 extends Migration { logger.info('') } - async backward(node: IronfishNode): Promise { - await node.wallet.walletDb.assets.clear() + async backward(context: MigrationContext): Promise { + await context.wallet.walletDb.assets.clear() } } diff --git a/ironfish/src/migrations/data/019-backfill-wallet-assets-from-chain.ts b/ironfish/src/migrations/data/019-backfill-wallet-assets-from-chain.ts index 101ce370fe..1e1b24949c 100644 --- a/ironfish/src/migrations/data/019-backfill-wallet-assets-from-chain.ts +++ b/ironfish/src/migrations/data/019-backfill-wallet-assets-from-chain.ts @@ -8,26 +8,26 @@ import { Logger } from '../../logger' import { FullNode } from '../../node' import { BUFFER_ENCODING, IDatabase, IDatabaseStore, IDatabaseTransaction } from '../../storage' import { createDB } from '../../storage/utils' -import { BufferUtils, IronfishNode } from '../../utils' +import { BufferUtils } from '../../utils' import { Account } from '../../wallet' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetOldAccounts } from './021-add-version-to-accounts/schemaOld' export class Migration019 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return node.wallet.walletDb.db + prepare(context: MigrationContext): IDatabase { + return context.wallet.walletDb.db } async forward( - node: IronfishNode, + context: MigrationContext, _db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, ): Promise { - const accounts = await GetOldAccounts(node, _db, tx) + const accounts = await GetOldAccounts(context, _db, tx) logger.info(`Backfilling assets for ${accounts.length} accounts`) @@ -40,7 +40,7 @@ export class Migration019 extends Migration { const assets = [] for await (const { note, sequence, blockHash: hash } of account.getNotes()) { - const asset = await node.wallet.walletDb.getAsset(account, note.assetId(), tx) + const asset = await context.wallet.walletDb.getAsset(account, note.assetId(), tx) if (!asset) { assets.push({ id: note.assetId(), sequence, hash }) } @@ -50,8 +50,8 @@ export class Migration019 extends Migration { } if (assetsToBackfill.length) { - Assert.isInstanceOf(node, FullNode) - const chainDb = createDB({ location: node.config.chainDatabasePath }) + Assert.isInstanceOf(context, FullNode) + const chainDb = createDB({ location: context.config.chainDatabasePath }) await chainDb.open() const chainAssets: IDatabaseStore = chainDb.addStore({ diff --git a/ironfish/src/migrations/data/020-backfill-null-asset-supplies.ts b/ironfish/src/migrations/data/020-backfill-null-asset-supplies.ts index 175bfe4bff..c42f89ba96 100644 --- a/ironfish/src/migrations/data/020-backfill-null-asset-supplies.ts +++ b/ironfish/src/migrations/data/020-backfill-null-asset-supplies.ts @@ -3,25 +3,25 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' -import { BufferUtils, IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { BufferUtils } from '../../utils' +import { Database, Migration, MigrationContext } from '../migration' import { GetOldAccounts } from './021-add-version-to-accounts/schemaOld' export class Migration020 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return node.wallet.walletDb.db + prepare(context: MigrationContext): IDatabase { + return context.wallet.walletDb.db } async forward( - node: IronfishNode, + context: MigrationContext, _db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, ): Promise { - const accounts = await GetOldAccounts(node, _db, tx) + const accounts = await GetOldAccounts(context, _db, tx) logger.info(`Backfilling assets for ${accounts.length} accounts`) @@ -36,7 +36,7 @@ export class Migration020 extends Migration { } logger.info(` Re-syncing asset ${BufferUtils.toHuman(asset.name)}`) - await node.wallet.walletDb.deleteAsset(account, asset.id, tx) + await context.wallet.walletDb.deleteAsset(account, asset.id, tx) assetCount++ } diff --git a/ironfish/src/migrations/data/021-add-version-to-accounts.ts b/ironfish/src/migrations/data/021-add-version-to-accounts.ts index 2e6e342656..ff0e65dcfa 100644 --- a/ironfish/src/migrations/data/021-add-version-to-accounts.ts +++ b/ironfish/src/migrations/data/021-add-version-to-accounts.ts @@ -3,8 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetNewStores } from './021-add-version-to-accounts/schemaNew' import { GetOldStores } from './021-add-version-to-accounts/schemaOld' @@ -12,12 +11,12 @@ export class Migration021 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return node.wallet.walletDb.db + prepare(context: MigrationContext): IDatabase { + return context.wallet.walletDb.db } async forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, @@ -42,7 +41,7 @@ export class Migration021 extends Migration { } async backward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, diff --git a/ironfish/src/migrations/data/021-add-version-to-accounts/schemaOld.ts b/ironfish/src/migrations/data/021-add-version-to-accounts/schemaOld.ts index 0d8045ce24..d367875e8a 100644 --- a/ironfish/src/migrations/data/021-add-version-to-accounts/schemaOld.ts +++ b/ironfish/src/migrations/data/021-add-version-to-accounts/schemaOld.ts @@ -10,8 +10,8 @@ import { IDatabaseTransaction, StringEncoding, } from '../../../storage' -import { IronfishNode } from '../../../utils' import { Account } from '../../../wallet' +import { MigrationContext } from '../../migration' const KEY_LENGTH = 32 @@ -85,7 +85,7 @@ export function GetOldStores(db: IDatabase): { } export async function GetOldAccounts( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx?: IDatabaseTransaction, ): Promise { @@ -101,7 +101,7 @@ export async function GetOldAccounts( version: 1, viewKey: key.viewKey, createdAt: null, - walletDb: node.wallet.walletDb, + walletDb: context.wallet.walletDb, }), ) } diff --git a/ironfish/src/migrations/data/022-add-view-key-account.ts b/ironfish/src/migrations/data/022-add-view-key-account.ts index 3f9a4a55be..7f08651f58 100644 --- a/ironfish/src/migrations/data/022-add-view-key-account.ts +++ b/ironfish/src/migrations/data/022-add-view-key-account.ts @@ -4,8 +4,7 @@ import { generateKeyFromPrivateKey } from '@ironfish/rust-nodejs' import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetNewStores } from './022-add-view-key-account/schemaNew' import { GetOldStores } from './022-add-view-key-account/schemaOld' @@ -13,12 +12,12 @@ export class Migration022 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return node.wallet.walletDb.db + prepare(context: MigrationContext): IDatabase { + return context.wallet.walletDb.db } async forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, @@ -45,7 +44,7 @@ export class Migration022 extends Migration { } async backward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, diff --git a/ironfish/src/migrations/data/023-wallet-optional-spending-key.ts b/ironfish/src/migrations/data/023-wallet-optional-spending-key.ts index c3e2b75f5b..1910d304b4 100644 --- a/ironfish/src/migrations/data/023-wallet-optional-spending-key.ts +++ b/ironfish/src/migrations/data/023-wallet-optional-spending-key.ts @@ -3,20 +3,19 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetStores } from './023-wallet-optional-spending-key/stores' export class Migration023 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return node.wallet.walletDb.db + prepare(context: MigrationContext): IDatabase { + return context.wallet.walletDb.db } async forward( - _node: IronfishNode, + _context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, @@ -30,7 +29,7 @@ export class Migration023 extends Migration { } backward( - _node: IronfishNode, + _context: MigrationContext, _db: IDatabase, _tx: IDatabaseTransaction | undefined, _logger: Logger, diff --git a/ironfish/src/migrations/data/024-unspent-notes.ts b/ironfish/src/migrations/data/024-unspent-notes.ts index 6b0919655b..d68bd4b1cc 100644 --- a/ironfish/src/migrations/data/024-unspent-notes.ts +++ b/ironfish/src/migrations/data/024-unspent-notes.ts @@ -5,21 +5,20 @@ import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' import { createDB } from '../../storage/utils' -import { IronfishNode } from '../../utils' import { Account } from '../../wallet' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetStores } from './024-unspent-notes/stores' export class Migration024 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return createDB({ location: node.config.walletDatabasePath }) + prepare(context: MigrationContext): IDatabase { + return createDB({ location: context.config.walletDatabasePath }) } async forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, @@ -33,7 +32,7 @@ export class Migration024 extends Migration { new Account({ ...accountValue, createdAt: null, - walletDb: node.wallet.walletDb, + walletDb: context.wallet.walletDb, }), ) } @@ -66,7 +65,7 @@ export class Migration024 extends Migration { } } - async backward(node: IronfishNode, db: IDatabase): Promise { + async backward(context: MigrationContext, db: IDatabase): Promise { const stores = GetStores(db) await stores.new.unspentNoteHashes.clear() diff --git a/ironfish/src/migrations/data/025-backfill-wallet-nullifier-to-transaction-hash.ts b/ironfish/src/migrations/data/025-backfill-wallet-nullifier-to-transaction-hash.ts index a9499f8a76..e731bebe6c 100644 --- a/ironfish/src/migrations/data/025-backfill-wallet-nullifier-to-transaction-hash.ts +++ b/ironfish/src/migrations/data/025-backfill-wallet-nullifier-to-transaction-hash.ts @@ -4,21 +4,20 @@ import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' import { createDB } from '../../storage/utils' -import { IronfishNode } from '../../utils' import { Account } from '../../wallet' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetStores } from './025-backfill-wallet-nullifier-to-transaction-hash/stores' export class Migration025 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return createDB({ location: node.config.walletDatabasePath }) + prepare(context: MigrationContext): IDatabase { + return createDB({ location: context.config.walletDatabasePath }) } async forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, _tx: IDatabaseTransaction | undefined, logger: Logger, @@ -31,7 +30,7 @@ export class Migration025 extends Migration { new Account({ ...account, createdAt: null, - walletDb: node.wallet.walletDb, + walletDb: context.wallet.walletDb, }), ) } @@ -91,7 +90,7 @@ export class Migration025 extends Migration { } async backward( - _node: IronfishNode, + _context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, diff --git a/ironfish/src/migrations/data/026-timestamp-to-transactions.ts b/ironfish/src/migrations/data/026-timestamp-to-transactions.ts index 8b49874b9a..535b165fec 100644 --- a/ironfish/src/migrations/data/026-timestamp-to-transactions.ts +++ b/ironfish/src/migrations/data/026-timestamp-to-transactions.ts @@ -4,21 +4,20 @@ import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' import { createDB } from '../../storage/utils' -import { IronfishNode } from '../../utils' import { Account } from '../../wallet' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetStores } from './026-timestamp-to-transactions/stores' export class Migration026 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return createDB({ location: node.config.walletDatabasePath }) + prepare(context: MigrationContext): IDatabase { + return createDB({ location: context.config.walletDatabasePath }) } async forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, _tx: IDatabaseTransaction | undefined, logger: Logger, @@ -31,7 +30,7 @@ export class Migration026 extends Migration { new Account({ ...account, createdAt: null, - walletDb: node.wallet.walletDb, + walletDb: context.wallet.walletDb, }), ) } @@ -68,7 +67,7 @@ export class Migration026 extends Migration { logger.info('') } - async backward(node: IronfishNode, db: IDatabase): Promise { + async backward(context: MigrationContext, db: IDatabase): Promise { const accounts = [] const stores = GetStores(db) @@ -77,7 +76,7 @@ export class Migration026 extends Migration { new Account({ ...account, createdAt: null, - walletDb: node.wallet.walletDb, + walletDb: context.wallet.walletDb, }), ) } diff --git a/ironfish/src/migrations/data/027-account-created-at-block.ts b/ironfish/src/migrations/data/027-account-created-at-block.ts index ba900abaab..33d4fc88d5 100644 --- a/ironfish/src/migrations/data/027-account-created-at-block.ts +++ b/ironfish/src/migrations/data/027-account-created-at-block.ts @@ -4,20 +4,19 @@ import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' import { createDB } from '../../storage/utils' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetStores } from './027-account-created-at-block/stores' export class Migration027 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return createDB({ location: node.config.walletDatabasePath }) + prepare(context: MigrationContext): IDatabase { + return createDB({ location: context.config.walletDatabasePath }) } async forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, @@ -34,7 +33,7 @@ export class Migration027 extends Migration { } async backward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, diff --git a/ironfish/src/migrations/data/028-backfill-assets-owner.ts b/ironfish/src/migrations/data/028-backfill-assets-owner.ts index 7e24bb9f53..7e06a46817 100644 --- a/ironfish/src/migrations/data/028-backfill-assets-owner.ts +++ b/ironfish/src/migrations/data/028-backfill-assets-owner.ts @@ -4,20 +4,19 @@ import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' import { createDB } from '../../storage/utils' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetStores } from './028-backfill-assets-owner/stores' export class Migration028 extends Migration { path = __filename database = Database.BLOCKCHAIN - prepare(node: IronfishNode): IDatabase { - return createDB({ location: node.config.chainDatabasePath }) + prepare(context: MigrationContext): IDatabase { + return createDB({ location: context.config.chainDatabasePath }) } async forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, @@ -40,7 +39,7 @@ export class Migration028 extends Migration { } async backward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, diff --git a/ironfish/src/migrations/data/029-backfill-assets-owner-wallet.ts b/ironfish/src/migrations/data/029-backfill-assets-owner-wallet.ts index edcdc118a7..925c223091 100644 --- a/ironfish/src/migrations/data/029-backfill-assets-owner-wallet.ts +++ b/ironfish/src/migrations/data/029-backfill-assets-owner-wallet.ts @@ -4,20 +4,19 @@ import { Logger } from '../../logger' import { IDatabase, IDatabaseTransaction } from '../../storage' import { createDB } from '../../storage/utils' -import { IronfishNode } from '../../utils' -import { Database, Migration } from '../migration' +import { Database, Migration, MigrationContext } from '../migration' import { GetStores } from './028-backfill-assets-owner/stores' export class Migration029 extends Migration { path = __filename database = Database.WALLET - prepare(node: IronfishNode): IDatabase { - return createDB({ location: node.config.walletDatabasePath }) + prepare(context: MigrationContext): IDatabase { + return createDB({ location: context.config.walletDatabasePath }) } async forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, @@ -40,7 +39,7 @@ export class Migration029 extends Migration { } async backward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, diff --git a/ironfish/src/migrations/migration.ts b/ironfish/src/migrations/migration.ts index ba4c7eed07..7c9bcd17c1 100644 --- a/ironfish/src/migrations/migration.ts +++ b/ironfish/src/migrations/migration.ts @@ -2,16 +2,23 @@ * 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 { FileSystem } from '../fileSystems' import { Logger } from '../logger' import { IDatabase, IDatabaseTransaction } from '../storage' -import { IronfishNode } from '../utils' +import { Wallet } from '../wallet' export enum Database { WALLET = 'wallet', BLOCKCHAIN = 'blockchain', } +export type MigrationContext = { + config: Config + files: FileSystem + wallet: Wallet +} + export abstract class Migration { id = 0 name = '' @@ -30,10 +37,10 @@ export abstract class Migration { return this } - abstract prepare(node: IronfishNode): Promise | IDatabase + abstract prepare(context: MigrationContext): Promise | IDatabase abstract forward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, @@ -41,7 +48,7 @@ export abstract class Migration { ): Promise abstract backward( - node: IronfishNode, + context: MigrationContext, db: IDatabase, tx: IDatabaseTransaction | undefined, logger: Logger, diff --git a/ironfish/src/migrations/migrator.ts b/ironfish/src/migrations/migrator.ts index 77997d67f0..527f8d15d7 100644 --- a/ironfish/src/migrations/migrator.ts +++ b/ironfish/src/migrations/migrator.ts @@ -7,23 +7,23 @@ import { LogLevel } from 'consola' import { Assert } from '../assert' import { Logger } from '../logger' import { IDatabaseTransaction } from '../storage/database/transaction' -import { IronfishNode, StrEnumUtils } from '../utils' +import { StrEnumUtils } from '../utils' import { ErrorUtils } from '../utils/error' import { MIGRATIONS } from './data' -import { Database, Migration } from './migration' +import { Database, Migration, MigrationContext } from './migration' export class Migrator { - readonly node: IronfishNode + readonly context: MigrationContext readonly logger: Logger readonly migrations: Migration[] - constructor(options: { node: IronfishNode; logger: Logger; databases?: Database[] }) { - this.node = options.node + constructor(options: { context: MigrationContext; logger: Logger; databases?: Database[] }) { + this.context = options.context this.logger = options.logger.withTag('migrator') const whitelistedDBs = options?.databases ?? StrEnumUtils.getValues(Database) this.migrations = MIGRATIONS.map((m) => { - return new m().init(options.node.files) + return new m().init(options.context.files) }) .filter((migration) => whitelistedDBs.includes(migration.database)) .sort((a, b) => a.id - b.id) @@ -45,7 +45,7 @@ export class Migrator { * Returns true if the migration database is at version 0 */ async isEmpty(migration: Migration): Promise { - const db = await migration.prepare(this.node) + const db = await migration.prepare(this.context) try { await db.open() @@ -57,7 +57,7 @@ export class Migrator { } async isApplied(migration: Migration): Promise { - const db = await migration.prepare(this.node) + const db = await migration.prepare(this.context) try { await db.open() @@ -78,7 +78,7 @@ export class Migrator { if (applied) { this.logger.info(`Reverting ${migration.name}`) - const db = await migration.prepare(this.node) + const db = await migration.prepare(this.context) const childLogger = this.logger.withTag(migration.name) let tx: IDatabaseTransaction | null = null @@ -87,7 +87,7 @@ export class Migrator { await db.open() tx = db.transaction() - await migration.backward(this.node, db, tx, childLogger, dryRun) + await migration.backward(this.context, db, tx, childLogger, dryRun) await db.putVersion(migration.id - 1, tx) if (dryRun) { @@ -141,7 +141,7 @@ export class Migrator { for (const migration of unapplied) { logger.info(`Running ${migration.name}...`) - const db = await migration.prepare(this.node) + const db = await migration.prepare(this.context) const childLogger = logger.withTag(migration.name) let tx: IDatabaseTransaction | undefined = undefined @@ -153,7 +153,7 @@ export class Migrator { tx = db.transaction() } - await migration.forward(this.node, db, tx, childLogger, dryRun) + await migration.forward(this.context, db, tx, childLogger, dryRun) await db.putVersion(migration.id, tx) if (dryRun) { diff --git a/ironfish/src/network/peerNetwork.test.ts b/ironfish/src/network/peerNetwork.test.ts index 0cd45c989f..444db3f4c6 100644 --- a/ironfish/src/network/peerNetwork.test.ts +++ b/ironfish/src/network/peerNetwork.test.ts @@ -97,10 +97,13 @@ describe('PeerNetwork', () => { describe('when peers connect', () => { it('changes isReady', async () => { + const dc = await import('node-datachannel') + const peerNetwork = new PeerNetwork({ identity: mockPrivateIdentity('local'), agent: 'sdk/1/cli', webSocket: ws, + nodeDataChannel: dc, node: mockNode(), chain: mockChain(), minPeers: 1, @@ -135,11 +138,13 @@ describe('PeerNetwork', () => { describe('when at max peers', () => { it('rejects websocket connections', async () => { const wsActual = jest.requireActual('ws') + const dc = await import('node-datachannel') const peerNetwork = new PeerNetwork({ identity: mockPrivateIdentity('local'), agent: 'sdk/1/cli', webSocket: wsActual, + nodeDataChannel: dc, node: mockNode(), chain: mockChain(), listen: true, diff --git a/ironfish/src/network/peerNetwork.ts b/ironfish/src/network/peerNetwork.ts index 36b8e9f304..1e58a8c2bf 100644 --- a/ironfish/src/network/peerNetwork.ts +++ b/ironfish/src/network/peerNetwork.ts @@ -62,7 +62,7 @@ import { BAN_SCORE, KnownBlockHashesValue, Peer } from './peers/peer' import { PeerConnectionManager } from './peers/peerConnectionManager' import { PeerManager } from './peers/peerManager' import { TransactionFetcher } from './transactionFetcher' -import { IsomorphicWebSocketConstructor } from './types' +import { IsomorphicWebSocketConstructor, NodeDataChannelType } from './types' import { getBlockSize } from './utils/serializers' import { parseUrl, WebSocketAddress } from './utils/url' import { @@ -159,6 +159,7 @@ export class PeerNetwork { identity: PrivateIdentity agent?: string webSocket: IsomorphicWebSocketConstructor + nodeDataChannel: NodeDataChannelType listen?: boolean port?: number bootstrapNodes?: string[] @@ -194,6 +195,7 @@ export class PeerNetwork { VERSION_PROTOCOL, options.chain, options.webSocket, + options.nodeDataChannel, options.networkId, this.enableSyncing, ) @@ -268,7 +270,17 @@ export class PeerNetwork { } this.started = true - // Start the WebSocket server if possible + this.startWebSocketServer() + this.peerManager.start() + this.peerConnectionManager.start() + + this.updateIsReady() + + this.connectToBootstrapNodes() + this.connectToPriorWebsocketConnections() + } + + private startWebSocketServer() { if (this.listen && 'Server' in this.localPeer.webSocket && this.localPeer.port !== null) { this.webSocketServer = new WebSocketServer( this.localPeer.webSocket.Server, @@ -342,15 +354,9 @@ export class PeerNetwork { } }) } + } - // Start up the PeerManager - this.peerManager.start() - - // Start up the PeerConnectionManager - this.peerConnectionManager.start() - - this.updateIsReady() - + private connectToBootstrapNodes() { for (const node of this.bootstrapNodes) { const url = parseUrl(node) @@ -370,9 +376,9 @@ export class PeerNetwork { whitelist: true, }) } + } - // Connect to prior websocket outbound connections that were saved to disk - // This should be replaced with populating from the peer candidates list [IFL-1786] + private connectToPriorWebsocketConnections() { for (const peerAddress of this.peerManager.peerStoreManager.priorConnectedPeerAddresses) { this.peerManager.connectToWebSocketAddress({ host: peerAddress.address, diff --git a/ironfish/src/network/peers/connections/webRtcConnection.test.ts b/ironfish/src/network/peers/connections/webRtcConnection.test.ts index 716ac47a32..7ca4964b8b 100644 --- a/ironfish/src/network/peers/connections/webRtcConnection.test.ts +++ b/ironfish/src/network/peers/connections/webRtcConnection.test.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 { Assert } from '../../../assert' import { createRootLogger } from '../../../logger' import { IdentifyMessage } from '../../messages/identify' import { defaultFeatures } from '../peerFeatures' @@ -10,8 +9,10 @@ import { WebRtcConnection } from './webRtcConnection' describe('WebRtcConnection', () => { describe('send', () => { describe('with no datachannel', () => { - it('returns false', () => { - const connection = new WebRtcConnection(false, createRootLogger()) + it('returns false', async () => { + const nodeDataChannel = await import('node-datachannel') + + const connection = new WebRtcConnection(nodeDataChannel, false, createRootLogger()) const message = new IdentifyMessage({ agent: '', head: Buffer.alloc(32, 0), @@ -30,14 +31,12 @@ describe('WebRtcConnection', () => { }) describe('with a valid message', () => { - it('serializes and sends the message on the datachannel', () => { - const connection = new WebRtcConnection(true, createRootLogger()) - const datachannel = connection['datachannel'] - Assert.isNotNull(datachannel) - jest.spyOn(datachannel, 'isOpen').mockImplementation(() => true) - const sendMessageBinary = jest - .spyOn(datachannel, 'sendMessageBinary') - .mockImplementationOnce(jest.fn()) + it('serializes and sends the message on the datachannel', async () => { + const nodeDataChannel = await import('node-datachannel') + + const connection = new WebRtcConnection(nodeDataChannel, true, createRootLogger()) + const sendSpy = jest.spyOn(connection, '_send') + const message = new IdentifyMessage({ agent: '', head: Buffer.alloc(32, 0), @@ -51,9 +50,10 @@ describe('WebRtcConnection', () => { features: defaultFeatures(), }) - expect(connection.send(message)).toBe(true) - expect(sendMessageBinary).toHaveBeenCalledWith(message.serialize()) + connection.send(message) connection.close() + + expect(sendSpy).toHaveBeenCalledWith(message.serialize()) }) }) }) diff --git a/ironfish/src/network/peers/connections/webRtcConnection.ts b/ironfish/src/network/peers/connections/webRtcConnection.ts index 29456f4376..69db4334cf 100644 --- a/ironfish/src/network/peers/connections/webRtcConnection.ts +++ b/ironfish/src/network/peers/connections/webRtcConnection.ts @@ -3,20 +3,22 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import type { Logger } from '../../../logger' +// @ts-expect-error Allow type-only import https://github.com/microsoft/TypeScript/issues/49721 +import type { DataChannel, DescriptionType, PeerConnection } from 'node-datachannel' import colors from 'colors/safe' -import nodeDataChannel from 'node-datachannel' import { Assert } from '../../../assert' import { Event } from '../../../event' import { MetricsMonitor } from '../../../metrics' import { ErrorUtils } from '../../../utils' import { parseNetworkMessage } from '../../messageRegistry' +import { NodeDataChannelType } from '../../types' import { MAX_MESSAGE_SIZE } from '../../version' import { Connection, ConnectionDirection, ConnectionType } from './connection' import { NetworkError } from './errors' export type SignalData = | { - type: nodeDataChannel.DescriptionType + type: DescriptionType sdp: string } | CandidateSignal @@ -35,8 +37,8 @@ type CandidateSignal = { * LooseMessages instead of strings/data. */ export class WebRtcConnection extends Connection { - private readonly peer: nodeDataChannel.PeerConnection - private datachannel: nodeDataChannel.DataChannel | null = null + private readonly peer: PeerConnection + private datachannel: DataChannel | null = null /** * True if we've received an SDP message from the peer. @@ -55,6 +57,7 @@ export class WebRtcConnection extends Connection { onSignal = new Event<[SignalData]>() constructor( + nodeDataChannel: NodeDataChannelType, initiator: boolean, logger: Logger, metrics?: MetricsMonitor, @@ -102,7 +105,7 @@ export class WebRtcConnection extends Connection { }) }) - this.peer.onDataChannel((dc: nodeDataChannel.DataChannel) => { + this.peer.onDataChannel((dc: DataChannel) => { Assert.isNull(this.datachannel) this.initializeDataChannel(dc) }) @@ -112,7 +115,7 @@ export class WebRtcConnection extends Connection { } } - initializeDataChannel(dc: nodeDataChannel.DataChannel): void { + initializeDataChannel(dc: DataChannel): void { this.datachannel = dc this.datachannel.onOpen(() => { @@ -199,9 +202,7 @@ export class WebRtcConnection extends Connection { return false } - this.datachannel.sendMessageBinary(data) - - return true + return this.datachannel.sendMessageBinary(data) } /** diff --git a/ironfish/src/network/peers/connections/webSocketConnection.ts b/ironfish/src/network/peers/connections/webSocketConnection.ts index c3f7f3901b..b0806c892b 100644 --- a/ironfish/src/network/peers/connections/webSocketConnection.ts +++ b/ironfish/src/network/peers/connections/webSocketConnection.ts @@ -7,6 +7,7 @@ import colors from 'colors/safe' import { MetricsMonitor } from '../../../metrics' import { parseNetworkMessage } from '../../messageRegistry' import { IsomorphicWebSocket, IsomorphicWebSocketErrorEvent } from '../../types' +import { WebSocketAddress } from '../../utils' import { Connection, ConnectionDirection, ConnectionType } from './connection' import { NetworkError } from './errors' @@ -17,24 +18,28 @@ import { NetworkError } from './errors' export class WebSocketConnection extends Connection { private readonly socket: IsomorphicWebSocket - // The hostname of the address that was used to establish the WebSocket connection, if any - readonly hostname?: string + private _address: WebSocketAddress | null + get address(): WebSocketAddress | null { + return this._address + } - // The port of the address that was used to establish the WebSocket connection, if any - port?: number + set port(port: number) { + if (this._address) { + this._address.port = port + } + } constructor( socket: IsomorphicWebSocket, direction: ConnectionDirection, logger: Logger, metrics?: MetricsMonitor, - options: { hostname?: string; port?: number } = {}, + address: WebSocketAddress | null = null, ) { super(ConnectionType.WebSocket, direction, logger.withTag('WebSocketConnection'), metrics) this.socket = socket - this.hostname = options.hostname - this.port = options.port + this._address = address if (this.socket.readyState === this.socket.OPEN) { this.setState({ type: 'WAITING_FOR_IDENTITY' }) diff --git a/ironfish/src/network/peers/localPeer.ts b/ironfish/src/network/peers/localPeer.ts index a080c9e4cc..7106f8367c 100644 --- a/ironfish/src/network/peers/localPeer.ts +++ b/ironfish/src/network/peers/localPeer.ts @@ -6,7 +6,7 @@ import { Assert } from '../../assert' import { Blockchain } from '../../blockchain' import { Identity, PrivateIdentity, privateIdentityToIdentity } from '../identity' import { IdentifyMessage } from '../messages/identify' -import { IsomorphicWebSocketConstructor } from '../types' +import { IsomorphicWebSocketConstructor, NodeDataChannelType } from '../types' /** * Wraps configuration needed for establishing connections with other peers @@ -24,6 +24,8 @@ export class LocalPeer { readonly version: number // constructor for either a Node WebSocket or a browser WebSocket readonly webSocket: IsomorphicWebSocketConstructor + // asynchronously imported WebRTC datachannel library + readonly nodeDataChannel: NodeDataChannelType // the unique ID number of the network readonly networkId: number // true if the peer supports syncing and gossip messages @@ -40,6 +42,7 @@ export class LocalPeer { version: number, chain: Blockchain, webSocket: IsomorphicWebSocketConstructor, + nodeDataChannel: NodeDataChannelType, networkId: number, enableSyncing: boolean, ) { @@ -52,6 +55,7 @@ export class LocalPeer { this.enableSyncing = enableSyncing this.webSocket = webSocket + this.nodeDataChannel = nodeDataChannel this.port = null this.name = null } diff --git a/ironfish/src/network/peers/peer.test.ts b/ironfish/src/network/peers/peer.test.ts index 7a061f53d9..d96c08a84a 100644 --- a/ironfish/src/network/peers/peer.test.ts +++ b/ironfish/src/network/peers/peer.test.ts @@ -51,10 +51,12 @@ describe('setWebSocketConnection', () => { }) describe('setWebRtcConnection', () => { - it('Changes to CONNECTING when in DISCONNECTED', () => { + it('Changes to CONNECTING when in DISCONNECTED', async () => { + const nodeDataChannel = await import('node-datachannel') + const identity = mockIdentity('peer') const peer = new Peer(identity) - const connection = new WebRtcConnection(false, createRootLogger()) + const connection = new WebRtcConnection(nodeDataChannel, false, createRootLogger()) peer.setWebRtcConnection(connection) expect(peer.state).toEqual({ @@ -65,8 +67,10 @@ describe('setWebRtcConnection', () => { }) }) -it('Times out WebRTC handshake', () => { - const connection = new WebRtcConnection(false, createRootLogger()) +it('Times out WebRTC handshake', async () => { + const nodeDataChannel = await import('node-datachannel') + + const connection = new WebRtcConnection(nodeDataChannel, false, createRootLogger()) expect(connection.state.type).toEqual('CONNECTING') const peer = new Peer(null) @@ -109,32 +113,52 @@ it('Times out WebRTC handshake', () => { expect(peer.state.type).toEqual('CONNECTED') }) -describe('Handles WebRTC message send failure', () => { - it('Handles failure if WebRTC is only connection', () => { - const connection = new WebRtcConnection(true, createRootLogger()) +describe('Handles message send failure', () => { + it('Disconnects peer on error in _send', async () => { + const nodeDataChannel = await import('node-datachannel') + + const connection = new WebRtcConnection(nodeDataChannel, true, createRootLogger()) expect(connection.state.type).toEqual('CONNECTING') const peer = new Peer(null) - // Time out requesting signaling connection.setState({ type: 'CONNECTED', identity: mockIdentity('peer') }) peer.setWebRtcConnection(connection) - if (!connection['datachannel']) { - throw new Error('Should have datachannel') - } - jest.spyOn(connection['datachannel'], 'sendMessageBinary').mockImplementation(() => { - throw new Error('Error') + const sendSpy = jest.spyOn(connection, '_send').mockImplementation(() => { + throw new Error() }) - jest.spyOn(connection['datachannel'], 'isOpen').mockImplementation(() => true) expect(peer.state.type).toEqual('CONNECTED') const result = peer.send(new PeerListRequestMessage()) + expect(sendSpy).toHaveBeenCalledTimes(1) expect(result).toBeNull() expect(peer.state.type).toEqual('DISCONNECTED') }) - it('Falls back to WebSockets if available and WebRTC send fails', () => { - const wrtcConnection = new WebRtcConnection(true, createRootLogger()) + it('Leaves peer connected if _send returns false', () => { + const connection = new WebSocketConnection( + new ws(''), + ConnectionDirection.Outbound, + createRootLogger(), + ) + expect(connection.state.type).toEqual('CONNECTING') + + const peer = new Peer(null) + + connection.setState({ type: 'CONNECTED', identity: mockIdentity('peer') }) + peer.setWebSocketConnection(connection) + jest.spyOn(connection, '_send').mockReturnValue(false) + + expect(peer.state.type).toEqual('CONNECTED') + const result = peer.send(new PeerListRequestMessage()) + expect(result).toBeNull() + expect(peer.state.type).toEqual('CONNECTED') + }) + + it('Falls back to WebSockets if available and WebRTC send fails', async () => { + const nodeDataChannel = await import('node-datachannel') + + const wrtcConnection = new WebRtcConnection(nodeDataChannel, true, createRootLogger()) const wsConnection = new WebSocketConnection( new ws(''), ConnectionDirection.Outbound, @@ -149,11 +173,7 @@ describe('Handles WebRTC message send failure', () => { peer.setWebRtcConnection(wrtcConnection) peer.setWebSocketConnection(wsConnection) - if (wrtcConnection['datachannel']) { - jest.spyOn(wrtcConnection['datachannel'], 'sendMessage').mockImplementation(() => { - throw new Error('Error') - }) - } + jest.spyOn(wrtcConnection, '_send').mockReturnValue(false) const wsSendSpy = jest.spyOn(wsConnection, 'send') const message = new PeerListRequestMessage() @@ -235,7 +255,9 @@ it('Transitions to CONNECTED when adding a connection with state CONNECTED', () }) }) -it('Stays in CONNECTED when adding an additional connection', () => { +it('Stays in CONNECTED when adding an additional connection', async () => { + const nodeDataChannel = await import('node-datachannel') + const identity = mockIdentity('peer') const peer = new Peer(null) const connection = new WebSocketConnection( @@ -253,7 +275,7 @@ it('Stays in CONNECTED when adding an additional connection', () => { connection.setState({ type: 'CONNECTED', identity }) // Add in an additional connection - const wrtcConnection = new WebRtcConnection(true, createRootLogger()) + const wrtcConnection = new WebRtcConnection(nodeDataChannel, true, createRootLogger()) peer.setWebRtcConnection(wrtcConnection) expect(wrtcConnection.state.type).not.toBe('CONNECTED') @@ -265,7 +287,9 @@ it('Stays in CONNECTED when adding an additional connection', () => { }) describe('Stays in CONNECTED when one connection disconnects', () => { - it('WebSocket disconnects', () => { + it('WebSocket disconnects', async () => { + const nodeDataChannel = await import('node-datachannel') + const identity = mockIdentity('peer') const peer = new Peer(null) @@ -279,7 +303,7 @@ describe('Stays in CONNECTED when one connection disconnects', () => { connection.setState({ type: 'CONNECTED', identity }) // Add a CONNECTED WebRTC connection - const wrtcConnection = new WebRtcConnection(true, createRootLogger()) + const wrtcConnection = new WebRtcConnection(nodeDataChannel, true, createRootLogger()) peer.setWebRtcConnection(wrtcConnection) wrtcConnection.setState({ type: 'CONNECTED', identity }) @@ -294,7 +318,9 @@ describe('Stays in CONNECTED when one connection disconnects', () => { }) }) - it('WebRTC disconnects', () => { + it('WebRTC disconnects', async () => { + const nodeDataChannel = await import('node-datachannel') + const identity = mockIdentity('peer') const peer = new Peer(null) @@ -308,7 +334,7 @@ describe('Stays in CONNECTED when one connection disconnects', () => { connection.setState({ type: 'CONNECTED', identity }) // Add a CONNECTED WebRTC connection - const wrtcConnection = new WebRtcConnection(true, createRootLogger()) + const wrtcConnection = new WebRtcConnection(nodeDataChannel, true, createRootLogger()) peer.setWebRtcConnection(wrtcConnection) wrtcConnection.setState({ type: 'CONNECTED', identity }) diff --git a/ironfish/src/network/peers/peer.ts b/ironfish/src/network/peers/peer.ts index a9a32000e7..30d06784b5 100644 --- a/ironfish/src/network/peers/peer.ts +++ b/ironfish/src/network/peers/peer.ts @@ -536,9 +536,9 @@ export class Peer { if ( connection.state.type === 'CONNECTED' && connection instanceof WebSocketConnection && - connection.hostname + connection.address ) { - this.wsAddress = { host: connection.hostname, port: connection.port || null } + this.wsAddress = connection.address } // onMessage @@ -580,8 +580,8 @@ export class Peer { if (connection.state.type === 'CONNECTED') { // If connection goes to connected, transition the peer to connected - if (connection instanceof WebSocketConnection && connection.hostname) { - this.wsAddress = { host: connection.hostname, port: connection.port || null } + if (connection instanceof WebSocketConnection && connection.address) { + this.wsAddress = connection.address } this.setState(this.state.connections.webSocket, this.state.connections.webRtc) } diff --git a/ironfish/src/network/peers/peerAddress.ts b/ironfish/src/network/peers/peerAddress.ts deleted file mode 100644 index a46df96db8..0000000000 --- a/ironfish/src/network/peers/peerAddress.ts +++ /dev/null @@ -1,12 +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 { Identity } from '../identity' - -export type PeerAddress = { - address: string - port: number - identity: Identity - name: string | null - lastAddedTimestamp: number -} diff --git a/ironfish/src/network/peers/peerCandidates.ts b/ironfish/src/network/peers/peerCandidates.ts index 8a319526ee..f8d834f633 100644 --- a/ironfish/src/network/peers/peerCandidates.ts +++ b/ironfish/src/network/peers/peerCandidates.ts @@ -42,7 +42,7 @@ export class PeerCandidates { websocketRetry: new ConnectionRetry(peer.isWhitelisted), localRequestedDisconnectUntil: null, peerRequestedDisconnectUntil: null, - name: null, + name: peer.name, } if (peer.state.identity !== null) { @@ -73,6 +73,7 @@ export class PeerCandidates { } else { const tempPeer = new Peer(peer.identity) tempPeer.wsAddress = peer.wsAddress + tempPeer.name = peer.name ?? null this.addFromPeer(tempPeer, new Set([sendingPeerIdentity])) } } diff --git a/ironfish/src/network/peers/peerConnectionManager.test.ts b/ironfish/src/network/peers/peerConnectionManager.test.ts index 4ad3ced07f..4dffcc21e9 100644 --- a/ironfish/src/network/peers/peerConnectionManager.test.ts +++ b/ironfish/src/network/peers/peerConnectionManager.test.ts @@ -392,6 +392,43 @@ describe('maintainMaxPeerCount', () => { } }) + it('should not disconnect white-listed peers', () => { + const maxPeers = 5 + + const pm = new PeerManager(mockLocalPeer(), mockPeerStore()) + const pcm = new PeerConnectionManager(pm, createRootLogger(), { + maxPeers, + keepOpenPeerSlot: true, + }) + + // Add 3 white-listed peers + const whitelistPeer1 = getConnectedPeer(pm) + whitelistPeer1.peer.isWhitelisted = true + + const whitelistPeer2 = getConnectedPeer(pm) + whitelistPeer2.peer.isWhitelisted = true + + const whitelistPeer3 = getConnectedPeer(pm) + whitelistPeer3.peer.isWhitelisted = true + + // Add non-white-listed peer + getConnectedPeer(pm) + + // Execute this test many times to ensure the logic is sound despite + // randomness being involved + for (let i = 0; i < 100; i++) { + // Add 5th peer who is not eligible to be disconnected this loop, but will + // be the only eligible peer next loop + getConnectedPeer(pm) + + pcm['maintainMaxPeerCount']() + } + + expect(whitelistPeer1.connection.state.type).toEqual('CONNECTED') + expect(whitelistPeer2.connection.state.type).toEqual('CONNECTED') + expect(whitelistPeer3.connection.state.type).toEqual('CONNECTED') + }) + describe('when keepOpenPeerSlot is false', () => { it('should only disconnect a peer if it is above maxPeers', () => { const maxPeers = 5 diff --git a/ironfish/src/network/peers/peerConnectionManager.ts b/ironfish/src/network/peers/peerConnectionManager.ts index 88fa9c9ccc..577edf6d10 100644 --- a/ironfish/src/network/peers/peerConnectionManager.ts +++ b/ironfish/src/network/peers/peerConnectionManager.ts @@ -143,10 +143,11 @@ export class PeerConnectionManager { return } - // Choose a random peer, but exclude the newest connections as they are - // least likely to have other peers + // Choose a random peer with some exceptions: + // - Exclude the most recent peer connections as they are more likely to have fewer peers + // - Exclude white-listed nodes const sampleEnd = Math.floor(connectedPeers.length * 0.8) - const peersSlice = connectedPeers.slice(0, sampleEnd) + const peersSlice = connectedPeers.slice(0, sampleEnd).filter((p) => !p.isWhitelisted) const peer = ArrayUtils.sample(peersSlice) if (!peer) { return diff --git a/ironfish/src/network/peers/peerManager.test.ts b/ironfish/src/network/peers/peerManager.test.ts index c49077c649..10113ae2ea 100644 --- a/ironfish/src/network/peers/peerManager.test.ts +++ b/ironfish/src/network/peers/peerManager.test.ts @@ -617,7 +617,7 @@ describe('PeerManager', () => { const peerIdentity = webRtcCanInitiateIdentity() const pm = new PeerManager(mockLocalPeer(), mockPeerStore()) - const { peer } = getSignalingWebRtcPeer(pm, brokerIdentity, peerIdentity) + const { peer, connection } = getSignalingWebRtcPeer(pm, brokerIdentity, peerIdentity) if (peer.state.type === 'DISCONNECTED') { throw new Error('Peer should not be DISCONNECTED') @@ -625,19 +625,12 @@ describe('PeerManager', () => { if (!peer.state.connections.webRtc) { 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({ + + jest.spyOn(connection, '_send').mockReturnValue(true) + peer.state.connections.webRtc.setState({ type: 'CONNECTED', identity: peerIdentity, }) - expect(closeSpy).toHaveBeenCalledTimes(1) expect(pm.peers.length).toBe(2) expect(pm.identifiedPeers.size).toBe(2) @@ -668,7 +661,7 @@ describe('PeerManager', () => { identity: peerIdentity, connections: { webSocket: unidentifiedConnection, - webRtc: webRtcConnection, + webRtc: peer.state.connections.webRtc, }, }) expect(unidentifiedPeer.state).toEqual({ @@ -1166,7 +1159,7 @@ describe('PeerManager', () => { peer.onMessage.emit(message, connection) expect(initWebRtcConnectionMock).toHaveBeenCalledTimes(1) - expect(initWebRtcConnectionMock).toHaveBeenCalledWith(peer, true) + expect(initWebRtcConnectionMock).toHaveBeenCalledWith(peer, expect.anything(), true) expect(pm['getBrokeringPeers'](peer)[0]).toEqual(peer) }) diff --git a/ironfish/src/network/peers/peerManager.ts b/ironfish/src/network/peers/peerManager.ts index db329e4f11..6be1809d72 100644 --- a/ironfish/src/network/peers/peerManager.ts +++ b/ironfish/src/network/peers/peerManager.ts @@ -22,7 +22,7 @@ import { PeerListMessage } from '../messages/peerList' import { PeerListRequestMessage } from '../messages/peerListRequest' import { SignalMessage } from '../messages/signal' import { SignalRequestMessage } from '../messages/signalRequest' -import { IsomorphicWebSocket } from '../types' +import { IsomorphicWebSocket, NodeDataChannelType } from '../types' import { formatWebSocketAddress, WebSocketAddress } from '../utils' import { VERSION_PROTOCOL_MIN } from '../version' import { ConnectionRetry } from './connectionRetry' @@ -275,7 +275,7 @@ export class PeerManager { } if (canInitiateWebRTC(this.localPeer.publicIdentity, peer.state.identity)) { - this.initWebRtcConnection(peer, true) + this.initWebRtcConnection(peer, this.localPeer.nodeDataChannel, true) return true } @@ -284,7 +284,7 @@ export class PeerManager { destinationIdentity: peer.state.identity, }) - const connection = this.initWebRtcConnection(peer, false) + const connection = this.initWebRtcConnection(peer, this.localPeer.nodeDataChannel, false) connection.setState({ type: 'REQUEST_SIGNALING' }) const brokeringPeers = this.getBrokeringPeers(peer) @@ -319,10 +319,13 @@ export class PeerManager { direction: ConnectionDirection, wsAddress: WebSocketAddress | null, ): WebSocketConnection { - const connection = new WebSocketConnection(ws, direction, this.logger, this.metrics, { - hostname: wsAddress?.host, - port: wsAddress?.port || undefined, - }) + const connection = new WebSocketConnection( + ws, + direction, + this.logger, + this.metrics, + wsAddress, + ) this.initConnectionHandlers(peer, connection) peer.setWebSocketConnection(connection) @@ -335,10 +338,20 @@ export class PeerManager { * @param peer The peer to establish a connection with * @param initiator Set to true if we are initiating a connection with `peer` */ - private initWebRtcConnection(peer: Peer, initiator: boolean): WebRtcConnection { - const connection = new WebRtcConnection(initiator, this.logger, this.metrics, { - stunServers: this.stunServers, - }) + private initWebRtcConnection( + peer: Peer, + nodeDataChannel: NodeDataChannelType, + initiator: boolean, + ): WebRtcConnection { + const connection = new WebRtcConnection( + nodeDataChannel, + initiator, + this.logger, + this.metrics, + { + stunServers: this.stunServers, + }, + ) connection.onSignal.on((data) => { let errorMessage @@ -1133,7 +1146,7 @@ export class PeerManager { connection instanceof WebSocketConnection && connection.direction === ConnectionDirection.Inbound ) { - connection.port = message.port || undefined + connection.port = message.port } peer.name = message.name @@ -1258,7 +1271,7 @@ export class PeerManager { return } - this.initWebRtcConnection(targetPeer, true) + this.initWebRtcConnection(targetPeer, this.localPeer.nodeDataChannel, true) } /** @@ -1363,7 +1376,11 @@ export class PeerManager { return } - signalingConnection = this.initWebRtcConnection(signalingPeer, false) + signalingConnection = this.initWebRtcConnection( + signalingPeer, + this.localPeer.nodeDataChannel, + false, + ) } else { signalingConnection = signalingPeer.state.connections.webRtc } diff --git a/ironfish/src/network/peers/peerStoreManager.test.ts b/ironfish/src/network/peers/peerStoreManager.test.ts index e82ee2cebc..fd1acf32d8 100644 --- a/ironfish/src/network/peers/peerStoreManager.test.ts +++ b/ironfish/src/network/peers/peerStoreManager.test.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ jest.mock('ws') +import { PeerAddress } from '../../fileStores' import { getConnectedPeer, getConnectingPeer, @@ -11,7 +12,6 @@ import { mockPeerStore, } from '../testUtilities' import { Peer } from './peer' -import { PeerAddress } from './peerAddress' import { PeerManager } from './peerManager' import { MAX_PEER_ADDRESSES, PeerStoreManager } from './peerStoreManager' @@ -27,6 +27,8 @@ describe('PeerStoreManager', () => { jest.useRealTimers() }) + const randomWsAddress = () => ({ host: Math.random().toString(36).substring(7), port: 9033 }) + it('removePeer should remove a peer address', async () => { const now = Date.now() jest.setSystemTime(now) @@ -39,11 +41,13 @@ describe('PeerStoreManager', () => { expect(peerStoreManager.priorConnectedPeerAddresses.length).toEqual(0) const { peer: peer1 } = getConnectedPeer(pm) + peer1.wsAddress = randomWsAddress() await peerStoreManager.addPeer(peer1) expect(peerStoreManager.priorConnectedPeerAddresses.length).toEqual(1) const { peer: peer2 } = getConnectedPeer(pm) + peer2.wsAddress = randomWsAddress() await peerStoreManager.addPeer(peer2) expect(peerStoreManager.priorConnectedPeerAddresses.length).toEqual(2) @@ -54,7 +58,6 @@ describe('PeerStoreManager', () => { const peer2Address = { address: peer2.address, port: peer2.port, - identity: peer2.state.identity, name: peer2.name, lastAddedTimestamp: now, } @@ -77,11 +80,13 @@ describe('PeerStoreManager', () => { expect(peerStoreManager.priorConnectedPeerAddresses.length).toEqual(0) const { peer: peer1 } = getConnectedPeer(pm) + peer1.wsAddress = randomWsAddress() void peerStoreManager.addPeer(peer1) expect(peerStoreManager.priorConnectedPeerAddresses.length).toEqual(1) const { peer: peer2 } = getConnectedPeer(pm) + peer2.wsAddress = randomWsAddress() void peerStoreManager.addPeer(peer2) expect(peerStoreManager.priorConnectedPeerAddresses.length).toEqual(2) @@ -92,7 +97,6 @@ describe('PeerStoreManager', () => { const peer2Address = { address: peer2.address, port: peer2.port, - identity: peer2.state.identity, name: peer2.name, lastAddedTimestamp: now, } @@ -112,18 +116,19 @@ describe('PeerStoreManager', () => { const peerStoreManager = new PeerStoreManager(peerStore) peerStoreManager.peerStore = peerStore const { peer: connectedPeer } = getConnectedPeer(pm) + connectedPeer.wsAddress = randomWsAddress() const { peer: connectingPeer } = getConnectingPeer(pm) + connectingPeer.wsAddress = randomWsAddress() const disconnectedPeer = getDisconnectedPeer(pm) - const allPeers: Peer[] = [connectedPeer, connectingPeer, disconnectedPeer] + disconnectedPeer.wsAddress = randomWsAddress() - for (const peer of allPeers) { + for (const peer of [connectedPeer, connectingPeer, disconnectedPeer]) { await peerStoreManager.addPeer(peer) } const connectedPeerAddress = { address: connectedPeer.address, port: connectedPeer.port, - identity: connectedPeer.state.identity, name: connectedPeer.name, lastAddedTimestamp: now, } @@ -134,22 +139,15 @@ describe('PeerStoreManager', () => { it('if more than MAX_PEER_ADDRESSES, then only load MAX_PEER_ADDRESSES peers', () => { const peerStore = mockPeerStore() - const pm = new PeerManager(mockLocalPeer(), peerStore) - const { peer: connectedPeer } = getConnectedPeer(pm) - const peerAddress = { - address: connectedPeer.address || '', - port: connectedPeer.port || 0, - identity: connectedPeer.state.identity, - name: connectedPeer.name, - lastAddedTimestamp: Date.now(), - } peerStore.set( 'priorPeers', Array.from({ length: MAX_PEER_ADDRESSES + 10 }, () => { - const randomIdentity = Math.random().toString(36).substring(7) + const wsAddress = randomWsAddress() return { - ...peerAddress, - identity: randomIdentity, + address: wsAddress.host, + port: wsAddress.port, + name: Math.random().toString(36).substring(7), + lastAddedTimestamp: Date.now(), } }), ) @@ -167,11 +165,12 @@ describe('PeerStoreManager', () => { const peerStoreManager = new PeerStoreManager(peerStore) const { peer: oldestPeer } = getConnectedPeer(pm) + oldestPeer.wsAddress = randomWsAddress() + await peerStoreManager.addPeer(oldestPeer) const oldestPeerAddress = { address: oldestPeer.address, port: oldestPeer.port, - identity: oldestPeer.state.identity, name: oldestPeer.name, lastAddedTimestamp: oldestNow, } @@ -183,11 +182,12 @@ describe('PeerStoreManager', () => { jest.setSystemTime(newNow) const { peer: newPeer } = getConnectedPeer(pm) + newPeer.wsAddress = randomWsAddress() + await peerStoreManager.addPeer(newPeer) const newPeerAddress = { address: newPeer.address, port: newPeer.port, - identity: newPeer.state.identity, name: newPeer.name, lastAddedTimestamp: newNow, } @@ -196,12 +196,11 @@ describe('PeerStoreManager', () => { expect(peerStoreManager.priorConnectedPeerAddresses).toContainEqual(newPeerAddress) for (let i = 0; i < MAX_PEER_ADDRESSES - 1; i++) { - const randomIdentity = Math.random().toString(36).substring(7) + newPeer.wsAddress = randomWsAddress() await peerStoreManager.addPeer({ ...newPeer, state: { ...newPeer.state, - identity: randomIdentity, }, } as Peer) } @@ -225,7 +224,6 @@ describe('PeerStoreManager', () => { const peerAddress = { address: peer.address, port: peer.port, - identity: peer.state.identity, name: peer.name, lastAddedTimestamp: now, } @@ -253,7 +251,6 @@ describe('PeerStoreManager', () => { const address: PeerAddress = { address: connectedPeer.address || '', port: connectedPeer.port || 0, - identity: connectedPeer.state.identity || '', name: connectedPeer.name, lastAddedTimestamp: now, } diff --git a/ironfish/src/network/peers/peerStoreManager.ts b/ironfish/src/network/peers/peerStoreManager.ts index b6b301ca8e..c7da44f5e1 100644 --- a/ironfish/src/network/peers/peerStoreManager.ts +++ b/ironfish/src/network/peers/peerStoreManager.ts @@ -1,11 +1,11 @@ /* 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 { PeerStore } from '../../fileStores' -import { Identity } from '../identity' +import { PeerAddress, PeerStore } from '../../fileStores' +import { PriorityQueue } from '../../utils' +import { formatFullWebSocketAddress, formatWebSocketAddress } from '../utils/url' import { ConnectionDirection } from './connections' import { Peer } from './peer' -import { PeerAddress } from './peerAddress' export const MAX_PEER_ADDRESSES = 50 @@ -15,60 +15,51 @@ export const MAX_PEER_ADDRESSES = 50 */ export class PeerStoreManager { peerStore: PeerStore - private peerIdentityMap: Map + + // Sort the peers with the oldest peer at the front of the queue + private peers = new PriorityQueue( + (p1, p2) => p1.lastAddedTimestamp < p2.lastAddedTimestamp, + (p) => formatFullWebSocketAddress({ host: p.address, port: p.port }), + ) constructor(peerStore: PeerStore) { this.peerStore = peerStore + // Load prior peers from disk - this.peerIdentityMap = new Map() + for (const peer of this.peerStore.getPriorPeers()) { + const existing = this.peers.remove(this.peers.hash(peer)) - let priorPeers = this.peerStore.getArray('priorPeers').filter((peer) => { - if (peer.identity === null || peer.address === null || peer.port === null) { - return false + if (existing && existing.lastAddedTimestamp > peer.lastAddedTimestamp) { + this.insertPeerAddress(existing) + } else { + this.insertPeerAddress(peer) } - - peer.lastAddedTimestamp = peer.lastAddedTimestamp ?? 0 - - return true - }) - - if (priorPeers.length > MAX_PEER_ADDRESSES) { - priorPeers = priorPeers.slice(0, MAX_PEER_ADDRESSES) - } - - for (const peer of priorPeers) { - this.peerIdentityMap.set(peer.identity, peer) } } get priorConnectedPeerAddresses(): ReadonlyArray> { - return [...this.peerIdentityMap.values()] + return [...this.peers.sorted()] } /** * Removes address associated with a peer from address stores */ async removePeer(peer: Peer): Promise { - if (peer.state.identity === null) { - return - } + const toRemove = formatWebSocketAddress(peer.wsAddress) - this.peerIdentityMap.delete(peer.state.identity) - await this.save() + if (toRemove) { + this.peers.remove(toRemove) + await this.save() + } } /** - * Adds a peer with the following conditions: - * 1. Peer is connected - * 2. Identity is valid - * 3. Peer has an outbound websocket connection + * Adds a peer if the peer has an outbound websocket connection */ async addPeer(peer: Peer): Promise { - if (peer.state.identity === null || peer.address === null || peer.port === null) { - return - } - if ( + peer.wsAddress === null || + peer.wsAddress.port === null || peer.state.type !== 'CONNECTED' || !peer.state.connections.webSocket || peer.state.connections.webSocket.direction !== ConnectionDirection.Outbound @@ -76,32 +67,9 @@ export class PeerStoreManager { return } - const peerAddress = this.peerIdentityMap.get(peer.state.identity) - - // If the peer is already in the address manager, update the timestamp, - // address and port - if (peerAddress) { - peerAddress.address = peer.address - peerAddress.port = peer.port - peerAddress.lastAddedTimestamp = Date.now() - this.peerIdentityMap.set(peer.state.identity, peerAddress) - await this.save() - return - } - - // If the address manager is full, remove the oldest peer - if (this.peerIdentityMap.size >= MAX_PEER_ADDRESSES) { - const oldestPeerIdentity = [...this.peerIdentityMap.entries()].sort( - (a, b) => a[1].lastAddedTimestamp - b[1].lastAddedTimestamp, - )[0][0] - - this.peerIdentityMap.delete(oldestPeerIdentity) - } - - this.peerIdentityMap.set(peer.state.identity, { - address: peer.address, - port: peer.port, - identity: peer.state.identity, + this.insertPeerAddress({ + address: peer.wsAddress.host, + port: peer.wsAddress.port, name: peer.name ?? null, lastAddedTimestamp: Date.now(), }) @@ -109,8 +77,18 @@ export class PeerStoreManager { await this.save() } + private insertPeerAddress(peerAddress: PeerAddress) { + this.peers.remove(this.peers.hash(peerAddress)) + this.peers.add(peerAddress) + + // Make sure we don't store too many peers + while (this.peers.size() > MAX_PEER_ADDRESSES) { + this.peers.poll() + } + } + async save(): Promise { - this.peerStore.set('priorPeers', [...this.peerIdentityMap.values()]) + this.peerStore.set('priorPeers', [...this.peers.sorted()]) await this.peerStore.save() } } diff --git a/ironfish/src/network/testUtilities/helpers.ts b/ironfish/src/network/testUtilities/helpers.ts index 515af1dd93..95697764fd 100644 --- a/ironfish/src/network/testUtilities/helpers.ts +++ b/ironfish/src/network/testUtilities/helpers.ts @@ -24,7 +24,7 @@ import { mockIdentity } from './mockIdentity' export function getConnectingPeer( pm: PeerManager, direction = ConnectionDirection.Outbound, - identity?: string | Identity, + identity?: string, ): { peer: Peer; connection: WebSocketConnection } { let peer: Peer | undefined @@ -71,7 +71,7 @@ export function getConnectingPeer( export function getWaitingForIdentityPeer( pm: PeerManager, direction = ConnectionDirection.Outbound, - identity?: string | Identity, + identity?: string, ): { peer: Peer; connection: WebSocketConnection } { const { peer, connection } = getConnectingPeer(pm, direction, identity) connection.setState({ type: 'WAITING_FOR_IDENTITY' }) @@ -103,7 +103,7 @@ export const getConnectedPeersWithSpies = ( export function getConnectedPeer( pm: PeerManager, - identity?: string | Identity, + identity?: string, ): { peer: Peer; connection: WebSocketConnection } { const { peer, connection } = getConnectingPeer(pm) @@ -122,7 +122,7 @@ export function getConnectedPeer( return { peer, connection: connection } } -export function getDisconnectedPeer(pm: PeerManager, identity?: string | Identity): Peer { +export function getDisconnectedPeer(pm: PeerManager, identity?: string): Peer { if (!identity) { identity = jest.requireActual('uuid').v4() } diff --git a/ironfish/src/network/testUtilities/mockLocalPeer.ts b/ironfish/src/network/testUtilities/mockLocalPeer.ts index 59da2e01b1..96ff2664e9 100644 --- a/ironfish/src/network/testUtilities/mockLocalPeer.ts +++ b/ironfish/src/network/testUtilities/mockLocalPeer.ts @@ -1,15 +1,36 @@ /* 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/. */ - +/* eslint-disable @typescript-eslint/no-empty-function */ import { Blockchain } from '../../blockchain' import { mockChain } from '../../testUtilities/mocks' import { PrivateIdentity } from '../identity' import { LocalPeer } from '../peers/localPeer' +import { NodeDataChannelType } from '../types' import { VERSION_PROTOCOL } from '../version' import { WebSocketClient } from '../webSocketClient' import { mockPrivateIdentity } from './mockPrivateIdentity' +const mockNodeDataChannel: NodeDataChannelType = { + PeerConnection: class { + onLocalDescription() {} + onLocalCandidate() {} + onDataChannel() {} + createDataChannel() { + return { + onOpen: () => {}, + onError: () => {}, + onClosed: () => {}, + onMessage: () => {}, + close: () => {}, + isOpen: () => {}, + sendMessage: () => {}, + sendMessageBinary: () => {}, + } + } + }, +} as unknown as NodeDataChannelType + /** * Utility to create a fake "keypair" for testing the network layer */ @@ -24,5 +45,14 @@ export function mockLocalPeer({ version?: number chain?: Blockchain } = {}): LocalPeer { - return new LocalPeer(identity, agent, version, chain || mockChain(), WebSocketClient, 0, true) + return new LocalPeer( + identity, + agent, + version, + chain || mockChain(), + WebSocketClient, + mockNodeDataChannel, + 0, + true, + ) } diff --git a/ironfish/src/network/testUtilities/mockPeerStore.ts b/ironfish/src/network/testUtilities/mockPeerStore.ts index 7fa713013a..055280fe44 100644 --- a/ironfish/src/network/testUtilities/mockPeerStore.ts +++ b/ironfish/src/network/testUtilities/mockPeerStore.ts @@ -1,9 +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 { DEFAULT_DATA_DIR, PeerStore, PeerStoreOptions } from '../../fileStores' +import { DEFAULT_DATA_DIR, PeerAddress, PeerStore, PeerStoreOptions } from '../../fileStores' import { FileSystem } from '../../fileSystems' -import { PeerAddress } from '../peers/peerAddress' /** * Utility to create a fake PeerStore for use in diff --git a/ironfish/src/network/types.ts b/ironfish/src/network/types.ts index 93db6dbb5e..a4ae3e41e6 100644 --- a/ironfish/src/network/types.ts +++ b/ironfish/src/network/types.ts @@ -1,6 +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/. */ +// @ts-expect-error Allow type-only import https://github.com/microsoft/TypeScript/issues/49721 +import type nodeDataChannel from 'node-datachannel' import type { ErrorEvent as WSErrorEvent } from 'ws' import { WebSocketClient } from './webSocketClient' @@ -34,6 +36,7 @@ export enum NetworkMessageType { NewTransactions = 83, } +export type NodeDataChannelType = typeof nodeDataChannel export type IsomorphicWebSocketConstructor = typeof WebSocket | typeof WebSocketClient export type IsomorphicWebSocket = WebSocket | WebSocketClient export type IsomorphicWebSocketErrorEvent = WSErrorEvent diff --git a/ironfish/src/network/utils/url.ts b/ironfish/src/network/utils/url.ts index 457fbe07ea..c74f226ed5 100644 --- a/ironfish/src/network/utils/url.ts +++ b/ironfish/src/network/utils/url.ts @@ -51,10 +51,13 @@ export function parseUrl(url: string): { * Format web socket address into a string */ export function formatWebSocketAddress(address: WebSocketAddress | null): string | null { - if (!address) { - return null - } + return address ? formatFullWebSocketAddress(address) : null +} +/** + * Format web socket address into a string + */ +export function formatFullWebSocketAddress(address: WebSocketAddress): string { const { host, port } = address const portString = port ? `${PORT_SEPARATOR}${port}` : `` return `ws://${host}${portString}` diff --git a/ironfish/src/network/webSocketClient.ts b/ironfish/src/network/webSocketClient.ts index c045facca2..880a74610d 100644 --- a/ironfish/src/network/webSocketClient.ts +++ b/ironfish/src/network/webSocketClient.ts @@ -12,7 +12,7 @@ import { WEBSOCKET_OPTIONS } from './webSocketServer' */ export class WebSocketClient extends WSWebSocket { constructor( - address: string | URL, + address: string, protocols?: string | string[], options?: WSWebSocket.ClientOptions | http.ClientRequestArgs, ) { diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index 9e42048cd5..8eb1deca72 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -22,7 +22,7 @@ import { MetricsMonitor } from './metrics' import { Migrator } from './migrations' import { MiningManager } from './mining' import { PeerNetwork, PrivateIdentity, privateIdentityToIdentity } from './network' -import { IsomorphicWebSocketConstructor } from './network/types' +import { IsomorphicWebSocketConstructor, NodeDataChannelType } from './network/types' import { getNetworkDefinition } from './networkDefinition' import { Package } from './package' import { Platform } from './platform' @@ -71,6 +71,7 @@ export class FullNode { workerPool, logger, webSocket, + nodeDataChannel, privateIdentity, peerStore, networkId, @@ -88,6 +89,7 @@ export class FullNode { workerPool: WorkerPool logger: Logger webSocket: IsomorphicWebSocketConstructor + nodeDataChannel: NodeDataChannelType privateIdentity?: PrivateIdentity peerStore: PeerStore networkId: number @@ -113,7 +115,7 @@ export class FullNode { this.logger = logger this.pkg = pkg - this.migrator = new Migrator({ node: this, logger }) + this.migrator = new Migrator({ context: this, logger }) const identity = privateIdentity || new BoxKeyPair() @@ -150,6 +152,7 @@ export class FullNode { bootstrapNodes: config.getArray('bootstrapNodes'), stunServers: config.getArray('p2pStunServers'), webSocket: webSocket, + nodeDataChannel: nodeDataChannel, node: this, chain: chain, metrics: this.metrics, @@ -234,7 +237,7 @@ export class FullNode { const numWorkers = calculateWorkers(config.get('nodeWorkers'), config.get('nodeWorkersMax')) - const workerPool = new WorkerPool({ metrics, numWorkers }) + const workerPool = new WorkerPool({ logger, metrics, numWorkers }) metrics = metrics || new MetricsMonitor({ logger }) @@ -270,6 +273,7 @@ export class FullNode { average: config.get('feeEstimatorPercentileAverage'), fast: config.get('feeEstimatorPercentileFast'), }, + logger, }) const memPool = new MemPool({ @@ -296,8 +300,11 @@ export class FullNode { workerPool, consensus, nodeClient: memoryClient, + logger, }) + const nodeDataChannel = await import('node-datachannel') + const node = new FullNode({ pkg, chain, @@ -311,6 +318,7 @@ export class FullNode { workerPool, logger, webSocket, + nodeDataChannel, privateIdentity, peerStore, networkId: networkDefinition.id, diff --git a/ironfish/src/primitives/target.ts b/ironfish/src/primitives/target.ts index ec17d4726e..154813f440 100644 --- a/ironfish/src/primitives/target.ts +++ b/ironfish/src/primitives/target.ts @@ -171,7 +171,7 @@ export class Target { * Return whether or not this target meets the requirements of the given target, * which is to say, this has a lower numeric value then the provided one. */ - static meets(hashValue: BigInt, target: Target): boolean { + static meets(hashValue: bigint, target: Target): boolean { return hashValue <= target.targetValue } diff --git a/ironfish/src/rpc/adapters/tlsAdapter.ts b/ironfish/src/rpc/adapters/tlsAdapter.ts index f8556b7de1..37f9713d78 100644 --- a/ironfish/src/rpc/adapters/tlsAdapter.ts +++ b/ironfish/src/rpc/adapters/tlsAdapter.ts @@ -1,12 +1,10 @@ /* 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 { randomBytes } from 'crypto' import net from 'net' import tls from 'tls' import { FileSystem } from '../../fileSystems' import { createRootLogger, Logger } from '../../logger' -import { IronfishNode } from '../../utils' import { TlsUtils } from '../../utils/tls' import { ApiNamespace } from '../routes' import { RpcSocketAdapter } from './socketAdapter/socketAdapter' @@ -15,7 +13,6 @@ export class RpcTlsAdapter extends RpcSocketAdapter { readonly fileSystem: FileSystem readonly nodeKeyPath: string readonly nodeCertPath: string - node: IronfishNode constructor( host: string, @@ -23,7 +20,6 @@ export class RpcTlsAdapter extends RpcSocketAdapter { fileSystem: FileSystem, nodeKeyPath: string, nodeCertPath: string, - node: IronfishNode, logger: Logger = createRootLogger(), namespaces: ApiNamespace[], ) { @@ -31,22 +27,10 @@ export class RpcTlsAdapter extends RpcSocketAdapter { this.fileSystem = fileSystem this.nodeKeyPath = nodeKeyPath this.nodeCertPath = nodeCertPath - this.node = node this.enableAuthentication = true } protected async createServer(): Promise { - const rpcAuthToken = this.node.internal.get('rpcAuthToken') - - if (!rpcAuthToken || rpcAuthToken === '') { - this.logger.debug( - `Missing RPC Auth token in internal.json config. Automatically generating auth token.`, - ) - const newPassword = randomBytes(32).toString('hex') - this.node.internal.set('rpcAuthToken', newPassword) - await this.node.internal.save() - } - const options = await TlsUtils.getTlsOptions( this.fileSystem, this.nodeKeyPath, diff --git a/ironfish/src/rpc/index.ts b/ironfish/src/rpc/index.ts index 4cf6aca546..0bebf3b35d 100644 --- a/ironfish/src/rpc/index.ts +++ b/ironfish/src/rpc/index.ts @@ -8,4 +8,4 @@ export * from './response' export * from './routes' export * from './server' export * from './stream' -export * from './types' +export * from './routes/types' diff --git a/ironfish/src/rpc/routes/chain/broadcastTransaction.ts b/ironfish/src/rpc/routes/chain/broadcastTransaction.ts index 6756bb7ced..fb7ed23322 100644 --- a/ironfish/src/rpc/routes/chain/broadcastTransaction.ts +++ b/ironfish/src/rpc/routes/chain/broadcastTransaction.ts @@ -39,22 +39,22 @@ export const BroadcastTransactionResponseSchema: yup.ObjectSchema( `${ApiNamespace.chain}/broadcastTransaction`, BroadcastTransactionRequestSchema, - async (request, node): Promise => { - Assert.isInstanceOf(node, FullNode) + async (request, context): Promise => { + Assert.isInstanceOf(context, FullNode) const data = Buffer.from(request.data.transaction, 'hex') const transaction = new Transaction(data) - const verify = await node.chain.verifier.verifyNewTransaction(transaction) + const verify = await context.chain.verifier.verifyNewTransaction(transaction) if (!verify.valid) { throw new ValidationError(`Invalid transaction, reason: ${String(verify.reason)}`) } - const accepted = node.memPool.acceptTransaction(transaction) + const accepted = context.memPool.acceptTransaction(transaction) let broadcasted = false - if (node.peerNetwork.isReady) { - node.peerNetwork.broadcastTransaction(transaction) + if (context.peerNetwork.isReady) { + context.peerNetwork.broadcastTransaction(transaction) broadcasted = true } diff --git a/ironfish/src/rpc/routes/chain/exportChainStream.ts b/ironfish/src/rpc/routes/chain/exportChainStream.ts index 7bbfffa72c..00120ae73d 100644 --- a/ironfish/src/rpc/routes/chain/exportChainStream.ts +++ b/ironfish/src/rpc/routes/chain/exportChainStream.ts @@ -5,9 +5,9 @@ import * as yup from 'yup' import { Assert } from '../../../assert' import { FullNode } from '../../../node' import { BlockchainUtils } from '../../../utils/blockchain' -import { RpcBlockHeader, RpcBlockHeaderSchema, serializeRpcBlockHeader } from '../../types' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { RpcBlockHeader, RpcBlockHeaderSchema, serializeRpcBlockHeader } from '../types' export type ExportChainStreamRequest = | { diff --git a/ironfish/src/rpc/routes/chain/followChainStream.ts b/ironfish/src/rpc/routes/chain/followChainStream.ts index cfb8bb70a5..d8041ec17d 100644 --- a/ironfish/src/rpc/routes/chain/followChainStream.ts +++ b/ironfish/src/rpc/routes/chain/followChainStream.ts @@ -9,9 +9,9 @@ import { FullNode } from '../../../node' import { Block, BlockHeader } from '../../../primitives' import { BlockHashSerdeInstance } from '../../../serde' import { BufferUtils, PromiseUtils } from '../../../utils' -import { RpcBlock, RpcBlockSchema, serializeRpcBlockHeader } from '../../types' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { RpcBlock, RpcBlockSchema, serializeRpcBlockHeader } from '../types' export type FollowChainStreamRequest = | { @@ -65,13 +65,13 @@ export const FollowChainStreamResponseSchema: yup.ObjectSchema( `${ApiNamespace.chain}/followChainStream`, FollowChainStreamRequestSchema, - async (request, node): Promise => { - Assert.isInstanceOf(node, FullNode) + async (request, context): Promise => { + Assert.isInstanceOf(context, FullNode) const head = request.data?.head ? Buffer.from(request.data.head, 'hex') : null const processor = new ChainProcessor({ - chain: node.chain, - logger: node.logger, + chain: context.chain, + logger: context.logger, head: head, }) @@ -120,7 +120,7 @@ routes.register { - const block = await node.chain.getBlock(header) + const block = await context.chain.getBlock(header) Assert.isNotNull(block) send(block, 'connected') } const onRemove = async (header: BlockHeader) => { - const block = await node.chain.getBlock(header) + const block = await context.chain.getBlock(header) Assert.isNotNull(block) send(block, 'disconnected') } @@ -161,7 +161,7 @@ routes.register = yup routes.register( `${ApiNamespace.chain}/getBlock`, GetBlockRequestSchema, - async (request, node): Promise => { - Assert.isInstanceOf(node, FullNode) + async (request, context): Promise => { + Assert.isInstanceOf(context, FullNode) let header: BlockHeader | null = null let error = '' - const confirmations = request.data.confirmations ?? node.config.get('confirmations') + const confirmations = request.data.confirmations ?? context.config.get('confirmations') if (request.data.search) { const search = request.data.search.trim() @@ -77,19 +77,19 @@ routes.register( // Use negative numbers to start from the head of the chain if (request.data.sequence && request.data.sequence < 0) { request.data.sequence = Math.max( - node.chain.head.sequence + request.data.sequence + 1, + context.chain.head.sequence + request.data.sequence + 1, GENESIS_BLOCK_SEQUENCE, ) } if (request.data.hash) { const hash = Buffer.from(request.data.hash, 'hex') - header = await node.chain.getHeader(hash) + header = await context.chain.getHeader(hash) error = `No block found with hash ${request.data.hash}` } if (request.data.sequence && !header) { - header = await node.chain.getHeaderAtSequence(request.data.sequence) + header = await context.chain.getHeaderAtSequence(request.data.sequence) error = `No block found with sequence ${request.data.sequence}` } @@ -101,7 +101,7 @@ routes.register( throw new ValidationError('Block header was saved to database without a note size') } - const block = await node.chain.getBlock(header) + const block = await context.chain.getBlock(header) if (!block) { throw new NotFoundError(`No block with header ${header.hash.toString('hex')}`) } @@ -145,8 +145,8 @@ routes.register( }) } - const main = await node.chain.isHeadChain(header) - const confirmed = node.chain.head.sequence - header.sequence >= confirmations + const main = await context.chain.isHeadChain(header) + const confirmed = context.chain.head.sequence - header.sequence >= confirmations const blockHeaderResponse = serializeRpcBlockHeader(header) diff --git a/ironfish/src/rpc/routes/chain/getNetworkHashPower.ts b/ironfish/src/rpc/routes/chain/getNetworkHashPower.ts index 97548e3f69..2bd5b6de2f 100644 --- a/ironfish/src/rpc/routes/chain/getNetworkHashPower.ts +++ b/ironfish/src/rpc/routes/chain/getNetworkHashPower.ts @@ -40,8 +40,8 @@ export const GetNetworkHashPowerResponseSchema: yup.ObjectSchema( `${ApiNamespace.chain}/getNetworkHashPower`, GetNetworkHashPowerRequestSchema, - async (request, node): Promise => { - Assert.isInstanceOf(node, FullNode) + async (request, context): Promise => { + Assert.isInstanceOf(context, FullNode) let blocks = request.data?.blocks ?? 120 let sequence = request.data?.sequence ?? -1 @@ -50,17 +50,17 @@ routes.register 0 && sequence < node.chain.head.sequence) { - const blockAtSequence = await node.chain.getHeaderAtSequence(sequence) + if (sequence > 0 && sequence < context.chain.head.sequence) { + const blockAtSequence = await context.chain.getHeaderAtSequence(sequence) if (!blockAtSequence) { throw new Error(`No end block found at sequence ${sequence}`) } @@ -72,7 +72,7 @@ routes.register( `${ApiNamespace.chain}/getTransaction`, GetTransactionRequestSchema, - async (request, node): Promise => { - Assert.isInstanceOf(node, FullNode) + async (request, context): Promise => { + Assert.isInstanceOf(context, FullNode) if (!request.data.transactionHash) { throw new ValidationError(`Missing transaction hash`) @@ -65,7 +65,7 @@ routes.register( const blockHashBuffer = request.data.blockHash ? BlockHashSerdeInstance.deserialize(request.data.blockHash) - : await node.chain.getBlockHashByTransactionHash(transactionHashBuffer) + : await context.chain.getBlockHashByTransactionHash(transactionHashBuffer) if (!blockHashBuffer) { throw new NotFoundError( @@ -73,14 +73,14 @@ routes.register( ) } - const blockHeader = await node.chain.getHeader(blockHashBuffer) + const blockHeader = await context.chain.getHeader(blockHashBuffer) if (!blockHeader) { throw new NotFoundError( `No block found for block hash ${blockHashBuffer.toString('hex')}`, ) } - const transactions = await node.chain.getBlockTransactions(blockHeader) + const transactions = await context.chain.getBlockTransactions(blockHeader) const foundTransaction = transactions.find(({ transaction }) => transaction.hash().equals(transactionHashBuffer), diff --git a/ironfish/src/rpc/routes/chain/getTransactionStream.ts b/ironfish/src/rpc/routes/chain/getTransactionStream.ts index 6bc42ee711..492b53862b 100644 --- a/ironfish/src/rpc/routes/chain/getTransactionStream.ts +++ b/ironfish/src/rpc/routes/chain/getTransactionStream.ts @@ -11,6 +11,8 @@ import { BufferUtils, CurrencyUtils } from '../../../utils' import { PromiseUtils } from '../../../utils/promise' import { isValidIncomingViewKey, isValidOutgoingViewKey } from '../../../wallet/validator' import { ValidationError } from '../../adapters/errors' +import { ApiNamespace } from '../namespaces' +import { routes } from '../router' import { RpcBlockHeader, RpcBlockHeaderSchema, @@ -19,9 +21,7 @@ import { RpcMint, RpcMintSchema, serializeRpcBlockHeader, -} from '../../types' -import { ApiNamespace } from '../namespaces' -import { routes } from '../router' +} from '../types' interface Note { assetId: string diff --git a/ironfish/src/rpc/routes/chain/showChain.ts b/ironfish/src/rpc/routes/chain/showChain.ts index e41821e66e..25c4e5dfb5 100644 --- a/ironfish/src/rpc/routes/chain/showChain.ts +++ b/ironfish/src/rpc/routes/chain/showChain.ts @@ -38,10 +38,10 @@ export const ShowChainResponseSchema: yup.ObjectSchema = yup routes.register( `${ApiNamespace.chain}/showChain`, ShowChainRequestSchema, - async (request, node): Promise => { - Assert.isInstanceOf(node, FullNode) + async (request, context): Promise => { + Assert.isInstanceOf(context, FullNode) - const content = await renderChain(node.chain, request.data?.start, request.data?.stop, { + const content = await renderChain(context.chain, request.data?.start, request.data?.stop, { indent: ' ', work: false, }) diff --git a/ironfish/src/rpc/routes/chain/types.ts b/ironfish/src/rpc/routes/chain/types.ts index db1ec8bb50..bb45eaf314 100644 --- a/ironfish/src/rpc/routes/chain/types.ts +++ b/ironfish/src/rpc/routes/chain/types.ts @@ -10,7 +10,7 @@ import { RpcEncryptedNoteSchema, RpcMint, RpcMintSchema, -} from '../../types' +} from '../types' export type RpcSpend = { nullifier: string diff --git a/ironfish/src/rpc/routes/config/getConfig.ts b/ironfish/src/rpc/routes/config/getConfig.ts index c49da8adf8..2a5c5f6300 100644 --- a/ironfish/src/rpc/routes/config/getConfig.ts +++ b/ironfish/src/rpc/routes/config/getConfig.ts @@ -6,6 +6,7 @@ import { ConfigOptions, ConfigOptionsSchema } from '../../../fileStores/config' import { ValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' export type GetConfigRequest = { user?: boolean; name?: string } | undefined export type GetConfigResponse = Partial @@ -22,14 +23,16 @@ export const GetConfigResponseSchema: yup.ObjectSchema = Conf routes.register( `${ApiNamespace.config}/getConfig`, GetConfigRequestSchema, - (request, node): void => { - if (request.data?.name && !(request.data.name in node.config.defaults)) { + (request, context): void => { + AssertHasRpcContext(request, context, 'config') + + if (request.data?.name && !(request.data.name in context.config.defaults)) { throw new ValidationError(`No config option ${String(request.data.name)}`) } let pickKeys: string[] | undefined = undefined if (!request.data?.user) { - pickKeys = Object.keys(node.config.defaults) + pickKeys = Object.keys(context.config.defaults) } if (request.data?.name) { pickKeys = [request.data.name] @@ -37,8 +40,8 @@ routes.register( const data = ( request.data?.user - ? JSON.parse(JSON.stringify(node.config.loaded)) - : JSON.parse(JSON.stringify(node.config.config, pickKeys)) + ? JSON.parse(JSON.stringify(context.config.loaded)) + : JSON.parse(JSON.stringify(context.config.config, pickKeys)) ) as GetConfigResponse request.end(data) diff --git a/ironfish/src/rpc/routes/config/setConfig.ts b/ironfish/src/rpc/routes/config/setConfig.ts index 2bf61daccc..a171d4ed1e 100644 --- a/ironfish/src/rpc/routes/config/setConfig.ts +++ b/ironfish/src/rpc/routes/config/setConfig.ts @@ -5,6 +5,7 @@ import * as yup from 'yup' import { ConfigOptions, ConfigOptionsSchema } from '../../../fileStores/config' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { setUnknownConfigValue } from './uploadConfig' export type SetConfigRequest = { name: string; value: unknown } @@ -22,9 +23,11 @@ export const SetConfigResponseSchema: yup.ObjectSchema = Conf routes.register( `${ApiNamespace.config}/setConfig`, SetConfigRequestSchema, - async (request, node): Promise => { - setUnknownConfigValue(node.config, request.data.name, request.data.value) - await node.config.save() + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'config') + + setUnknownConfigValue(context.config, request.data.name, request.data.value) + await context.config.save() request.end() }, ) diff --git a/ironfish/src/rpc/routes/config/unsetConfig.ts b/ironfish/src/rpc/routes/config/unsetConfig.ts index 36ef8d6959..8ccaae1995 100644 --- a/ironfish/src/rpc/routes/config/unsetConfig.ts +++ b/ironfish/src/rpc/routes/config/unsetConfig.ts @@ -5,6 +5,7 @@ import * as yup from 'yup' import { ConfigOptions, ConfigOptionsSchema } from '../../../fileStores/config' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { setUnknownConfigValue } from './uploadConfig' export type UnsetConfigRequest = { name: string } @@ -22,9 +23,11 @@ export const UnsetConfigResponseSchema: yup.ObjectSchema = routes.register( `${ApiNamespace.config}/unsetConfig`, UnsetConfigRequestSchema, - async (request, node): Promise => { - setUnknownConfigValue(node.config, request.data.name) - await node.config.save() + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'config') + + setUnknownConfigValue(context.config, request.data.name) + await context.config.save() request.end() }, ) diff --git a/ironfish/src/rpc/routes/config/uploadConfig.ts b/ironfish/src/rpc/routes/config/uploadConfig.ts index 93b79dae19..bf6cbbd5cb 100644 --- a/ironfish/src/rpc/routes/config/uploadConfig.ts +++ b/ironfish/src/rpc/routes/config/uploadConfig.ts @@ -6,6 +6,7 @@ import { Config, ConfigOptions, ConfigOptionsSchema } from '../../../fileStores/ import { ValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' export type UploadConfigRequest = { config: Record } export type UploadConfigResponse = Partial @@ -20,16 +21,18 @@ export const UploadConfigResponseSchema: yup.ObjectSchema routes.register( `${ApiNamespace.config}/uploadConfig`, UploadConfigRequestSchema, - async (request, node): Promise => { - clearConfig(node.config) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'config') + + clearConfig(context.config) for (const key of Object.keys(request.data.config)) { if (Object.prototype.hasOwnProperty.call(request.data.config, key)) { - setUnknownConfigValue(node.config, key, request.data.config[key], true) + setUnknownConfigValue(context.config, key, request.data.config[key], true) } } - await node.config.save() + await context.config.save() request.end() }, ) diff --git a/ironfish/src/rpc/routes/event/onGossip.ts b/ironfish/src/rpc/routes/event/onGossip.ts index dfb83dcbbb..b625514754 100644 --- a/ironfish/src/rpc/routes/event/onGossip.ts +++ b/ironfish/src/rpc/routes/event/onGossip.ts @@ -5,9 +5,9 @@ import * as yup from 'yup' import { Assert } from '../../../assert' import { FullNode } from '../../../node' import { BlockHeader } from '../../../primitives' -import { RpcBlockHeader, RpcBlockHeaderSchema, serializeRpcBlockHeader } from '../../types' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { RpcBlockHeader, RpcBlockHeaderSchema, serializeRpcBlockHeader } from '../types' export type OnGossipRequest = undefined export type OnGossipResponse = { blockHeader: RpcBlockHeader } diff --git a/ironfish/src/rpc/routes/event/onReorganizeChain.ts b/ironfish/src/rpc/routes/event/onReorganizeChain.ts index 23864cead1..18861f8ee3 100644 --- a/ironfish/src/rpc/routes/event/onReorganizeChain.ts +++ b/ironfish/src/rpc/routes/event/onReorganizeChain.ts @@ -5,9 +5,9 @@ import * as yup from 'yup' import { Assert } from '../../../assert' import { FullNode } from '../../../node' import { BlockHeader } from '../../../primitives' -import { RpcBlockHeader, RpcBlockHeaderSchema, serializeRpcBlockHeader } from '../../types' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { RpcBlockHeader, RpcBlockHeaderSchema, serializeRpcBlockHeader } from '../types' export type OnReorganizeChainRequest = undefined export type OnReorganizeChainResponse = { diff --git a/ironfish/src/rpc/routes/faucet/getFunds.test.ts b/ironfish/src/rpc/routes/faucet/getFunds.test.ts index 2c24611158..df055379ca 100644 --- a/ironfish/src/rpc/routes/faucet/getFunds.test.ts +++ b/ironfish/src/rpc/routes/faucet/getFunds.test.ts @@ -37,14 +37,10 @@ describe('Route faucet.getFunds', () => { // Response gives back string for ID expect(response).toMatchObject({ status: 200, content: { id: '5' } }) - expect(axios.post).toHaveBeenCalledWith( - 'foo.com', - { - email, - public_key: publicAddress, - }, - expect.anything(), - ) + expect(axios.post).toHaveBeenCalledWith('foo.com', { + email, + public_key: publicAddress, + }) }) }) diff --git a/ironfish/src/rpc/routes/faucet/getFunds.ts b/ironfish/src/rpc/routes/faucet/getFunds.ts index 0bf6a42f4d..b559c3788d 100644 --- a/ironfish/src/rpc/routes/faucet/getFunds.ts +++ b/ironfish/src/rpc/routes/faucet/getFunds.ts @@ -1,13 +1,13 @@ /* 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 { AxiosError } from 'axios' +import axios, { AxiosError } from 'axios' import * as yup from 'yup' import { Assert } from '../../../assert' -import { WebApi } from '../../../webApi' import { ERROR_CODES, ResponseError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from '../wallet/utils' export type GetFundsRequest = { account?: string; email?: string } @@ -29,51 +29,71 @@ export const GetFundsResponseSchema: yup.ObjectSchema = yup routes.register( `${ApiNamespace.faucet}/getFunds`, GetFundsRequestSchema, - async (request, node): Promise => { + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'internal', 'config', 'wallet') + // check node network id - const networkId = node.internal.get('networkId') + const networkId = context.internal.get('networkId') if (networkId !== 0) { // not testnet throw new ResponseError('This endpoint is only available for testnet.', ERROR_CODES.ERROR) } - const account = getAccount(node.wallet, request.data.account) + const account = getAccount(context.wallet, request.data.account) - const api = new WebApi({ - getFundsEndpoint: node.config.get('getFundsApi'), - }) + const getFundsEndpoint = context.config.get('getFundsApi') + + const response = await getFunds(getFundsEndpoint, { + email: request.data.email, + public_key: account.publicAddress, + }).catch((error: AxiosError<{ code: string; message?: string }>) => { + if (error.response) { + const { data, status } = error.response - const response = await api - .getFunds({ - email: request.data.email, - public_key: account.publicAddress, - }) - .catch((error: AxiosError<{ code: string; message?: string }>) => { - if (error.response) { - const { data, status } = error.response - - if (status === 422) { - if (data.code === 'faucet_max_requests_reached') { - Assert.isNotUndefined(data.message) - throw new ResponseError(data.message, ERROR_CODES.VALIDATION, status) - } - - throw new ResponseError( - 'You entered an invalid email.', - ERROR_CODES.VALIDATION, - status, - ) - } else if (data.message) { - throw new ResponseError(data.message, ERROR_CODES.ERROR, status) + if (status === 422) { + if (data.code === 'faucet_max_requests_reached') { + Assert.isNotUndefined(data.message) + throw new ResponseError(data.message, ERROR_CODES.VALIDATION, status) } + + throw new ResponseError( + 'You entered an invalid email.', + ERROR_CODES.VALIDATION, + status, + ) + } else if (data.message) { + throw new ResponseError(data.message, ERROR_CODES.ERROR, status) } + } - throw new ResponseError(error.message, ERROR_CODES.ERROR, Number(error.code)) - }) + throw new ResponseError(error.message, ERROR_CODES.ERROR, Number(error.code)) + }) request.end({ id: response.id.toString(), }) }, ) + +type GetFundsApiRequest = { + email: string | undefined + public_key: string +} + +type GetFundsApiResponse = { + id: number + object: 'faucet_transaction' + public_key: string + completed_at: number | null + started_at: number | null +} + +async function getFunds( + endpoint: string, + request: GetFundsApiRequest, +): Promise { + const response = await axios.post(endpoint, request) + + return response.data +} diff --git a/ironfish/src/rpc/routes/index.ts b/ironfish/src/rpc/routes/index.ts index 74d2441200..ab5322804a 100644 --- a/ironfish/src/rpc/routes/index.ts +++ b/ironfish/src/rpc/routes/index.ts @@ -14,3 +14,4 @@ export * from './faucet' export * from './worker' export * from './mempool' export * from './namespaces' +export * from './rpcContext' diff --git a/ironfish/src/rpc/routes/mempool/acceptTransaction.ts b/ironfish/src/rpc/routes/mempool/acceptTransaction.ts index ceae19cca9..ecbd7d6462 100644 --- a/ironfish/src/rpc/routes/mempool/acceptTransaction.ts +++ b/ironfish/src/rpc/routes/mempool/acceptTransaction.ts @@ -33,13 +33,13 @@ export const AcceptTransactionResponseSchema: yup.ObjectSchema( `${ApiNamespace.mempool}/acceptTransaction`, AcceptTransactionRequestSchema, - async (request, node): Promise => { - Assert.isInstanceOf(node, FullNode) + async (request, context): Promise => { + Assert.isInstanceOf(context, FullNode) const data = Buffer.from(request.data.transaction, 'hex') const transaction = new Transaction(data) - const verify = await node.chain.verifier.verifyNewTransaction(transaction) + const verify = await context.chain.verifier.verifyNewTransaction(transaction) if (!verify.valid) { request.end({ accepted: false, @@ -48,7 +48,7 @@ routes.register = routes.register( `${ApiNamespace.miner}/submitBlock`, SubmitBlockRequestSchema, - async (request, node): Promise => { - Assert.isInstanceOf(node, FullNode) + async (request, context): Promise => { + Assert.isInstanceOf(context, FullNode) - const result = await node.miningManager.submitBlockTemplate(request.data) + const result = await context.miningManager.submitBlockTemplate(request.data) request.end({ added: result === MINED_RESULT.SUCCESS, diff --git a/ironfish/src/rpc/routes/node/getLogStream.ts b/ironfish/src/rpc/routes/node/getLogStream.ts index 7a203884b4..72c84543a6 100644 --- a/ironfish/src/rpc/routes/node/getLogStream.ts +++ b/ironfish/src/rpc/routes/node/getLogStream.ts @@ -7,6 +7,7 @@ import { InterceptReporter } from '../../../logger' import { IJSON } from '../../../serde' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' // eslint-disable-next-line @typescript-eslint/ban-types export type GetLogStreamRequest = {} | undefined @@ -37,7 +38,9 @@ export const GetLogStreamResponseSchema: yup.ObjectSchema routes.register( `${ApiNamespace.node}/getLogStream`, GetLogStreamRequestSchema, - (request, node): void => { + (request, context): void => { + AssertHasRpcContext(request, context, 'logger') + const reporter = new InterceptReporter((logObj: ConsolaReporterLogObject): void => { request.stream({ level: String(logObj.level), @@ -48,10 +51,10 @@ routes.register( }) }) - node.logger.addReporter(reporter) + context.logger.addReporter(reporter) request.onClose.on(() => { - node.logger.removeReporter(reporter) + context.logger.removeReporter(reporter) }) }, ) diff --git a/ironfish/src/rpc/routes/node/stopNode.ts b/ironfish/src/rpc/routes/node/stopNode.ts index 3a13577023..e95c0229dc 100644 --- a/ironfish/src/rpc/routes/node/stopNode.ts +++ b/ironfish/src/rpc/routes/node/stopNode.ts @@ -4,6 +4,7 @@ import * as yup from 'yup' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' // eslint-disable-next-line @typescript-eslint/ban-types export type StopNodeRequest = undefined @@ -20,9 +21,11 @@ export const StopNodeResponseSchema: yup.MixedSchema = yup routes.register( `${ApiNamespace.node}/stopNode`, StopNodeRequestSchema, - async (request, node): Promise => { - node.logger.withTag('stopnode').info('Shutting down') + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'shutdown', 'logger') + + context.logger.withTag('stopnode').info('Shutting down') request.end() - await node.shutdown() + await context.shutdown() }, ) diff --git a/ironfish/src/rpc/routes/peer/getPeer.ts b/ironfish/src/rpc/routes/peer/getPeer.ts index dee0a450e1..156632dbb3 100644 --- a/ironfish/src/rpc/routes/peer/getPeer.ts +++ b/ironfish/src/rpc/routes/peer/getPeer.ts @@ -5,9 +5,9 @@ import * as yup from 'yup' import { Assert } from '../../../assert' import { PeerNetwork } from '../../../network' import { FullNode } from '../../../node' -import { ConnectionState, RpcPeerResponse, RpcPeerResponseSchema } from '../../types' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { ConnectionState, RpcPeerResponse, RpcPeerResponseSchema } from '../types' export type GetPeerRequest = { identity: string diff --git a/ironfish/src/rpc/routes/peer/getPeers.ts b/ironfish/src/rpc/routes/peer/getPeers.ts index 844e0ed7d4..1e35d423d0 100644 --- a/ironfish/src/rpc/routes/peer/getPeers.ts +++ b/ironfish/src/rpc/routes/peer/getPeers.ts @@ -5,9 +5,9 @@ import * as yup from 'yup' import { Assert } from '../../../assert' import { PeerNetwork } from '../../../network' import { FullNode } from '../../../node' -import { ConnectionState, RpcPeerResponse, RpcPeerResponseSchema } from '../../types' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { ConnectionState, RpcPeerResponse, RpcPeerResponseSchema } from '../types' export type GetPeersRequest = | undefined diff --git a/ironfish/src/rpc/routes/router.ts b/ironfish/src/rpc/routes/router.ts index c32dcb7c4b..e995ba17f4 100644 --- a/ironfish/src/rpc/routes/router.ts +++ b/ironfish/src/rpc/routes/router.ts @@ -2,21 +2,20 @@ * 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 { Assert } from '../../assert' -import { IronfishNode, YupSchema, YupSchemaResult, YupUtils } from '../../utils' +import { YupSchema, YupSchemaResult, YupUtils } from '../../utils' import { StrEnumUtils } from '../../utils/enums' import { ERROR_CODES } from '../adapters' import { ResponseError, ValidationError } from '../adapters/errors' import { RpcRequest } from '../request' import { RpcServer } from '../server' import { ApiNamespace } from './namespaces' +import { RpcContext } from './rpcContext' export const ALL_API_NAMESPACES = StrEnumUtils.getValues(ApiNamespace) -export type RequestContext = IronfishNode - export type RouteHandler = ( request: RpcRequest, - context: RequestContext, + context: RpcContext, ) => Promise | void export class RouteNotFoundError extends ResponseError { diff --git a/ironfish/src/rpc/routes/rpc/getStatus.ts b/ironfish/src/rpc/routes/rpc/getStatus.ts index 95c642a4bc..a3a6330cfc 100644 --- a/ironfish/src/rpc/routes/rpc/getStatus.ts +++ b/ironfish/src/rpc/routes/rpc/getStatus.ts @@ -8,6 +8,7 @@ import { RpcSocketAdapter } from '../../adapters/socketAdapter/socketAdapter' import { RpcServer } from '../../server' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' export type GetRpcStatusRequest = | undefined @@ -63,8 +64,10 @@ export const GetRpcStatusResponseSchema: yup.ObjectSchema routes.register( `${ApiNamespace.rpc}/getStatus`, GetRpcStatusRequestSchema, - async (request, node): Promise => { - const jobs = await getRpcStatus(node.rpc) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'rpc') + + const jobs = await getRpcStatus(context.rpc) if (!request.data?.stream) { request.end(jobs) @@ -74,7 +77,7 @@ routes.register( request.stream(jobs) while (!request.closed) { - const jobs = await getRpcStatus(node.rpc) + const jobs = await getRpcStatus(context.rpc) request.stream(jobs) await PromiseUtils.sleep(1000) } diff --git a/ironfish/src/rpc/routes/rpcContext.ts b/ironfish/src/rpc/routes/rpcContext.ts new file mode 100644 index 0000000000..b2881e43f4 --- /dev/null +++ b/ironfish/src/rpc/routes/rpcContext.ts @@ -0,0 +1,39 @@ +/* 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 { Assert } from '../../assert' +import { AssetsVerifier } from '../../assets' +import { Config, InternalStore } from '../../fileStores' +import { FileSystem } from '../../fileSystems' +import { Logger } from '../../logger' +import { Strategy } from '../../strategy' +import { Wallet } from '../../wallet' +import { WorkerPool } from '../../workerPool' +import { RpcRequest } from '../request' +import { RpcServer } from '../server' + +export type RpcContext = Partial<{ + config: Config + internal: InternalStore + files: FileSystem + wallet: Wallet + workerPool: WorkerPool + logger: Logger + rpc: RpcServer + assetsVerifier: AssetsVerifier + strategy: Strategy + shutdown: () => Promise +}> + +export function AssertHasRpcContext( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request: RpcRequest, + context: RpcContext, + ...keys: TKeys[] +): asserts context is Required> { + Assert.hasKeys( + context, + keys, + `Expected RPC context to have keys ${String(keys)} but has ${String(Object.keys(context))}`, + ) +} diff --git a/ironfish/src/rpc/types.ts b/ironfish/src/rpc/routes/types.ts similarity index 96% rename from ironfish/src/rpc/types.ts rename to ironfish/src/rpc/routes/types.ts index 64d9d78628..d2dc4ef347 100644 --- a/ironfish/src/rpc/types.ts +++ b/ironfish/src/rpc/routes/types.ts @@ -3,11 +3,11 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import * as yup from 'yup' -import { AssetVerification } from '../assets' -import { Connection } from '../network' -import { Features } from '../network/peers/peerFeatures' -import { BlockHeader } from '../primitives' -import { RpcTransaction, RpcTransactionSchema } from './routes' +import { AssetVerification } from '../../assets' +import { Connection } from '../../network' +import { Features } from '../../network/peers/peerFeatures' +import { BlockHeader } from '../../primitives' +import { RpcTransaction, RpcTransactionSchema } from './chain/types' export type RpcBurn = { assetId: string diff --git a/ironfish/src/rpc/routes/wallet/addTransaction.ts b/ironfish/src/rpc/routes/wallet/addTransaction.ts index 988534f717..57404d48a7 100644 --- a/ironfish/src/rpc/routes/wallet/addTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/addTransaction.ts @@ -8,6 +8,7 @@ import { AsyncUtils } from '../../../utils' import { ValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' export type AddTransactionRequest = { transaction: string @@ -38,19 +39,21 @@ export const AddTransactionResponseSchema: yup.ObjectSchema( `${ApiNamespace.wallet}/addTransaction`, AddTransactionRequestSchema, - async (request, node): Promise => { + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'strategy', 'wallet') + const data = Buffer.from(request.data.transaction, 'hex') const transaction = new Transaction(data) - const verify = Verifier.verifyCreatedTransaction(transaction, node.strategy.consensus) + const verify = Verifier.verifyCreatedTransaction(transaction, context.strategy.consensus) if (!verify.valid) { throw new ValidationError(`Invalid transaction, reason: ${String(verify.reason)}`, 400) } - await node.wallet.addPendingTransaction(transaction) + await context.wallet.addPendingTransaction(transaction) - const accounts = await AsyncUtils.filter(node.wallet.listAccounts(), (account) => + const accounts = await AsyncUtils.filter(context.wallet.listAccounts(), (account) => account.hasTransaction(transaction.hash()), ) @@ -62,7 +65,7 @@ routes.register( let accepted = false if (request.data.broadcast) { - const result = await node.wallet.broadcastTransaction(transaction) + const result = await context.wallet.broadcastTransaction(transaction) accepted = result.accepted } diff --git a/ironfish/src/rpc/routes/wallet/burnAsset.test.ts b/ironfish/src/rpc/routes/wallet/burnAsset.test.ts index d1038c78b1..a422dc04ab 100644 --- a/ironfish/src/rpc/routes/wallet/burnAsset.test.ts +++ b/ironfish/src/rpc/routes/wallet/burnAsset.test.ts @@ -104,7 +104,12 @@ describe('Route wallet/burnAsset', () => { verification: node.assetsVerifier.verify(asset.id()), createdTransactionHash: accountAsset.createdTransactionHash.toString('hex') ?? null, }, - transaction: await serializeRpcWalletTransaction(node, account, walletTransaction), + transaction: await serializeRpcWalletTransaction( + node.config, + node.wallet, + account, + walletTransaction, + ), id: asset.id().toString('hex'), assetId: asset.id().toString('hex'), name: asset.name().toString('hex'), diff --git a/ironfish/src/rpc/routes/wallet/burnAsset.ts b/ironfish/src/rpc/routes/wallet/burnAsset.ts index 43aaa52971..337f942bac 100644 --- a/ironfish/src/rpc/routes/wallet/burnAsset.ts +++ b/ironfish/src/rpc/routes/wallet/burnAsset.ts @@ -4,9 +4,10 @@ import * as yup from 'yup' import { Assert } from '../../../assert' import { CurrencyUtils, YupUtils } from '../../../utils' -import { RpcAsset, RpcAssetSchema, RpcBurn, RpcBurnSchema } from '../../types' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' +import { RpcAsset, RpcAssetSchema, RpcBurn, RpcBurnSchema } from '../types' import { RpcWalletTransaction, RpcWalletTransactionSchema } from './types' import { getAccount, serializeRpcWalletTransaction } from './utils' @@ -61,8 +62,10 @@ export const BurnAssetResponseSchema: yup.ObjectSchema = routes.register( `${ApiNamespace.wallet}/burnAsset`, BurnAssetRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet', 'assetsVerifier', 'config') + + const account = getAccount(context.wallet, request.data.account) const fee: bigint | undefined = request.data.fee ? CurrencyUtils.decode(request.data.fee) @@ -78,11 +81,11 @@ routes.register( const asset = await account.getAsset(assetId) Assert.isNotUndefined(asset) - const transaction = await node.wallet.burn( + const transaction = await context.wallet.burn( account, assetId, value, - request.data.expirationDelta ?? node.config.get('transactionExpirationDelta'), + request.data.expirationDelta ?? context.config.get('transactionExpirationDelta'), fee, feeRate, request.data.expiration, @@ -102,13 +105,18 @@ routes.register( nonce: asset.nonce, creator: asset.creator.toString('hex'), owner: asset.owner.toString('hex'), - verification: node.assetsVerifier.verify(asset.id), - status: await node.wallet.getAssetStatus(account, asset, { + verification: context.assetsVerifier.verify(asset.id), + status: await context.wallet.getAssetStatus(account, asset, { confirmations: request.data.confirmations, }), createdTransactionHash: asset.createdTransactionHash.toString('hex'), }, - transaction: await serializeRpcWalletTransaction(node, account, transactionValue), + transaction: await serializeRpcWalletTransaction( + context.config, + context.wallet, + account, + transactionValue, + ), id: burn.assetId.toString('hex'), assetId: burn.assetId.toString('hex'), hash: transaction.hash().toString('hex'), diff --git a/ironfish/src/rpc/routes/wallet/create.ts b/ironfish/src/rpc/routes/wallet/create.ts index 02016701ad..c1cf9d6de9 100644 --- a/ironfish/src/rpc/routes/wallet/create.ts +++ b/ironfish/src/rpc/routes/wallet/create.ts @@ -11,15 +11,18 @@ import { ERROR_CODES, ValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { CreateAccountRequestSchema, CreateAccountResponse } from '../wallet' routes.register( `${ApiNamespace.wallet}/create`, CreateAccountRequestSchema, - async (request, node): Promise => { + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + const name = request.data.name - if (node.wallet.accountExists(name)) { + if (context.wallet.accountExists(name)) { throw new ValidationError( `There is already an account with the name ${name}`, 400, @@ -27,14 +30,14 @@ routes.register( ) } - const account = await node.wallet.createAccount(name) - if (node.wallet.nodeClient) { - void node.wallet.scanTransactions() + const account = await context.wallet.createAccount(name) + if (context.wallet.nodeClient) { + void context.wallet.scanTransactions() } let isDefaultAccount = false - if (!node.wallet.hasDefaultAccount || request.data.default) { - await node.wallet.setDefaultAccount(name) + if (!context.wallet.hasDefaultAccount || request.data.default) { + await context.wallet.setDefaultAccount(name) isDefaultAccount = true } diff --git a/ironfish/src/rpc/routes/wallet/createAccount.ts b/ironfish/src/rpc/routes/wallet/createAccount.ts index a0b2dffc57..11d1387294 100644 --- a/ironfish/src/rpc/routes/wallet/createAccount.ts +++ b/ironfish/src/rpc/routes/wallet/createAccount.ts @@ -5,6 +5,7 @@ import * as yup from 'yup' import { ERROR_CODES, ValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' /** * Our endpoints follow the verbObject naming convention, where the verb is the * HTTP verb and the object is the object being acted upon. For example, @@ -41,10 +42,12 @@ export const CreateAccountResponseSchema: yup.ObjectSchema( `${ApiNamespace.wallet}/createAccount`, CreateAccountRequestSchema, - async (request, node): Promise => { + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + const name = request.data.name - if (node.wallet.accountExists(name)) { + if (context.wallet.accountExists(name)) { throw new ValidationError( `There is already an account with the name ${name}`, 400, @@ -52,14 +55,14 @@ routes.register( ) } - const account = await node.wallet.createAccount(name) - if (node.wallet.nodeClient) { - void node.wallet.scanTransactions() + const account = await context.wallet.createAccount(name) + if (context.wallet.nodeClient) { + void context.wallet.scanTransactions() } let isDefaultAccount = false - if (!node.wallet.hasDefaultAccount || request.data.default) { - await node.wallet.setDefaultAccount(name) + if (!context.wallet.hasDefaultAccount || request.data.default) { + await context.wallet.setDefaultAccount(name) isDefaultAccount = true } diff --git a/ironfish/src/rpc/routes/wallet/createTransaction.ts b/ironfish/src/rpc/routes/wallet/createTransaction.ts index a503b96c8e..0ddb8094ef 100644 --- a/ironfish/src/rpc/routes/wallet/createTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/createTransaction.ts @@ -16,6 +16,7 @@ import { NotEnoughFundsError } from '../../../wallet/errors' import { ERROR_CODES, ValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' export type CreateTransactionRequest = { @@ -106,6 +107,8 @@ routes.register => { + AssertHasRpcContext(request, node, 'wallet') + const account = getAccount(node.wallet, request.data.account) const params: Parameters[0] = { diff --git a/ironfish/src/rpc/routes/wallet/estimateFeeRates.ts b/ironfish/src/rpc/routes/wallet/estimateFeeRates.ts index 953258af7f..7aa84c4469 100644 --- a/ironfish/src/rpc/routes/wallet/estimateFeeRates.ts +++ b/ironfish/src/rpc/routes/wallet/estimateFeeRates.ts @@ -8,11 +8,14 @@ import { } from '../chain/estimateFeeRates' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' routes.register( `${ApiNamespace.wallet}/estimateFeeRates`, EstimateFeeRatesRequestSchema, async (request, node): Promise => { + AssertHasRpcContext(request, node, 'wallet') + Assert.isNotNull(node.wallet.nodeClient) const rates = await node.wallet.nodeClient.chain.estimateFeeRates() diff --git a/ironfish/src/rpc/routes/wallet/exportAccount.ts b/ironfish/src/rpc/routes/wallet/exportAccount.ts index 64100e3927..7febfb9536 100644 --- a/ironfish/src/rpc/routes/wallet/exportAccount.ts +++ b/ironfish/src/rpc/routes/wallet/exportAccount.ts @@ -7,6 +7,7 @@ import { encodeAccount } from '../../../wallet/account/encoder/account' import { AccountFormat } from '../../../wallet/account/encoder/encoder' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { RpcAccountImport } from './types' import { getAccount } from './utils' @@ -39,6 +40,8 @@ routes.register( `${ApiNamespace.wallet}/exportAccount`, ExportAccountRequestSchema, (request, node): void => { + AssertHasRpcContext(request, node, 'wallet') + const account = getAccount(node.wallet, request.data.account) const { id: _, ...accountInfo } = account.serialize() if (request.data.viewOnly) { diff --git a/ironfish/src/rpc/routes/wallet/getAccountNotesStream.ts b/ironfish/src/rpc/routes/wallet/getAccountNotesStream.ts index 3da2751c99..82a53f64c6 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountNotesStream.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountNotesStream.ts @@ -5,6 +5,7 @@ import * as yup from 'yup' import { BufferUtils, CurrencyUtils } from '../../../utils' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { RpcWalletNote, RpcWalletNoteSchema } from './types' import { getAccount } from './utils' @@ -25,8 +26,10 @@ export const GetAccountNotesStreamResponseSchema: yup.ObjectSchema( `${ApiNamespace.wallet}/getAccountNotesStream`, GetAccountNotesStreamRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const account = getAccount(context.wallet, request.data.account) for await (const transaction of account.getTransactionsByTime()) { if (request.closed) { diff --git a/ironfish/src/rpc/routes/wallet/getAccountTransaction.ts b/ironfish/src/rpc/routes/wallet/getAccountTransaction.ts index 64759f24ac..e9b93805ae 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountTransaction.ts @@ -4,6 +4,7 @@ import * as yup from 'yup' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { RpcWalletTransaction, RpcWalletTransactionSchema } from '../wallet/types' import { getAccount, getAccountDecryptedNotes, serializeRpcWalletTransaction } from './utils' @@ -45,6 +46,8 @@ routes.register => { + AssertHasRpcContext(request, node, 'wallet', 'config', 'workerPool') + const account = getAccount(node.wallet, request.data.account) const transactionHash = Buffer.from(request.data.hash, 'hex') @@ -59,7 +62,8 @@ routes.register => { + AssertHasRpcContext(request, node, 'wallet', 'config') + const account = getAccount(node.wallet, request.data.account) const headSequence = (await account.getHead())?.sequence ?? null @@ -70,7 +74,14 @@ routes.register, - node: IronfishNode, + config: Config, + wallet: Wallet, account: Account, transaction: TransactionValue, options: { @@ -115,10 +127,9 @@ const streamTransaction = async ( confirmations: number }, ): Promise => { - const wallet = node.wallet - const serializedTransaction = await serializeRpcWalletTransaction( - node, + config, + wallet, account, transaction, { diff --git a/ironfish/src/rpc/routes/wallet/getAccounts.ts b/ironfish/src/rpc/routes/wallet/getAccounts.ts index 356ae16d5e..234ea3e3ed 100644 --- a/ironfish/src/rpc/routes/wallet/getAccounts.ts +++ b/ironfish/src/rpc/routes/wallet/getAccounts.ts @@ -5,6 +5,7 @@ import * as yup from 'yup' import { Account } from '../../../wallet' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' // eslint-disable-next-line @typescript-eslint/ban-types export type GetAccountsRequest = { default?: boolean; displayName?: boolean } | undefined @@ -27,6 +28,8 @@ routes.register( `${ApiNamespace.wallet}/getAccounts`, GetAccountsRequestSchema, (request, node): void => { + AssertHasRpcContext(request, node, 'wallet') + let accounts: Account[] = [] if (request.data?.default) { diff --git a/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts b/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts index 732ba3bcb8..bc867332c8 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts @@ -5,6 +5,7 @@ import * as yup from 'yup' import { HeadValue } from '../../../wallet/walletdb/headValue' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' export type GetAccountStatusRequest = { account?: string } @@ -45,6 +46,8 @@ routes.register( GetAccountStatusRequestSchema, async (request, node): Promise => { const heads = new Map() + AssertHasRpcContext(request, node, 'wallet') + for await (const { accountId, head } of node.wallet.walletDb.loadHeads()) { heads.set(accountId, head) } diff --git a/ironfish/src/rpc/routes/wallet/getAsset.ts b/ironfish/src/rpc/routes/wallet/getAsset.ts index bd39b57aff..9d0d515966 100644 --- a/ironfish/src/rpc/routes/wallet/getAsset.ts +++ b/ironfish/src/rpc/routes/wallet/getAsset.ts @@ -5,9 +5,10 @@ import { ASSET_ID_LENGTH } from '@ironfish/rust-nodejs' import * as yup from 'yup' import { CurrencyUtils } from '../../../utils' import { NotFoundError, ValidationError } from '../../adapters' -import { RpcAsset, RpcAssetSchema } from '../../types' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' +import { RpcAsset, RpcAssetSchema } from '../types' import { getAccount } from './utils' export type GetWalletAssetRequest = { @@ -34,6 +35,8 @@ routes.register( `${ApiNamespace.wallet}/getAsset`, GetWalletAssetRequestSchema, async (request, node): Promise => { + AssertHasRpcContext(request, node, 'wallet', 'assetsVerifier') + const account = getAccount(node.wallet, request.data.account) const id = Buffer.from(request.data.id, 'hex') diff --git a/ironfish/src/rpc/routes/wallet/getAssets.ts b/ironfish/src/rpc/routes/wallet/getAssets.ts index 817aa0de18..4b9e178f96 100644 --- a/ironfish/src/rpc/routes/wallet/getAssets.ts +++ b/ironfish/src/rpc/routes/wallet/getAssets.ts @@ -3,9 +3,10 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import * as yup from 'yup' import { CurrencyUtils } from '../../../utils' -import { RpcAsset, RpcAssetSchema } from '../../types' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' +import { RpcAsset, RpcAssetSchema } from '../types' import { getAccount } from './utils' export type GetAssetsRequest = { @@ -30,6 +31,8 @@ routes.register( `${ApiNamespace.wallet}/getAssets`, GetAssetsRequestSchema, async (request, node): Promise => { + AssertHasRpcContext(request, node, 'wallet', 'assetsVerifier') + const account = getAccount(node.wallet, request.data.account) for await (const asset of account.getAssets()) { diff --git a/ironfish/src/rpc/routes/wallet/getBalance.ts b/ironfish/src/rpc/routes/wallet/getBalance.ts index 404f84cb81..4ee4f2afe4 100644 --- a/ironfish/src/rpc/routes/wallet/getBalance.ts +++ b/ironfish/src/rpc/routes/wallet/getBalance.ts @@ -6,6 +6,7 @@ import * as yup from 'yup' import { AssetVerification } from '../../../assets' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' export type GetBalanceRequest = @@ -65,6 +66,8 @@ routes.register( `${ApiNamespace.wallet}/getBalance`, GetBalanceRequestSchema, async (request, node): Promise => { + AssertHasRpcContext(request, node, 'wallet', 'config', 'assetsVerifier') + const confirmations = request.data?.confirmations ?? node.config.get('confirmations') const account = getAccount(node.wallet, request.data?.account) diff --git a/ironfish/src/rpc/routes/wallet/getBalances.ts b/ironfish/src/rpc/routes/wallet/getBalances.ts index 43db63f12a..e5c28bf185 100644 --- a/ironfish/src/rpc/routes/wallet/getBalances.ts +++ b/ironfish/src/rpc/routes/wallet/getBalances.ts @@ -6,6 +6,7 @@ import { AssetVerification } from '../../../assets' import { CurrencyUtils } from '../../../utils' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' export interface GetBalancesRequest { @@ -87,11 +88,16 @@ export const GetBalancesResponseSchema: yup.ObjectSchema = routes.register( `${ApiNamespace.wallet}/getBalances`, GetBalancesRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet', 'assetsVerifier') + + const account = getAccount(context.wallet, request.data.account) const balances = [] - for await (const balance of node.wallet.getBalances(account, request.data.confirmations)) { + for await (const balance of context.wallet.getBalances( + account, + request.data.confirmations, + )) { if (request.closed) { return } @@ -103,7 +109,7 @@ routes.register( assetName: asset?.name.toString('hex') ?? '', assetCreator: asset?.creator.toString('hex') ?? '', assetOwner: asset?.owner.toString('hex') ?? '', - assetVerification: node.assetsVerifier.verify(balance.assetId), + assetVerification: context.assetsVerifier.verify(balance.assetId), blockHash: balance.blockHash?.toString('hex') ?? null, confirmed: CurrencyUtils.encode(balance.confirmed), sequence: balance.sequence, diff --git a/ironfish/src/rpc/routes/wallet/getDefaultAccount.ts b/ironfish/src/rpc/routes/wallet/getDefaultAccount.ts index 28c88831a1..4d730d2779 100644 --- a/ironfish/src/rpc/routes/wallet/getDefaultAccount.ts +++ b/ironfish/src/rpc/routes/wallet/getDefaultAccount.ts @@ -4,6 +4,7 @@ import * as yup from 'yup' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' // eslint-disable-next-line @typescript-eslint/ban-types export type GetDefaultAccountRequest = {} | undefined @@ -29,6 +30,7 @@ routes.register { + AssertHasRpcContext(request, node, 'wallet') const account = node.wallet.getDefaultAccount() request.end({ account: account ? { name: account.name } : null }) }, diff --git a/ironfish/src/rpc/routes/wallet/getNotes.ts b/ironfish/src/rpc/routes/wallet/getNotes.ts index fdff580e77..b5e70f3dce 100644 --- a/ironfish/src/rpc/routes/wallet/getNotes.ts +++ b/ironfish/src/rpc/routes/wallet/getNotes.ts @@ -4,6 +4,7 @@ import * as yup from 'yup' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { RpcWalletNote, RpcWalletNoteSchema } from './types' import { getAccount, serializeRpcWalletNote } from './utils' @@ -72,8 +73,10 @@ export const GetNotesResponseSchema: yup.ObjectSchema = yup routes.register( `${ApiNamespace.wallet}/getNotes`, GetNotesRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const account = getAccount(context.wallet, request.data.account) const pageSize = request.data.pageSize ?? DEFAULT_PAGE_SIZE const pageCursor = request.data.pageCursor @@ -84,7 +87,7 @@ routes.register( for await (const decryptedNote of account.getNotes(keyRange)) { if (notes.length === pageSize) { - nextPageCursor = node.wallet.walletDb.decryptedNotes.keyEncoding.serialize([ + nextPageCursor = context.wallet.walletDb.decryptedNotes.keyEncoding.serialize([ account.prefix, decryptedNote.hash, ]) diff --git a/ironfish/src/rpc/routes/wallet/getPublicKey.ts b/ironfish/src/rpc/routes/wallet/getPublicKey.ts index 0d7e396614..17b68bfa77 100644 --- a/ironfish/src/rpc/routes/wallet/getPublicKey.ts +++ b/ironfish/src/rpc/routes/wallet/getPublicKey.ts @@ -4,6 +4,7 @@ import * as yup from 'yup' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' export type GetPublicKeyRequest = { account?: string } @@ -26,6 +27,7 @@ routes.register( `${ApiNamespace.wallet}/getPublicKey`, GetPublicKeyRequestSchema, (request, node): void => { + AssertHasRpcContext(request, node, 'wallet') const account = getAccount(node.wallet, request.data.account) request.end({ diff --git a/ironfish/src/rpc/routes/wallet/importAccount.ts b/ironfish/src/rpc/routes/wallet/importAccount.ts index 0ac36136e8..fb78519357 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.ts @@ -6,6 +6,7 @@ import * as yup from 'yup' import { decodeAccount } from '../../../wallet/account/encoder/account' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { RpcAccountImport } from './types' import { deserializeRpcAccountImport } from './utils' @@ -40,7 +41,9 @@ export const ImportAccountResponseSchema: yup.ObjectSchema = yup routes.register( `${ApiNamespace.wallet}/importAccount`, ImportAccountRequestSchema, - async (request, node): Promise => { + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + let accountImport = null if (typeof request.data.account === 'string') { accountImport = decodeAccount(request.data.account, { @@ -50,22 +53,22 @@ routes.register( accountImport = deserializeRpcAccountImport(request.data.account) } - const account = await node.wallet.importAccount({ + const account = await context.wallet.importAccount({ id: uuid(), ...accountImport, }) if (request.data.rescan) { - if (node.wallet.nodeClient) { - void node.wallet.scanTransactions(undefined, true) + if (context.wallet.nodeClient) { + void context.wallet.scanTransactions(undefined, true) } } else { - await node.wallet.skipRescan(account) + await context.wallet.skipRescan(account) } let isDefaultAccount = false - if (!node.wallet.hasDefaultAccount) { - await node.wallet.setDefaultAccount(account.name) + if (!context.wallet.hasDefaultAccount) { + await context.wallet.setDefaultAccount(account.name) isDefaultAccount = true } diff --git a/ironfish/src/rpc/routes/wallet/mintAsset.test.ts b/ironfish/src/rpc/routes/wallet/mintAsset.test.ts index 02f57f595b..9ff3209763 100644 --- a/ironfish/src/rpc/routes/wallet/mintAsset.test.ts +++ b/ironfish/src/rpc/routes/wallet/mintAsset.test.ts @@ -133,7 +133,12 @@ describe('Route wallet/mintAsset', () => { }), verification: node.assetsVerifier.verify(asset.id()), }, - transaction: await serializeRpcWalletTransaction(node, account, walletTransaction), + transaction: await serializeRpcWalletTransaction( + node.config, + node.wallet, + account, + walletTransaction, + ), id: asset.id().toString('hex'), creator: asset.creator().toString('hex'), assetId: asset.id().toString('hex'), diff --git a/ironfish/src/rpc/routes/wallet/mintAsset.ts b/ironfish/src/rpc/routes/wallet/mintAsset.ts index 3ac8f5720c..11a03b0894 100644 --- a/ironfish/src/rpc/routes/wallet/mintAsset.ts +++ b/ironfish/src/rpc/routes/wallet/mintAsset.ts @@ -11,9 +11,10 @@ import { Assert } from '../../../assert' import { CurrencyUtils, YupUtils } from '../../../utils' import { MintAssetOptions } from '../../../wallet/interfaces/mintAssetOptions' import { ValidationError } from '../../adapters' -import { RpcAsset, RpcAssetSchema, RpcMint, RpcMintSchema } from '../../types' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' +import { RpcAsset, RpcAssetSchema, RpcMint, RpcMintSchema } from '../types' import { RpcWalletTransaction, RpcWalletTransactionSchema } from './types' import { getAccount, serializeRpcWalletTransaction } from './utils' @@ -69,8 +70,11 @@ export const MintAssetResponseSchema: yup.ObjectSchema = routes.register( `${ApiNamespace.wallet}/mintAsset`, MintAssetRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'config', 'assetsVerifier', 'wallet') + + Assert.isNotUndefined(context.wallet) + const account = getAccount(context.wallet, request.data.account) const fee: bigint | undefined = request.data.fee ? CurrencyUtils.decode(request.data.fee) @@ -82,7 +86,7 @@ routes.register( const value = CurrencyUtils.decode(request.data.value) const expirationDelta = - request.data.expirationDelta ?? node.config.get('transactionExpirationDelta') + request.data.expirationDelta ?? context.config.get('transactionExpirationDelta') if ( request.data.transferOwnershipTo && @@ -121,7 +125,7 @@ routes.register( } } - const transaction = await node.wallet.mint(account, options) + const transaction = await context.wallet.mint(account, options) Assert.isEqual(transaction.mints.length, 1) const mint = transaction.mints[0] @@ -139,13 +143,18 @@ routes.register( nonce: asset.nonce, creator: asset.creator.toString('hex'), owner: asset.owner.toString('hex'), - verification: node.assetsVerifier.verify(mint.asset.id()), - status: await node.wallet.getAssetStatus(account, asset, { + verification: context.assetsVerifier.verify(mint.asset.id()), + status: await context.wallet.getAssetStatus(account, asset, { confirmations: request.data.confirmations, }), createdTransactionHash: asset.createdTransactionHash.toString('hex'), }, - transaction: await serializeRpcWalletTransaction(node, account, transactionValue), + transaction: await serializeRpcWalletTransaction( + context.config, + context.wallet, + account, + transactionValue, + ), assetId: asset.id.toString('hex'), hash: transaction.hash().toString('hex'), name: asset.name.toString('hex'), diff --git a/ironfish/src/rpc/routes/wallet/postTransaction.ts b/ironfish/src/rpc/routes/wallet/postTransaction.ts index bbd2f4d66d..b8d9007385 100644 --- a/ironfish/src/rpc/routes/wallet/postTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/postTransaction.ts @@ -5,6 +5,7 @@ import * as yup from 'yup' import { RawTransactionSerde } from '../../../primitives/rawTransaction' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' export type PostTransactionRequest = { @@ -40,13 +41,15 @@ export const PostTransactionResponseSchema: yup.ObjectSchema( `${ApiNamespace.wallet}/postTransaction`, PostTransactionRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const account = getAccount(context.wallet, request.data.account) const bytes = Buffer.from(request.data.transaction, 'hex') const raw = RawTransactionSerde.deserialize(bytes) - const { accepted, broadcasted, transaction } = await node.wallet.post({ + const { accepted, broadcasted, transaction } = await context.wallet.post({ transaction: raw, account, broadcast: request.data.broadcast, diff --git a/ironfish/src/rpc/routes/wallet/remove.ts b/ironfish/src/rpc/routes/wallet/remove.ts index 9426407449..1fd1e5afd9 100644 --- a/ironfish/src/rpc/routes/wallet/remove.ts +++ b/ironfish/src/rpc/routes/wallet/remove.ts @@ -10,17 +10,20 @@ import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { RemoveAccountRequestSchema, RemoveAccountResponse } from './removeAccount' import { getAccount } from './utils' routes.register( `${ApiNamespace.wallet}/remove`, RemoveAccountRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const account = getAccount(context.wallet, request.data.account) if (!request.data.confirm) { - if (!(await node.wallet.isAccountUpToDate(account))) { + if (!(await context.wallet.isAccountUpToDate(account))) { request.end({ needsConfirm: true }) return } @@ -34,9 +37,9 @@ routes.register( } } } - await node.wallet.removeAccountByName(account.name) + await context.wallet.removeAccountByName(account.name) if (request.data.wait) { - await node.wallet.forceCleanupDeletedAccounts() + await context.wallet.forceCleanupDeletedAccounts() } request.end({}) }, diff --git a/ironfish/src/rpc/routes/wallet/removeAccount.ts b/ironfish/src/rpc/routes/wallet/removeAccount.ts index cefbe57ac9..9c8d4eeebb 100644 --- a/ironfish/src/rpc/routes/wallet/removeAccount.ts +++ b/ironfish/src/rpc/routes/wallet/removeAccount.ts @@ -4,6 +4,7 @@ import * as yup from 'yup' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' export type RemoveAccountRequest = { account: string; confirm?: boolean; wait?: boolean } @@ -26,11 +27,13 @@ export const RemoveAccountResponseSchema: yup.ObjectSchema( `${ApiNamespace.wallet}/removeAccount`, RemoveAccountRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const account = getAccount(context.wallet, request.data.account) if (!request.data.confirm) { - if (!(await node.wallet.isAccountUpToDate(account))) { + if (!(await context.wallet.isAccountUpToDate(account))) { request.end({ needsConfirm: true }) return } @@ -44,9 +47,9 @@ routes.register( } } } - await node.wallet.removeAccountByName(account.name) + await context.wallet.removeAccountByName(account.name) if (request.data.wait) { - await node.wallet.forceCleanupDeletedAccounts() + await context.wallet.forceCleanupDeletedAccounts() } request.end({}) }, diff --git a/ironfish/src/rpc/routes/wallet/rename.ts b/ironfish/src/rpc/routes/wallet/rename.ts index 53d3b5ced6..760c00124f 100644 --- a/ironfish/src/rpc/routes/wallet/rename.ts +++ b/ironfish/src/rpc/routes/wallet/rename.ts @@ -10,14 +10,17 @@ import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { RenameAccountRequestSchema, RenameAccountResponse } from '../wallet' import { getAccount } from './utils' routes.register( `${ApiNamespace.wallet}/rename`, RenameAccountRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const account = getAccount(context.wallet, request.data.account) await account.setName(request.data.newName) request.end() }, diff --git a/ironfish/src/rpc/routes/wallet/renameAccount.ts b/ironfish/src/rpc/routes/wallet/renameAccount.ts index 3c22e25564..5268ea4430 100644 --- a/ironfish/src/rpc/routes/wallet/renameAccount.ts +++ b/ironfish/src/rpc/routes/wallet/renameAccount.ts @@ -4,6 +4,7 @@ import * as yup from 'yup' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' export type RenameAccountRequest = { account: string; newName: string } @@ -23,8 +24,10 @@ export const RenameAccountResponseSchema: yup.MixedSchema routes.register( `${ApiNamespace.wallet}/renameAccount`, RenameAccountRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const account = getAccount(context.wallet, request.data.account) await account.setName(request.data.newName) request.end() }, diff --git a/ironfish/src/rpc/routes/wallet/rescanAccount.ts b/ironfish/src/rpc/routes/wallet/rescanAccount.ts index bbf4b44a6e..6de98b4a77 100644 --- a/ironfish/src/rpc/routes/wallet/rescanAccount.ts +++ b/ironfish/src/rpc/routes/wallet/rescanAccount.ts @@ -6,6 +6,7 @@ import { GENESIS_BLOCK_SEQUENCE } from '../../../primitives' import { ValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' export type RescanAccountRequest = { follow?: boolean; from?: number } export type RescanAccountResponse = { sequence: number; startedAt: number; endSequence: number } @@ -28,23 +29,25 @@ export const RescanAccountResponseSchema: yup.ObjectSchema( `${ApiNamespace.wallet}/rescanAccount`, RescanAccountRequestSchema, - async (request, node): Promise => { - let scan = node.wallet.scan + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet', 'logger') + + let scan = context.wallet.scan if (scan && !request.data.follow) { throw new ValidationError(`A transaction rescan is already running`) } if (!scan) { - if (node.wallet.updateHeadState) { - await node.wallet.updateHeadState.abort() + if (context.wallet.updateHeadState) { + await context.wallet.updateHeadState.abort() } - await node.wallet.reset() + await context.wallet.reset() let fromHash = undefined if (request.data.from && request.data.from > GENESIS_BLOCK_SEQUENCE) { - const response = await node.wallet.chainGetBlock({ sequence: request.data.from }) + const response = await context.wallet.chainGetBlock({ sequence: request.data.from }) if (response === null) { throw new ValidationError( @@ -54,7 +57,7 @@ routes.register( fromHash = Buffer.from(response.block.hash, 'hex') - for (const account of node.wallet.listAccounts()) { + for (const account of context.wallet.listAccounts()) { await account.updateHead({ hash: Buffer.from(response.block.previousBlockHash, 'hex'), sequence: response.block.sequence - 1, @@ -62,11 +65,11 @@ routes.register( } } - void node.wallet.scanTransactions(fromHash) - scan = node.wallet.scan + void context.wallet.scanTransactions(fromHash) + scan = context.wallet.scan if (!scan) { - node.logger.warn(`Attempted to start accounts scan but one did not start.`) + context.logger.warn(`Attempted to start accounts scan but one did not start.`) } } diff --git a/ironfish/src/rpc/routes/wallet/sendTransaction.ts b/ironfish/src/rpc/routes/wallet/sendTransaction.ts index 253b59affb..b611c6fde4 100644 --- a/ironfish/src/rpc/routes/wallet/sendTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/sendTransaction.ts @@ -11,6 +11,7 @@ import { NotEnoughFundsError } from '../../../wallet/errors' import { ERROR_CODES, ValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' export type SendTransactionRequest = { @@ -68,11 +69,13 @@ export const SendTransactionResponseSchema: yup.ObjectSchema( `${ApiNamespace.wallet}/sendTransaction`, SendTransactionRequestSchema, - async (request, node): Promise => { - Assert.isNotNull(node.wallet.nodeClient) - const account = getAccount(node.wallet, request.data.account) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') - const status = await node.wallet.nodeClient.node.getStatus() + Assert.isNotNull(context.wallet.nodeClient) + const account = getAccount(context.wallet, request.data.account) + + const status = await context.wallet.nodeClient.node.getStatus() if (!status.content.blockchain.synced) { throw new ValidationError( @@ -113,7 +116,7 @@ routes.register( // Check that the node has enough balance for (const [assetId, sum] of totalByAssetId) { - const balance = await node.wallet.getBalance(account, assetId, { + const balance = await context.wallet.getBalance(account, assetId, { confirmations: request.data.confirmations ?? undefined, }) @@ -127,7 +130,7 @@ routes.register( } try { - const transaction = await node.wallet.send(params) + const transaction = await context.wallet.send(params) request.end({ account: account.name, diff --git a/ironfish/src/rpc/routes/wallet/types.ts b/ironfish/src/rpc/routes/wallet/types.ts index 76b77accf1..fce9c8033e 100644 --- a/ironfish/src/rpc/routes/wallet/types.ts +++ b/ironfish/src/rpc/routes/wallet/types.ts @@ -4,8 +4,8 @@ import * as yup from 'yup' import { TransactionStatus, TransactionType } from '../../../wallet' -import { RpcBurn, RpcBurnSchema, RpcMint, RpcMintSchema } from '../../types' import { RpcSpend, RpcSpendSchema } from '../chain' +import { RpcBurn, RpcBurnSchema, RpcMint, RpcMintSchema } from '../types' export type RpcAccountAssetBalanceDelta = { assetId: string diff --git a/ironfish/src/rpc/routes/wallet/use.ts b/ironfish/src/rpc/routes/wallet/use.ts index beeb395abf..fca1113f23 100644 --- a/ironfish/src/rpc/routes/wallet/use.ts +++ b/ironfish/src/rpc/routes/wallet/use.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { UseAccountRequestSchema, UseAccountResponse } from './useAccount' import { getAccount } from './utils' @@ -15,9 +16,11 @@ import { getAccount } from './utils' routes.register( `${ApiNamespace.wallet}/use`, UseAccountRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) - await node.wallet.setDefaultAccount(account.name) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const account = getAccount(context.wallet, request.data.account) + await context.wallet.setDefaultAccount(account.name) request.end() }, ) diff --git a/ironfish/src/rpc/routes/wallet/useAccount.ts b/ironfish/src/rpc/routes/wallet/useAccount.ts index 040facb97c..7413869967 100644 --- a/ironfish/src/rpc/routes/wallet/useAccount.ts +++ b/ironfish/src/rpc/routes/wallet/useAccount.ts @@ -4,6 +4,7 @@ import * as yup from 'yup' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' export type UseAccountRequest = { account: string } @@ -22,9 +23,11 @@ export const UseAccountResponseSchema: yup.MixedSchema = yup routes.register( `${ApiNamespace.wallet}/useAccount`, UseAccountRequestSchema, - async (request, node): Promise => { - const account = getAccount(node.wallet, request.data.account) - await node.wallet.setDefaultAccount(account.name) + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const account = getAccount(context.wallet, request.data.account) + await context.wallet.setDefaultAccount(account.name) request.end() }, ) diff --git a/ironfish/src/rpc/routes/wallet/utils.ts b/ironfish/src/rpc/routes/wallet/utils.ts index 66f6f6a31f..8693970fb6 100644 --- a/ironfish/src/rpc/routes/wallet/utils.ts +++ b/ironfish/src/rpc/routes/wallet/utils.ts @@ -1,8 +1,9 @@ /* 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 { Note } from '../../../primitives' -import { BufferUtils, CurrencyUtils, IronfishNode } from '../../../utils' +import { BufferUtils, CurrencyUtils } from '../../../utils' import { Account, Wallet } from '../../../wallet' import { AccountImport } from '../../../wallet/walletdb/accountValue' import { AssetValue } from '../../../wallet/walletdb/assetValue' @@ -38,7 +39,8 @@ export function getAccount(wallet: Wallet, name?: string): Account { } export async function serializeRpcWalletTransaction( - node: IronfishNode, + config: Config, + wallet: Wallet, account: Account, transaction: TransactionValue, options?: { @@ -47,9 +49,9 @@ export async function serializeRpcWalletTransaction( }, ): Promise { const assetBalanceDeltas = await getAssetBalanceDeltas(account, transaction) - const type = await node.wallet.getTransactionType(account, transaction) - const confirmations = options?.confirmations ?? node.config.get('confirmations') - const status = await node.wallet.getTransactionStatus(account, transaction, { + const type = await wallet.getTransactionType(account, transaction) + const confirmations = options?.confirmations ?? config.get('confirmations') + const status = await wallet.getTransactionStatus(account, transaction, { confirmations, }) diff --git a/ironfish/src/rpc/routes/worker/getStatus.ts b/ironfish/src/rpc/routes/worker/getStatus.ts index 0ea706774a..e868e0fb9a 100644 --- a/ironfish/src/rpc/routes/worker/getStatus.ts +++ b/ironfish/src/rpc/routes/worker/getStatus.ts @@ -7,6 +7,7 @@ import { WorkerPool } from '../../../workerPool' import { WorkerMessageType } from '../../../workerPool/tasks/workerMessage' import { ApiNamespace } from '../namespaces' import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' export type GetWorkersStatusRequest = | undefined @@ -66,8 +67,10 @@ export const GetWorkersStatusResponseSchema: yup.ObjectSchema( `${ApiNamespace.worker}/getStatus`, GetWorkersStatusRequestSchema, - (request, node): void => { - const jobs = getWorkersStatus(node.workerPool) + (request, context): void => { + AssertHasRpcContext(request, context, 'workerPool') + + const jobs = getWorkersStatus(context.workerPool) if (!request.data?.stream) { request.end(jobs) @@ -77,7 +80,7 @@ routes.register( request.stream(jobs) const interval = setInterval(() => { - const jobs = getWorkersStatus(node.workerPool) + const jobs = getWorkersStatus(context.workerPool) request.stream(jobs) }, 1000) diff --git a/ironfish/src/rpc/server.test.ts b/ironfish/src/rpc/server.test.ts new file mode 100644 index 0000000000..5f146d97b0 --- /dev/null +++ b/ironfish/src/rpc/server.test.ts @@ -0,0 +1,30 @@ +/* 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 { createNodeTest } from '../testUtilities' + +describe('RpcServer', () => { + const nodeTest = createNodeTest() + + it('should authenticate', () => { + nodeTest.node.rpc.internal.set('rpcAuthToken', 'ironfish') + expect(nodeTest.node.rpc.authenticate('ironfish')).toBe(true) + expect(nodeTest.node.rpc.authenticate('')).toBe(false) + expect(nodeTest.node.rpc.authenticate('foobar')).toBe(false) + }) + + it('should generate auth when started', async () => { + // token should be empty + expect(nodeTest.node.rpc.internal.get('rpcAuthToken')).toEqual('') + expect(nodeTest.node.rpc.authenticate('')).toBe(false) + + // token should be generated + await nodeTest.node.rpc.start() + expect(nodeTest.node.rpc.internal.get('rpcAuthToken')).not.toEqual('') + expect(nodeTest.node.rpc.authenticate('')).toBe(false) + + // should be able to auth with generated token + const token = nodeTest.node.rpc.internal.get('rpcAuthToken') + expect(nodeTest.node.rpc.authenticate(token)).toBe(true) + }) +}) diff --git a/ironfish/src/rpc/server.ts b/ironfish/src/rpc/server.ts index 1bd4dc9218..024a632f9d 100644 --- a/ironfish/src/rpc/server.ts +++ b/ironfish/src/rpc/server.ts @@ -2,28 +2,42 @@ * 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 { randomBytes, timingSafeEqual } from 'crypto' +import { Assert } from '../assert' import { InternalStore } from '../fileStores' import { createRootLogger, Logger } from '../logger' import { IRpcAdapter } from './adapters' -import { ApiNamespace, RequestContext, Router, routes } from './routes' +import { ApiNamespace, Router, routes, RpcContext } from './routes' + +const AUTH_MAX_LENGTH = 256 export class RpcServer { readonly internal: InternalStore - readonly context: RequestContext + readonly context: RpcContext readonly adapters: IRpcAdapter[] = [] private _isRunning = false private _startPromise: Promise | null = null - logger: Logger + private logger: Logger + private authTokenBuffer = Buffer.alloc(0) constructor( - context: RequestContext, + context: RpcContext, internal: InternalStore, logger: Logger = createRootLogger(), ) { this.context = context this.internal = internal this.logger = logger.withTag('rpcserver') + + this.loadAuth(this.internal.get('rpcAuthToken')) + + this.internal.onConfigChange.on((key, value) => { + if (key === 'rpcAuthToken') { + Assert.isString(value) + this.loadAuth(value) + } + }) } get isRunning(): boolean { @@ -41,6 +55,8 @@ export class RpcServer { return } + await this.generateAuth() + const promises = this.adapters.map>((a) => a.start()) this._startPromise = Promise.all(promises) this._isRunning = true @@ -83,17 +99,38 @@ export class RpcServer { /** Authenticate the RPC request */ authenticate(requestAuthToken: string | undefined | null): boolean { if (!requestAuthToken) { - this.logger.debug(`Missing Auth token in RPC request.`) return false } + if (!this.authTokenBuffer.byteLength) { + return false + } + + if (requestAuthToken.length > AUTH_MAX_LENGTH) { + return false + } + + const requestAuthBuffer = Buffer.alloc(AUTH_MAX_LENGTH) + requestAuthBuffer.write(requestAuthToken) + return timingSafeEqual(requestAuthBuffer, this.authTokenBuffer) + } + + private async generateAuth(): Promise { const rpcAuthToken = this.internal.get('rpcAuthToken') if (!rpcAuthToken) { - this.logger.debug(`Missing RPC Auth token in internal.json config.`) - return false + this.logger.debug( + `Missing RPC Auth token in internal.json config. Automatically generating auth token.`, + ) + const newPassword = randomBytes(AUTH_MAX_LENGTH / 2).toString('hex') + this.internal.set('rpcAuthToken', newPassword) + await this.internal.save() } + } - return requestAuthToken === rpcAuthToken + private loadAuth(token: string): void { + const buffer = Buffer.alloc(AUTH_MAX_LENGTH) + buffer.write(token) + this.authTokenBuffer = buffer } } diff --git a/ironfish/src/sdk.ts b/ironfish/src/sdk.ts index e5d16f069c..dd1dd82051 100644 --- a/ironfish/src/sdk.ts +++ b/ironfish/src/sdk.ts @@ -34,11 +34,9 @@ import { RpcIpcClient } from './rpc/clients/ipcClient' import { RpcMemoryClient } from './rpc/clients/memoryClient' import { RpcTcpClient } from './rpc/clients/tcpClient' import { RpcTlsClient } from './rpc/clients/tlsClient' -import { ApiNamespace } from './rpc/routes/namespaces' import { ALL_API_NAMESPACES } from './rpc/routes/router' import { Strategy } from './strategy' import { NodeUtils } from './utils' -import { WalletNode } from './walletNode' export class IronfishSdk { pkg: Package @@ -228,7 +226,6 @@ export class IronfishSdk { this.fileSystem, this.config.get('tlsKeyPath'), this.config.get('tlsCertPath'), - node, this.logger, ALL_API_NAMESPACES, ), @@ -272,103 +269,6 @@ export class IronfishSdk { return clientMemory } - async walletNode( - options: { connectNodeClient: boolean } = { connectNodeClient: true }, - ): Promise { - let nodeClient: RpcSocketClient | null = null - - if (options.connectNodeClient) { - if (this.config.get('walletNodeTcpEnabled')) { - if (this.config.get('walletNodeTlsEnabled')) { - nodeClient = new RpcTlsClient( - this.config.get('walletNodeTcpHost'), - this.config.get('walletNodeTcpPort'), - this.logger, - this.config.get('walletNodeRpcAuthToken'), - ) - } else { - nodeClient = new RpcTcpClient( - this.config.get('walletNodeTcpHost'), - this.config.get('walletNodeTcpPort'), - this.logger, - ) - } - } else if (this.config.get('walletNodeIpcEnabled')) { - nodeClient = new RpcIpcClient(this.config.get('walletNodeIpcPath'), this.logger) - } else { - throw new Error(`Cannot start the wallet: no node connection configuration specified. - -Use 'ironfish config:set' to connect to a node via TCP, TLS, or IPC.\n`) - } - } - - const node = await WalletNode.init({ - pkg: this.pkg, - config: this.config, - internal: this.internal, - files: this.fileSystem, - logger: this.logger, - metrics: this.metrics, - strategyClass: this.strategyClass, - dataDir: this.dataDir, - nodeClient, - }) - - const namespaces = [ - ApiNamespace.config, - ApiNamespace.faucet, - ApiNamespace.rpc, - ApiNamespace.wallet, - ApiNamespace.worker, - ApiNamespace.node, - ] - - if (this.config.get('enableRpcIpc')) { - await node.rpc.mount( - new RpcIpcAdapter(this.config.get('ipcPath'), this.logger, namespaces), - ) - } - - if (this.config.get('enableRpcHttp')) { - await node.rpc.mount( - new RpcHttpAdapter( - this.config.get('rpcHttpHost'), - this.config.get('rpcHttpPort'), - this.logger, - namespaces, - ), - ) - } - - if (this.config.get('enableRpcTcp')) { - if (this.config.get('enableRpcTls')) { - await node.rpc.mount( - new RpcTlsAdapter( - this.config.get('rpcTcpHost'), - this.config.get('rpcTcpPort'), - this.fileSystem, - this.config.get('tlsKeyPath'), - this.config.get('tlsCertPath'), - node, - this.logger, - namespaces, - ), - ) - } else { - await node.rpc.mount( - new RpcTcpAdapter( - this.config.get('rpcTcpHost'), - this.config.get('rpcTcpPort'), - this.logger, - namespaces, - ), - ) - } - } - - return node - } - getPrivateIdentity(): PrivateIdentity | undefined { const networkIdentity = this.internal.get('networkIdentity') if ( diff --git a/ironfish/src/storage/database/encoding.ts b/ironfish/src/storage/database/encoding.ts index d2de3ebdde..9c60d18501 100644 --- a/ironfish/src/storage/database/encoding.ts +++ b/ironfish/src/storage/database/encoding.ts @@ -162,7 +162,7 @@ export class NullableStringEncoding implements IDatabaseEncoding export class ArrayEncoding extends JsonEncoding {} -export class BigIntLEEncoding implements IDatabaseEncoding { +export class BigIntLEEncoding implements IDatabaseEncoding { serialize(value: bigint): Buffer { return BigIntUtils.toBytesLE(value) } @@ -172,7 +172,7 @@ export class BigIntLEEncoding implements IDatabaseEncoding { } } -export class BigU64BEEncoding implements IDatabaseEncoding { +export class BigU64BEEncoding implements IDatabaseEncoding { serialize(value: bigint): Buffer { const buffer = bufio.write(8) buffer.writeBigU64BE(value) diff --git a/ironfish/src/storage/database/errors.ts b/ironfish/src/storage/database/errors.ts index f2934b6b9f..f167cb2271 100644 --- a/ironfish/src/storage/database/errors.ts +++ b/ironfish/src/storage/database/errors.ts @@ -17,7 +17,7 @@ export class DuplicateKeyError extends Error { export class DatabaseOpenError extends Error { name = this.constructor.name - constructor(message?: string, error?: Error) { + constructor(message?: string, error?: { message: string; stack?: string }) { super(message ?? error?.message) if (error && error.stack) { diff --git a/ironfish/src/telemetry/telemetry.test.ts b/ironfish/src/telemetry/telemetry.test.ts index 60a9bf7c8f..6e42e4cdac 100644 --- a/ironfish/src/telemetry/telemetry.test.ts +++ b/ironfish/src/telemetry/telemetry.test.ts @@ -38,8 +38,8 @@ describe('Telemetry', () => { telemetry.start() }) - afterEach(() => { - telemetry?.stop() + afterEach(async () => { + await telemetry?.stop() }) describe('stop', () => { diff --git a/ironfish/src/typedefs/blru.ts b/ironfish/src/typedefs/blru.d.ts similarity index 100% rename from ironfish/src/typedefs/blru.ts rename to ironfish/src/typedefs/blru.d.ts diff --git a/ironfish/src/utils/node.ts b/ironfish/src/utils/node.ts index 89300e7211..dc13eae3b4 100644 --- a/ironfish/src/utils/node.ts +++ b/ironfish/src/utils/node.ts @@ -4,13 +4,12 @@ import { FullNode } from '../node' import { DatabaseIsLockedError } from '../storage/database/errors' -import { WalletNode } from '../walletNode' import { PromiseUtils } from './promise' /** * Try to open the node DB's and wait until they can be opened */ -async function waitForOpen(node: IronfishNode, abort?: null | (() => boolean)): Promise { +async function waitForOpen(node: FullNode, abort?: null | (() => boolean)): Promise { let logged = false while (!abort || !abort()) { @@ -36,5 +35,3 @@ async function waitForOpen(node: IronfishNode, abort?: null | (() => boolean)): } export const NodeUtils = { waitForOpen } - -export type IronfishNode = FullNode | WalletNode diff --git a/ironfish/src/walletNode.ts b/ironfish/src/walletNode.ts deleted file mode 100644 index 565f9724a7..0000000000 --- a/ironfish/src/walletNode.ts +++ /dev/null @@ -1,355 +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 { Assert } from './assert' -import { AssetsVerifier } from './assets' -import { TestnetConsensus } from './consensus' -import { - Config, - ConfigOptions, - DEFAULT_DATA_DIR, - InternalStore, - VerifiedAssetsCacheStore, -} from './fileStores' -import { FileSystem } from './fileSystems' -import { createRootLogger, Logger } from './logger' -import { MetricsMonitor } from './metrics' -import { Migrator } from './migrations' -import { Database } from './migrations/migration' -import { getNetworkDefinition } from './networkDefinition' -import { Package } from './package' -import { BlockHeaderSerde } from './primitives/blockheader' -import { RpcSocketClient } from './rpc' -import { RpcServer } from './rpc/server' -import { Strategy } from './strategy' -import { SetTimeoutToken } from './utils' -import { Wallet, WalletDB } from './wallet' -import { calculateWorkers, WorkerPool } from './workerPool' - -export class WalletNode { - strategy: Strategy - config: Config - internal: InternalStore - wallet: Wallet - logger: Logger - metrics: MetricsMonitor - migrator: Migrator - workerPool: WorkerPool - files: FileSystem - rpc: RpcServer - pkg: Package - assetsVerifier: AssetsVerifier - nodeClient: RpcSocketClient | null - - started = false - shutdownPromise: Promise | null = null - shutdownResolve: (() => void) | null = null - - private nodeClientConnectionWarned: boolean - private nodeClientConnectionTimeout: SetTimeoutToken | null - - constructor({ - pkg, - files, - config, - internal, - wallet, - strategy, - metrics, - workerPool, - logger, - assetsVerifier, - nodeClient, - }: { - pkg: Package - files: FileSystem - config: Config - internal: InternalStore - wallet: Wallet - strategy: Strategy - metrics: MetricsMonitor - workerPool: WorkerPool - logger: Logger - assetsVerifier: AssetsVerifier - nodeClient: RpcSocketClient | null - }) { - this.files = files - this.config = config - this.internal = internal - this.wallet = wallet - this.strategy = strategy - this.metrics = metrics - this.workerPool = workerPool - this.rpc = new RpcServer(this, internal) - this.logger = logger - this.pkg = pkg - this.nodeClient = nodeClient - this.assetsVerifier = assetsVerifier - - this.migrator = new Migrator({ node: this, logger, databases: [Database.WALLET] }) - - this.nodeClientConnectionWarned = false - this.nodeClientConnectionTimeout = null - - this.config.onConfigChange.on((key, value) => this.onConfigChange(key, value)) - } - - static async init({ - pkg: pkg, - dataDir, - config, - internal, - logger = createRootLogger(), - metrics, - files, - strategyClass, - nodeClient, - }: { - pkg: Package - dataDir?: string - config?: Config - internal?: InternalStore - logger?: Logger - metrics?: MetricsMonitor - files: FileSystem - strategyClass: typeof Strategy | null - nodeClient: RpcSocketClient | null - }): Promise { - logger = logger.withTag('walletnode') - dataDir = dataDir || DEFAULT_DATA_DIR - - if (!config) { - config = new Config(files, dataDir) - await config.load() - } - - if (!internal) { - internal = new InternalStore(files, dataDir) - await internal.load() - } - - const verifiedAssetsCache = new VerifiedAssetsCacheStore(files, dataDir) - await verifiedAssetsCache.load() - - const assetsVerifier = new AssetsVerifier({ - apiUrl: config.get('assetVerificationApi'), - cache: verifiedAssetsCache, - logger, - }) - - const numWorkers = calculateWorkers(config.get('nodeWorkers'), config.get('nodeWorkersMax')) - - const workerPool = new WorkerPool({ metrics, numWorkers }) - - metrics = metrics || new MetricsMonitor({ logger }) - - const networkDefinition = await getNetworkDefinition(config, internal, files) - - const consensus = new TestnetConsensus(networkDefinition.consensus) - - strategyClass = strategyClass || Strategy - const strategy = new strategyClass({ workerPool, consensus }) - - const walletDB = new WalletDB({ - location: config.walletDatabasePath, - workerPool, - files, - }) - - const wallet = new Wallet({ - config, - database: walletDB, - workerPool, - consensus, - nodeClient, - }) - - return new WalletNode({ - pkg, - strategy, - files, - config, - internal, - wallet, - metrics, - workerPool, - logger, - assetsVerifier, - nodeClient, - }) - } - - async openDB(): Promise { - const migrate = this.config.get('databaseMigrate') - const initial = await this.migrator.isInitial() - - if (migrate || initial) { - await this.migrator.migrate({ - quiet: !migrate, - quietNoop: true, - }) - } - - try { - await this.wallet.open() - } catch (e) { - await this.wallet.close() - throw e - } - } - - async closeDB(): Promise { - await this.wallet.close() - } - - async start(): Promise { - this.shutdownPromise = new Promise((r) => (this.shutdownResolve = r)) - this.started = true - - // Work in the worker pool happens concurrently, - // so we should start it as soon as possible - this.workerPool.start() - - if (this.config.get('enableMetrics')) { - this.metrics.start() - } - - if (this.config.get('enableRpc')) { - await this.rpc.start() - } - - if (this.config.get('enableAssetVerification')) { - this.assetsVerifier.start() - } - - await this.connectRpc(true) - await this.verifyGenesisBlockHash() - } - - async verifyGenesisBlockHash(): Promise { - const networkDefinition = await getNetworkDefinition(this.config, this.internal, this.files) - - Assert.isNotNull(this.nodeClient) - - const response = await this.nodeClient.chain.getChainInfo() - - const nodeGenesisHash = Buffer.from(response.content.genesisBlockIdentifier.hash, 'hex') - const walletGenesisHeader = BlockHeaderSerde.deserialize(networkDefinition.genesis.header) - - if (walletGenesisHeader.hash.equals(nodeGenesisHash)) { - this.logger.info('Verified genesis block hash') - } else { - throw new Error( - `Cannot sync from this node because the node's genesis block hash ${nodeGenesisHash.toString( - 'hex', - )} does not match the wallet's genesis block hash ${walletGenesisHeader.hash.toString( - 'hex', - )}`, - ) - } - } - - async connectRpc(startWallet?: boolean): Promise { - Assert.isNotNull(this.nodeClient) - this.nodeClient.onClose.on(() => this.onDisconnectRpc(startWallet)) - await this.startConnectingRpc(startWallet) - } - - private async startConnectingRpc(startWallet?: boolean): Promise { - Assert.isNotNull(this.nodeClient) - const connected = await this.nodeClient.tryConnect() - if (!connected) { - if (!this.nodeClientConnectionWarned) { - this.logger.warn( - `Failed to connect to node on ${this.nodeClient.describe()}, retrying...`, - ) - this.logger.warn('') - this.nodeClientConnectionWarned = true - } - - this.nodeClientConnectionTimeout = setTimeout( - () => void this.startConnectingRpc(startWallet), - 5000, - ) - return - } - - this.nodeClientConnectionWarned = false - this.logger.info('Successfully connected to node') - - if (startWallet) { - await this.wallet.start() - } - } - - private onDisconnectRpc = (startWallet?: boolean): void => { - this.logger.info('') - this.logger.info('Disconnected from node unexpectedly. Reconnecting.') - void this.wallet.stop() - - void this.startConnectingRpc(startWallet) - } - - async waitForShutdown(): Promise { - await this.shutdownPromise - } - - async shutdown(): Promise { - Assert.isNotNull(this.nodeClient) - this.nodeClient.onClose.off(this.onDisconnectRpc) - this.nodeClient.close() - - if (this.nodeClientConnectionTimeout) { - clearTimeout(this.nodeClientConnectionTimeout) - } - - await Promise.allSettled([ - this.wallet.stop(), - this.rpc.stop(), - this.assetsVerifier.stop(), - this.metrics.stop(), - ]) - - // Do after to avoid unhandled error from aborted jobs - await Promise.allSettled([this.workerPool.stop()]) - - if (this.shutdownResolve) { - this.shutdownResolve() - } - - this.started = false - } - - async onConfigChange( - key: Key, - newValue: ConfigOptions[Key], - ): Promise { - switch (key) { - case 'enableMetrics': { - if (newValue) { - this.metrics.start() - } else { - this.metrics.stop() - } - break - } - case 'enableRpc': { - if (newValue) { - await this.rpc.start() - } else { - await this.rpc.stop() - } - break - } - case 'enableAssetVerification': { - if (newValue) { - this.assetsVerifier.start() - } else { - this.assetsVerifier.stop() - } - break - } - } - } -} diff --git a/ironfish/src/webApi.ts b/ironfish/src/webApi.ts deleted file mode 100644 index 1027e2fd2b..0000000000 --- a/ironfish/src/webApi.ts +++ /dev/null @@ -1,199 +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, { AxiosError, AxiosRequestConfig } from 'axios' -import { getTransactionSize } from './network/utils/serializers' -import { Transaction } from './primitives' -import { FollowChainStreamResponse } from './rpc/routes/chain/followChainStream' -import { BlockHashSerdeInstance } from './serde' -import { Metric } from './telemetry' -import { BufferUtils } from './utils' -import { HasOwnProperty, UnwrapPromise } from './utils/types' - -type FaucetTransaction = { - object: 'faucet_transaction' - id: number - public_key: string - started_at: string | null - completed_at: string | null -} - -/** - * The API should be compatible with the Ironfish API here - * used to host our Facuet, BlockExplorer, and other things. - * https://github.com/iron-fish/ironfish-api - */ -export class WebApi { - host: string - token: string - getFundsEndpoint: string | null - - constructor(options?: { host?: string; token?: string; getFundsEndpoint?: string }) { - let host = options?.host ?? 'https://api.ironfish.network' - - if (host.endsWith('/')) { - host = host.slice(0, -1) - } - - this.host = host - this.token = options?.token || '' - this.getFundsEndpoint = options?.getFundsEndpoint || null - } - - async headBlocks(): Promise { - const response = await axios - .get<{ hash: string }>(`${this.host}/blocks/head`) - .catch((e) => { - // The API returns 404 for no head - if (IsAxiosError(e) && e.response?.status === 404) { - return null - } - - throw e - }) - - return response?.data.hash || null - } - - async blocks(blocks: FollowChainStreamResponse[]): Promise { - this.requireToken() - - const serialized = blocks.map(({ type, block }) => ({ - type: type, - hash: block.hash, - sequence: block.sequence, - timestamp: block.timestamp, - previous_block_hash: block.previous, - difficulty: block.difficulty, - size: block.size, - graffiti: block.graffiti, - main: type === 'connected', - transactions: block.transactions, - work: block.work, - })) - - const options = this.options({ 'Content-Type': 'application/json' }) - - await axios.post(`${this.host}/blocks`, { blocks: serialized }, options) - } - - async transactions(transactions: Transaction[]): Promise { - this.requireToken() - - const serialized = [] - - for (const transaction of transactions) { - serialized.push({ - hash: BlockHashSerdeInstance.serialize(transaction.hash()), - size: getTransactionSize(transaction), - fee: Number(transaction.fee()), - notes: transaction.notes.map((note) => ({ - commitment: note.hash().toString('hex'), - })), - spends: transaction.spends.map((spend) => ({ - nullifier: spend.nullifier.toString('hex'), - })), - mints: transaction.mints.map((mint) => ({ - id: mint.asset.id().toString('hex'), - metadata: BufferUtils.toHuman(mint.asset.metadata()), - name: BufferUtils.toHuman(mint.asset.name()), - creator: mint.asset.creator().toString('hex'), - owner: mint.owner.toString('hex'), - value: mint.value.toString(), - })), - burns: transaction.burns.map((burn) => ({ - id: burn.assetId.toString('hex'), - value: burn.value.toString(), - })), - expiration: transaction.expiration(), - }) - } - - const options = this.options({ 'Content-Type': 'application/json' }) - - await axios.post(`${this.host}/transactions`, { transactions: serialized }, options) - } - - async getFunds(data: { email?: string; public_key: string }): Promise<{ - id: number - object: 'faucet_transaction' - public_key: string - completed_at: number | null - started_at: number | null - }> { - const endpoint = this.getFundsEndpoint || `${this.host}/faucet_transactions` - const options = this.options({ 'Content-Type': 'application/json' }) - - type GetFundsResponse = UnwrapPromise> - - const response = await axios.post( - endpoint, - { - email: data.email, - public_key: data.public_key, - }, - options, - ) - - return response.data - } - - async getNextFaucetTransactions(count: number): Promise { - this.requireToken() - - const response = await axios.get<{ data: FaucetTransaction[] }>( - `${this.host}/faucet_transactions/next?count=${count}`, - this.options(), - ) - - return response.data.data - } - - async startFaucetTransaction(id: number): Promise { - this.requireToken() - - const response = await axios.post( - `${this.host}/faucet_transactions/${id}/start`, - undefined, - this.options(), - ) - - return response.data - } - - async completeFaucetTransaction(id: number, hash: string): Promise { - this.requireToken() - - const response = await axios.post( - `${this.host}/faucet_transactions/${id}/complete`, - { hash }, - this.options(), - ) - - return response.data - } - - async submitTelemetry(payload: { points: Metric[]; graffiti?: string }): Promise { - await axios.post(`${this.host}/telemetry`, payload) - } - - options(headers: Record = {}): AxiosRequestConfig { - return { - headers: { - Authorization: `Bearer ${this.token}`, - ...headers, - }, - } - } - - requireToken(): void { - if (!this.token) { - throw new Error(`Token required for endpoint`) - } - } -} - -export function IsAxiosError(e: unknown): e is AxiosError { - return typeof e === 'object' && e != null && HasOwnProperty(e, 'isAxiosError') -} diff --git a/ironfish/src/workerPool/tasks/submitTelemetry.ts b/ironfish/src/workerPool/tasks/submitTelemetry.ts index 24d91de8e2..20845fa757 100644 --- a/ironfish/src/workerPool/tasks/submitTelemetry.ts +++ b/ironfish/src/workerPool/tasks/submitTelemetry.ts @@ -1,11 +1,11 @@ /* 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 bufio from 'bufio' import { Tag } from '../../telemetry' import { Metric } from '../../telemetry/interfaces/metric' import { BufferUtils } from '../../utils/buffer' -import { WebApi } from '../../webApi' import { WorkerMessage, WorkerMessageType } from './workerMessage' import { WorkerTask } from './workerTask' @@ -206,8 +206,10 @@ export class SubmitTelemetryTask extends WorkerTask { graffiti, apiHost, }: SubmitTelemetryRequest): Promise { - const api = new WebApi({ host: apiHost }) - await api.submitTelemetry({ points, graffiti: BufferUtils.toHuman(graffiti) }) + await axios.post(`${apiHost}/telemetry`, { + points, + graffiti: BufferUtils.toHuman(graffiti), + }) return new SubmitTelemetryResponse(jobId) } } diff --git a/ironfish/tsconfig.test.json b/ironfish/tsconfig.test.json index 641a393a95..26971efaa2 100644 --- a/ironfish/tsconfig.test.json +++ b/ironfish/tsconfig.test.json @@ -5,5 +5,4 @@ "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo" }, "include": ["src", "package.json"], - "references": [], } diff --git a/package.json b/package.json index 44d978adf4..5bb7d14f30 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "coverage:upload": "lerna exec '\"yarn codecov -t $CODECOV_TOKEN -f ./coverage/clover.xml -F $LERNA_PACKAGE_NAME -p $ROOT_PATH/ --disable=gcov\"'" }, "devDependencies": { - "@types/jest": "29.2.4", + "@types/jest": "29.5.8", "@typescript-eslint/eslint-plugin": "4.28.1", "@typescript-eslint/parser": "4.28.1", "codecov": "3.8.3", @@ -50,13 +50,13 @@ "eslint-plugin-react": "7.24.0", "eslint-plugin-react-hooks": "4.2.0", "eslint-plugin-simple-import-sort": "7.0.0", - "jest": "29.3.1", - "jest-jasmine2": "29.3.1", + "jest": "29.7.0", + "jest-jasmine2": "29.7.0", "lerna": "6.4.1", "node-gyp": "8.4.1", "prettier": "2.3.2", - "ts-jest": "29.0.3", - "typescript": "4.3.4" + "ts-jest": "29.1.1", + "typescript": "5.0.4" }, "resolutions": { "axios": "0.21.4", diff --git a/yarn.lock b/yarn.lock index 8a1663c68d..1d83f4da82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -1341,7 +1346,7 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/traverse@^7.23.2", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.23.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== @@ -1481,105 +1486,61 @@ jest-util "^29.3.1" slash "^3.0.0" -"@jest/console@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.6.1.tgz#b48ba7b9c34b51483e6d590f46e5837f1ab5f639" - integrity sha512-Aj772AYgwTSr5w8qnyoJ0eDYvN6bMsH3ORH1ivMotrInHLKdUz6BDlaEXHdM6kODaBIkNIyQGzsMvRdOv7VG7Q== - dependencies: - "@jest/types" "^29.6.1" - "@types/node" "*" - chalk "^4.0.0" - jest-message-util "^29.6.1" - jest-util "^29.6.1" - slash "^3.0.0" - -"@jest/core@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.3.1.tgz#bff00f413ff0128f4debec1099ba7dcd649774a1" - integrity sha512-0ohVjjRex985w5MmO5L3u5GR1O30DexhBSpuwx2P+9ftyqHdJXnk7IUWiP80oHMvt7ubHCJHxV0a0vlKVuZirw== +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== dependencies: - "@jest/console" "^29.3.1" - "@jest/reporters" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/types" "^29.6.3" "@types/node" "*" - ansi-escapes "^4.2.1" chalk "^4.0.0" - ci-info "^3.2.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - jest-changed-files "^29.2.0" - jest-config "^29.3.1" - jest-haste-map "^29.3.1" - jest-message-util "^29.3.1" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-resolve-dependencies "^29.3.1" - jest-runner "^29.3.1" - jest-runtime "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" - jest-watcher "^29.3.1" - micromatch "^4.0.4" - pretty-format "^29.3.1" + jest-message-util "^29.7.0" + jest-util "^29.7.0" slash "^3.0.0" - strip-ansi "^6.0.0" -"@jest/core@^29.5.0", "@jest/core@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.6.1.tgz#fac0d9ddf320490c93356ba201451825231e95f6" - integrity sha512-CcowHypRSm5oYQ1obz1wfvkjZZ2qoQlrKKvlfPwh5jUXVU12TWr2qMeH8chLMuTFzHh5a1g2yaqlqDICbr+ukQ== +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== dependencies: - "@jest/console" "^29.6.1" - "@jest/reporters" "^29.6.1" - "@jest/test-result" "^29.6.1" - "@jest/transform" "^29.6.1" - "@jest/types" "^29.6.1" + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" ci-info "^3.2.0" exit "^0.1.2" graceful-fs "^4.2.9" - jest-changed-files "^29.5.0" - jest-config "^29.6.1" - jest-haste-map "^29.6.1" - jest-message-util "^29.6.1" - jest-regex-util "^29.4.3" - jest-resolve "^29.6.1" - jest-resolve-dependencies "^29.6.1" - jest-runner "^29.6.1" - jest-runtime "^29.6.1" - jest-snapshot "^29.6.1" - jest-util "^29.6.1" - jest-validate "^29.6.1" - jest-watcher "^29.6.1" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" micromatch "^4.0.4" - pretty-format "^29.6.1" + pretty-format "^29.7.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.3.1.tgz#eb039f726d5fcd14698acd072ac6576d41cfcaa6" - integrity sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag== - dependencies: - "@jest/fake-timers" "^29.3.1" - "@jest/types" "^29.3.1" - "@types/node" "*" - jest-mock "^29.3.1" - -"@jest/environment@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.6.1.tgz#ee358fff2f68168394b4a50f18c68278a21fe82f" - integrity sha512-RMMXx4ws+Gbvw3DfLSuo2cfQlK7IwGbpuEWXCqyYDcqYTI+9Ju3a5hDnXaxjNsa6uKh9PQF2v+qg+RLe63tz5A== +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== dependencies: - "@jest/fake-timers" "^29.6.1" - "@jest/types" "^29.6.1" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" - jest-mock "^29.6.1" + jest-mock "^29.7.0" "@jest/expect-utils@^29.3.1": version "29.3.1" @@ -1588,74 +1549,44 @@ dependencies: jest-get-type "^29.2.0" -"@jest/expect-utils@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.1.tgz#ab83b27a15cdd203fe5f68230ea22767d5c3acc5" - integrity sha512-o319vIf5pEMx0LmzSxxkYYxo4wrRLKHq9dP1yJU7FoPTB0LfAKSz8SWD6D/6U3v/O52t9cF5t+MeJiRsfk7zMw== - dependencies: - jest-get-type "^29.4.3" - -"@jest/expect@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.3.1.tgz#456385b62894349c1d196f2d183e3716d4c6a6cd" - integrity sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg== - dependencies: - expect "^29.3.1" - jest-snapshot "^29.3.1" - -"@jest/expect@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.6.1.tgz#fef18265188f6a97601f1ea0a2912d81a85b4657" - integrity sha512-N5xlPrAYaRNyFgVf2s9Uyyvr795jnB6rObuPx4QFvNJz8aAjpZUDfO4bh5G/xuplMID8PrnuF1+SfSyDxhsgYg== +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== dependencies: - expect "^29.6.1" - jest-snapshot "^29.6.1" + jest-get-type "^29.6.3" -"@jest/fake-timers@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.3.1.tgz#b140625095b60a44de820876d4c14da1aa963f67" - integrity sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A== +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== dependencies: - "@jest/types" "^29.3.1" - "@sinonjs/fake-timers" "^9.1.2" - "@types/node" "*" - jest-message-util "^29.3.1" - jest-mock "^29.3.1" - jest-util "^29.3.1" + expect "^29.7.0" + jest-snapshot "^29.7.0" -"@jest/fake-timers@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.6.1.tgz#c773efddbc61e1d2efcccac008139f621de57c69" - integrity sha512-RdgHgbXyosCDMVYmj7lLpUwXA4c69vcNzhrt69dJJdf8azUrpRh3ckFCaTPNjsEeRi27Cig0oKDGxy5j7hOgHg== +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== dependencies: - "@jest/types" "^29.6.1" + "@jest/types" "^29.6.3" "@sinonjs/fake-timers" "^10.0.2" "@types/node" "*" - jest-message-util "^29.6.1" - jest-mock "^29.6.1" - jest-util "^29.6.1" - -"@jest/globals@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.3.1.tgz#92be078228e82d629df40c3656d45328f134a0c6" - integrity sha512-cTicd134vOcwO59OPaB6AmdHQMCtWOe+/DitpTZVxWgMJ+YvXL1HNAmPyiGbSHmF/mXVBkvlm8YYtQhyHPnV6Q== - dependencies: - "@jest/environment" "^29.3.1" - "@jest/expect" "^29.3.1" - "@jest/types" "^29.3.1" - jest-mock "^29.3.1" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" -"@jest/globals@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.6.1.tgz#c8a8923e05efd757308082cc22893d82b8aa138f" - integrity sha512-2VjpaGy78JY9n9370H8zGRCFbYVWwjY6RdDMhoJHa1sYfwe6XM/azGN0SjY8kk7BOZApIejQ1BFPyH7FPG0w3A== +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== dependencies: - "@jest/environment" "^29.6.1" - "@jest/expect" "^29.6.1" - "@jest/types" "^29.6.1" - jest-mock "^29.6.1" + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" -"@jest/reporters@29.3.1", "@jest/reporters@^29.3.1": +"@jest/reporters@29.3.1": version "29.3.1" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.3.1.tgz#9a6d78c109608e677c25ddb34f907b90e07b4310" integrity sha512-GhBu3YFuDrcAYW/UESz1JphEAbvUjaY2vShRZRoRY1mxpCMB3yGSJ4j9n0GxVlEOdCf7qjvUfBCrTUUqhVfbRA== @@ -1685,16 +1616,16 @@ strip-ansi "^6.0.0" v8-to-istanbul "^9.0.1" -"@jest/reporters@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.6.1.tgz#3325a89c9ead3cf97ad93df3a427549d16179863" - integrity sha512-9zuaI9QKr9JnoZtFQlw4GREQbxgmNYXU6QuWtmuODvk5nvPUeBYapVR/VYMyi2WSx3jXTLJTJji8rN6+Cm4+FA== +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.6.1" - "@jest/test-result" "^29.6.1" - "@jest/transform" "^29.6.1" - "@jest/types" "^29.6.1" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" "@jridgewell/trace-mapping" "^0.3.18" "@types/node" "*" chalk "^4.0.0" @@ -1703,13 +1634,13 @@ glob "^7.1.3" graceful-fs "^4.2.9" istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^5.1.0" + istanbul-lib-instrument "^6.0.0" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.1.3" - jest-message-util "^29.6.1" - jest-util "^29.6.1" - jest-worker "^29.6.1" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" slash "^3.0.0" string-length "^4.0.1" strip-ansi "^6.0.0" @@ -1722,13 +1653,6 @@ dependencies: "@sinclair/typebox" "^0.24.1" -"@jest/schemas@^29.4.3", "@jest/schemas@^29.6.0": - version "29.6.0" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.0.tgz#0f4cb2c8e3dca80c135507ba5635a4fd755b0040" - integrity sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ== - dependencies: - "@sinclair/typebox" "^0.27.8" - "@jest/schemas@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" @@ -1736,19 +1660,10 @@ dependencies: "@sinclair/typebox" "^0.27.8" -"@jest/source-map@^29.2.0": - version "29.2.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.2.0.tgz#ab3420c46d42508dcc3dc1c6deee0b613c235744" - integrity sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ== - dependencies: - "@jridgewell/trace-mapping" "^0.3.15" - callsites "^3.0.0" - graceful-fs "^4.2.9" - -"@jest/source-map@^29.6.0": - version "29.6.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.0.tgz#bd34a05b5737cb1a99d43e1957020ac8e5b9ddb1" - integrity sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA== +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== dependencies: "@jridgewell/trace-mapping" "^0.3.18" callsites "^3.0.0" @@ -1764,34 +1679,24 @@ "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-result@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.6.1.tgz#850e565a3f58ee8ca6ec424db00cb0f2d83c36ba" - integrity sha512-Ynr13ZRcpX6INak0TPUukU8GWRfm/vAytE3JbJNGAvINySWYdfE7dGZMbk36oVuK4CigpbhMn8eg1dixZ7ZJOw== +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== dependencies: - "@jest/console" "^29.6.1" - "@jest/types" "^29.6.1" + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.3.1.tgz#fa24b3b050f7a59d48f7ef9e0b782ab65123090d" - integrity sha512-IqYvLbieTv20ArgKoAMyhLHNrVHJfzO6ARZAbQRlY4UGWfdDnLlZEF0BvKOMd77uIiIjSZRwq3Jb3Fa3I8+2UA== - dependencies: - "@jest/test-result" "^29.3.1" - graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" - slash "^3.0.0" - -"@jest/test-sequencer@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.6.1.tgz#e3e582ee074dd24ea9687d7d1aaf05ee3a9b068e" - integrity sha512-oBkC36PCDf/wb6dWeQIhaviU0l5u6VCsXa119yqdUosYAt7/FbQU2M2UoziO3igj/HBDEgp57ONQ3fm0v9uyyg== +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== dependencies: - "@jest/test-result" "^29.6.1" + "@jest/test-result" "^29.7.0" graceful-fs "^4.2.9" - jest-haste-map "^29.6.1" + jest-haste-map "^29.7.0" slash "^3.0.0" "@jest/transform@^29.3.1": @@ -1815,27 +1720,6 @@ slash "^3.0.0" write-file-atomic "^4.0.1" -"@jest/transform@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.6.1.tgz#acb5606019a197cb99beda3c05404b851f441c92" - integrity sha512-URnTneIU3ZjRSaf906cvf6Hpox3hIeJXRnz3VDSw5/X93gR8ycdfSIEy19FlVx8NFmpN7fe3Gb1xF+NjXaQLWg== - dependencies: - "@babel/core" "^7.11.6" - "@jest/types" "^29.6.1" - "@jridgewell/trace-mapping" "^0.3.18" - babel-plugin-istanbul "^6.1.1" - chalk "^4.0.0" - convert-source-map "^2.0.0" - fast-json-stable-stringify "^2.1.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.6.1" - jest-regex-util "^29.4.3" - jest-util "^29.6.1" - micromatch "^4.0.4" - pirates "^4.0.4" - slash "^3.0.0" - write-file-atomic "^4.0.2" - "@jest/transform@^29.7.0": version "29.7.0" resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" @@ -1857,12 +1741,12 @@ slash "^3.0.0" write-file-atomic "^4.0.2" -"@jest/types@29.5.0": - version "29.5.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593" - integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog== +"@jest/types@29.6.3", "@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== dependencies: - "@jest/schemas" "^29.4.3" + "@jest/schemas" "^29.6.3" "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" "@types/node" "*" @@ -1881,30 +1765,6 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jest/types@^29.5.0", "@jest/types@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.1.tgz#ae79080278acff0a6af5eb49d063385aaa897bf2" - integrity sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw== - dependencies: - "@jest/schemas" "^29.6.0" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jest/types@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" - integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== - dependencies: - "@jest/schemas" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - "@jridgewell/gen-mapping@^0.3.0": version "0.3.3" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" @@ -3525,13 +3385,6 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@sinonjs/fake-timers@^9.1.2": - version "9.1.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" - integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== - dependencies: - "@sinonjs/commons" "^1.7.0" - "@smithy/protocol-http@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-1.0.1.tgz#62fd73d73db285fd8e9a2287ed2904ac66e0d43f" @@ -3697,18 +3550,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@29.2.4": - version "29.2.4" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.4.tgz#9c155c4b81c9570dbd183eb8604aa0ae80ba5a5b" - integrity sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A== - dependencies: - expect "^29.0.0" - pretty-format "^29.0.0" - -"@types/jest@29.5.2": - version "29.5.2" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.2.tgz#86b4afc86e3a8f3005b297ed8a72494f89e6395b" - integrity sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg== +"@types/jest@29.5.8": + version "29.5.8" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.8.tgz#ed5c256fe2bc7c38b1915ee5ef1ff24a3427e120" + integrity sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -3718,7 +3563,12 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== -"@types/json-schema@^7.0.7", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.7": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== @@ -3785,20 +3635,15 @@ dependencies: "@types/node" "*" -"@types/node@*": - version "16.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42" - integrity sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw== - -"@types/node@18.11.16": +"@types/node@*", "@types/node@18.11.16": version "18.11.16" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.16.tgz#966cae211e970199559cfbd295888fca189e49af" integrity sha512-6T7P5bDkRhqRxrQtwj7vru+bWTpelgtcETAZEUSdq0YISKz8WKdoBukQLYQQ6DFHvU9JRsbFq0JH5C51X2ZdnA== "@types/node@^14.0.1": - version "14.18.51" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.51.tgz#cb90935b89c641201c3d07a595c3e22d1cfaa417" - integrity sha512-P9bsdGFPpVtofEKlhWMVS2qqx1A/rt9QBfihWlklfHHpUpjtYse5AzFz6j4DWrARLYh6gRnw9+5+DJcrq3KvBA== + version "14.18.63" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" + integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== "@types/node@^15.6.1": version "15.14.9" @@ -3815,11 +3660,6 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== -"@types/prettier@^2.1.5": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e" - integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow== - "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" @@ -4217,9 +4057,9 @@ ajv@^6.10.0, ajv@^6.12.4: uri-js "^4.2.2" ajv@^8.0.1: - version "8.8.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.8.0.tgz#c501f10df72914bb77a458919e79fc73e4a2f9ef" - integrity sha512-L+cJ/+pkdICMueKR6wIx3VP2fjIx3yAhuvadUv/osv9yFD7OVZy442xFF+Oeu3ZvmhBGQzoF6mTSt+LUWBmGQg== + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -4452,7 +4292,7 @@ axios@0.21.4, axios@^1.0.0: dependencies: follow-redirects "^1.14.0" -babel-jest@^29.3.1, babel-jest@^29.6.1: +babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== @@ -5272,6 +5112,19 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -5329,7 +5182,7 @@ dateformat@^4.5.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== @@ -5350,6 +5203,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.0.1, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debug@^4.3.2, debug@^4.3.3: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" @@ -5357,13 +5217,6 @@ debug@^4.3.2, debug@^4.3.3: dependencies: ms "2.1.2" -debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - 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" @@ -5399,6 +5252,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +dedent@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" + integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== + deep-eql@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" @@ -5501,10 +5359,10 @@ diff-sequences@^29.3.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e" integrity sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ== -diff-sequences@^29.4.3: - version "29.4.3" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" - integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== diff@^4.0.1: version "4.0.2" @@ -5597,7 +5455,15 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -enquirer@^2.3.5, enquirer@~2.3.6: +enquirer@^2.3.5: + version "2.4.1" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" + integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== + dependencies: + ansi-colors "^4.1.1" + strip-ansi "^6.0.1" + +enquirer@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -5824,16 +5690,16 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== - -eslint-visitor-keys@^3.4.1: +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.4.1: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + eslint@7.29.0: version "7.29.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.29.0.tgz#ee2a7648f2e729485e4d0bd6383ec1deabc8b3c0" @@ -5893,13 +5759,20 @@ esprima@^4.0.0, esprima@~4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.0.1, esquery@^1.4.0: +esquery@^1.0.1: version "1.4.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== dependencies: estraverse "^5.1.0" +esquery@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -5998,7 +5871,7 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -expect@^29.0.0, expect@^29.3.1: +expect@^29.0.0: version "29.3.1" resolved "https://registry.yarnpkg.com/expect/-/expect-29.3.1.tgz#92877aad3f7deefc2e3f6430dd195b92295554a6" integrity sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA== @@ -6009,17 +5882,16 @@ expect@^29.0.0, expect@^29.3.1: jest-message-util "^29.3.1" jest-util "^29.3.1" -expect@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.1.tgz#64dd1c8f75e2c0b209418f2b8d36a07921adfdf1" - integrity sha512-XEdDLonERCU1n9uR56/Stx9OqojaLAQtZf9PrCHH9Hl8YXiEIka3H4NXJ3NOIBmQJTg7+j7buh34PMHfJujc8g== +expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== dependencies: - "@jest/expect-utils" "^29.6.1" - "@types/node" "*" - jest-get-type "^29.4.3" - jest-matcher-utils "^29.6.1" - jest-message-util "^29.6.1" - jest-util "^29.6.1" + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" external-editor@^3.0.3: version "3.1.0" @@ -6306,7 +6178,7 @@ function-bind@^1.1.1: functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== gauge@^3.0.0: version "3.0.2" @@ -6554,9 +6426,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: - version "13.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.0.tgz#4d733760304230a0082ed96e21e5c565f898089e" - integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== + version "13.23.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.23.0.tgz#ef31673c926a0976e1f61dab4dca57e0c0a8af02" + integrity sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA== dependencies: type-fest "^0.20.2" @@ -7239,6 +7111,17 @@ istanbul-lib-instrument@^5.1.0: istanbul-lib-coverage "^3.2.0" semver "^6.3.0" +istanbul-lib-instrument@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz#71e87707e8041428732518c6fb5211761753fbdf" + integrity sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + istanbul-lib-report@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" @@ -7275,162 +7158,83 @@ jake@^10.8.5: filelist "^1.0.1" minimatch "^3.0.4" -jest-changed-files@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.2.0.tgz#b6598daa9803ea6a4dce7968e20ab380ddbee289" - integrity sha512-qPVmLLyBmvF5HJrY7krDisx6Voi8DmlV3GZYX0aFNbaQsZeoz1hfxcCMbqDGuQCxU1dJy9eYc2xscE8QrCCYaA== - dependencies: - execa "^5.0.0" - p-limit "^3.1.0" - -jest-changed-files@^29.5.0: - version "29.5.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" - integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag== +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== dependencies: execa "^5.0.0" + jest-util "^29.7.0" p-limit "^3.1.0" -jest-circus@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.3.1.tgz#177d07c5c0beae8ef2937a67de68f1e17bbf1b4a" - integrity sha512-wpr26sEvwb3qQQbdlmei+gzp6yoSSoSL6GsLPxnuayZSMrSd5Ka7IjAvatpIernBvT2+Ic6RLTg+jSebScmasg== - dependencies: - "@jest/environment" "^29.3.1" - "@jest/expect" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - dedent "^0.7.0" - is-generator-fn "^2.0.0" - jest-each "^29.3.1" - jest-matcher-utils "^29.3.1" - jest-message-util "^29.3.1" - jest-runtime "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" - p-limit "^3.1.0" - pretty-format "^29.3.1" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-circus@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.6.1.tgz#861dab37e71a89907d1c0fabc54a0019738ed824" - integrity sha512-tPbYLEiBU4MYAL2XoZme/bgfUeotpDBd81lgHLCbDZZFaGmECk0b+/xejPFtmiBP87GgP/y4jplcRpbH+fgCzQ== +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== dependencies: - "@jest/environment" "^29.6.1" - "@jest/expect" "^29.6.1" - "@jest/test-result" "^29.6.1" - "@jest/types" "^29.6.1" + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - dedent "^0.7.0" + dedent "^1.0.0" is-generator-fn "^2.0.0" - jest-each "^29.6.1" - jest-matcher-utils "^29.6.1" - jest-message-util "^29.6.1" - jest-runtime "^29.6.1" - jest-snapshot "^29.6.1" - jest-util "^29.6.1" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" p-limit "^3.1.0" - pretty-format "^29.6.1" + pretty-format "^29.7.0" pure-rand "^6.0.0" slash "^3.0.0" stack-utils "^2.0.3" -jest-cli@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.3.1.tgz#e89dff427db3b1df50cea9a393ebd8640790416d" - integrity sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ== - dependencies: - "@jest/core" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - import-local "^3.0.2" - jest-config "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" - prompts "^2.0.1" - yargs "^17.3.1" - -jest-cli@^29.5.0: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.6.1.tgz#99d9afa7449538221c71f358f0fdd3e9c6e89f72" - integrity sha512-607dSgTA4ODIN6go9w6xY3EYkyPFGicx51a69H7yfvt7lN53xNswEVLovq+E77VsTRi5fWprLH0yl4DJgE8Ing== +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== dependencies: - "@jest/core" "^29.6.1" - "@jest/test-result" "^29.6.1" - "@jest/types" "^29.6.1" + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" chalk "^4.0.0" + create-jest "^29.7.0" exit "^0.1.2" - graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^29.6.1" - jest-util "^29.6.1" - jest-validate "^29.6.1" - prompts "^2.0.1" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" yargs "^17.3.1" -jest-config@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.3.1.tgz#0bc3dcb0959ff8662957f1259947aedaefb7f3c6" - integrity sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg== - dependencies: - "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.3.1" - "@jest/types" "^29.3.1" - babel-jest "^29.3.1" - chalk "^4.0.0" - ci-info "^3.2.0" - deepmerge "^4.2.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-circus "^29.3.1" - jest-environment-node "^29.3.1" - jest-get-type "^29.2.0" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-runner "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" - micromatch "^4.0.4" - parse-json "^5.2.0" - pretty-format "^29.3.1" - slash "^3.0.0" - strip-json-comments "^3.1.1" - -jest-config@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.6.1.tgz#d785344509065d53a238224c6cdc0ed8e2f2f0dd" - integrity sha512-XdjYV2fy2xYixUiV2Wc54t3Z4oxYPAELUzWnV6+mcbq0rh742X2p52pii5A3oeRzYjLnQxCsZmp0qpI6klE2cQ== +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== dependencies: "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.6.1" - "@jest/types" "^29.6.1" - babel-jest "^29.6.1" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.3" graceful-fs "^4.2.9" - jest-circus "^29.6.1" - jest-environment-node "^29.6.1" - jest-get-type "^29.4.3" - jest-regex-util "^29.4.3" - jest-resolve "^29.6.1" - jest-runner "^29.6.1" - jest-util "^29.6.1" - jest-validate "^29.6.1" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" micromatch "^4.0.4" parse-json "^5.2.0" - pretty-format "^29.6.1" + pretty-format "^29.7.0" slash "^3.0.0" strip-json-comments "^3.1.1" @@ -7444,85 +7248,55 @@ jest-diff@^29.3.1: jest-get-type "^29.2.0" pretty-format "^29.3.1" -jest-diff@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.1.tgz#13df6db0a89ee6ad93c747c75c85c70ba941e545" - integrity sha512-FsNCvinvl8oVxpNLttNQX7FAq7vR+gMDGj90tiP7siWw1UdakWUGqrylpsYrpvj908IYckm5Y0Q7azNAozU1Kg== +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== dependencies: chalk "^4.0.0" - diff-sequences "^29.4.3" - jest-get-type "^29.4.3" - pretty-format "^29.6.1" - -jest-docblock@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.2.0.tgz#307203e20b637d97cee04809efc1d43afc641e82" - integrity sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A== - dependencies: - detect-newline "^3.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" -jest-docblock@^29.4.3: - version "29.4.3" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" - integrity sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg== +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== dependencies: detect-newline "^3.0.0" -jest-each@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.3.1.tgz#bc375c8734f1bb96625d83d1ca03ef508379e132" - integrity sha512-qrZH7PmFB9rEzCSl00BWjZYuS1BSOH8lLuC0azQE9lQrAx3PWGKHTDudQiOSwIy5dGAJh7KA0ScYlCP7JxvFYA== - dependencies: - "@jest/types" "^29.3.1" - chalk "^4.0.0" - jest-get-type "^29.2.0" - jest-util "^29.3.1" - pretty-format "^29.3.1" - -jest-each@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.6.1.tgz#975058e5b8f55c6780beab8b6ab214921815c89c" - integrity sha512-n5eoj5eiTHpKQCAVcNTT7DRqeUmJ01hsAL0Q1SMiBHcBcvTKDELixQOGMCpqhbIuTcfC4kMfSnpmDqRgRJcLNQ== +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== dependencies: - "@jest/types" "^29.6.1" + "@jest/types" "^29.6.3" chalk "^4.0.0" - jest-get-type "^29.4.3" - jest-util "^29.6.1" - pretty-format "^29.6.1" - -jest-environment-node@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.3.1.tgz#5023b32472b3fba91db5c799a0d5624ad4803e74" - integrity sha512-xm2THL18Xf5sIHoU7OThBPtuH6Lerd+Y1NLYiZJlkE3hbE+7N7r8uvHIl/FkZ5ymKXJe/11SQuf3fv4v6rUMag== - dependencies: - "@jest/environment" "^29.3.1" - "@jest/fake-timers" "^29.3.1" - "@jest/types" "^29.3.1" - "@types/node" "*" - jest-mock "^29.3.1" - jest-util "^29.3.1" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" -jest-environment-node@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.6.1.tgz#08a122dece39e58bc388da815a2166c58b4abec6" - integrity sha512-ZNIfAiE+foBog24W+2caIldl4Irh8Lx1PUhg/GZ0odM1d/h2qORAsejiFc7zb+SEmYPn1yDZzEDSU5PmDkmVLQ== +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== dependencies: - "@jest/environment" "^29.6.1" - "@jest/fake-timers" "^29.6.1" - "@jest/types" "^29.6.1" + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" - jest-mock "^29.6.1" - jest-util "^29.6.1" + jest-mock "^29.7.0" + jest-util "^29.7.0" jest-get-type@^29.2.0: version "29.2.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.2.0.tgz#726646f927ef61d583a3b3adb1ab13f3a5036408" integrity sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA== -jest-get-type@^29.4.3: - version "29.4.3" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" - integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== jest-haste-map@^29.3.1: version "29.3.1" @@ -7543,25 +7317,6 @@ jest-haste-map@^29.3.1: optionalDependencies: fsevents "^2.3.2" -jest-haste-map@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.6.1.tgz#62655c7a1c1b349a3206441330fb2dbdb4b63803" - integrity sha512-0m7f9PZXxOCk1gRACiVgX85knUKPKLPg4oRCjLoqIm9brTHXaorMA0JpmtmVkQiT8nmXyIVoZd/nnH1cfC33ig== - dependencies: - "@jest/types" "^29.6.1" - "@types/graceful-fs" "^4.1.3" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.9" - jest-regex-util "^29.4.3" - jest-util "^29.6.1" - jest-worker "^29.6.1" - micromatch "^4.0.4" - walker "^1.0.8" - optionalDependencies: - fsevents "^2.3.2" - jest-haste-map@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" @@ -7581,44 +7336,36 @@ jest-haste-map@^29.7.0: optionalDependencies: fsevents "^2.3.2" -jest-jasmine2@29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-29.3.1.tgz#15544cce47a3ad7de1beb6ae9b644d091c0092cb" - integrity sha512-GAsY7aie7YcQc85m/grsOyRGWPDefaJlPYCt2iIPBbA5MMeTbXKrJa4vfMfxJjSmDtbQHxWEoGuUXIA1+bLgvg== +jest-jasmine2@29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-29.7.0.tgz#c3704e29ca7d75dfb546578e7e821abc5a6314a1" + integrity sha512-N3nRpBVTM5erHtMi6ODBUEqG/LpVgSJC8qk14duw88d9Eigx2vL+n4LF1d8eV8pegnnzKyNHdTGxa/NsIKj0Zw== dependencies: - "@jest/environment" "^29.3.1" - "@jest/expect" "^29.3.1" - "@jest/source-map" "^29.2.0" - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" is-generator-fn "^2.0.0" - jest-each "^29.3.1" - jest-matcher-utils "^29.3.1" - jest-message-util "^29.3.1" - jest-runtime "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" p-limit "^3.1.0" - pretty-format "^29.3.1" + pretty-format "^29.7.0" -jest-leak-detector@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.3.1.tgz#95336d020170671db0ee166b75cd8ef647265518" - integrity sha512-3DA/VVXj4zFOPagGkuqHnSQf1GZBmmlagpguxEERO6Pla2g84Q1MaVIB3YMxgUaFIaYag8ZnTyQgiZ35YEqAQA== - dependencies: - jest-get-type "^29.2.0" - pretty-format "^29.3.1" - -jest-leak-detector@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.6.1.tgz#66a902c81318e66e694df7d096a95466cb962f8e" - integrity sha512-OrxMNyZirpOEwkF3UHnIkAiZbtkBWiye+hhBweCHkVbCgyEy71Mwbb5zgeTNYWJBi1qgDVfPC1IwO9dVEeTLwQ== +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== dependencies: - jest-get-type "^29.4.3" - pretty-format "^29.6.1" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" jest-matcher-utils@^29.3.1: version "29.3.1" @@ -7630,15 +7377,15 @@ jest-matcher-utils@^29.3.1: jest-get-type "^29.2.0" pretty-format "^29.3.1" -jest-matcher-utils@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.1.tgz#6c60075d84655d6300c5d5128f46531848160b53" - integrity sha512-SLaztw9d2mfQQKHmJXKM0HCbl2PPVld/t9Xa6P9sgiExijviSp7TnZZpw2Fpt+OI3nwUO/slJbOfzfUMKKC5QA== +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== dependencies: chalk "^4.0.0" - jest-diff "^29.6.1" - jest-get-type "^29.4.3" - pretty-format "^29.6.1" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" jest-message-util@^29.3.1: version "29.3.1" @@ -7655,38 +7402,29 @@ jest-message-util@^29.3.1: slash "^3.0.0" stack-utils "^2.0.3" -jest-message-util@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.1.tgz#d0b21d87f117e1b9e165e24f245befd2ff34ff8d" - integrity sha512-KoAW2zAmNSd3Gk88uJ56qXUWbFk787QKmjjJVOjtGFmmGSZgDBrlIL4AfQw1xyMYPNVD7dNInfIbur9B2rd/wQ== +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.6.1" + "@jest/types" "^29.6.3" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.9" micromatch "^4.0.4" - pretty-format "^29.6.1" + pretty-format "^29.7.0" slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.3.1.tgz#60287d92e5010979d01f218c6b215b688e0f313e" - integrity sha512-H8/qFDtDVMFvFP4X8NuOT3XRDzOUTz+FeACjufHzsOIBAxivLqkB1PoLCaJx9iPPQ8dZThHPp/G3WRWyMgA3JA== - dependencies: - "@jest/types" "^29.3.1" - "@types/node" "*" - jest-util "^29.3.1" - -jest-mock@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.6.1.tgz#049ee26aea8cbf54c764af649070910607316517" - integrity sha512-brovyV9HBkjXAEdRooaTQK42n8usKoSRR3gihzUpYeV/vwqgSoNfrksO7UfSACnPmxasO/8TmHM3w9Hp3G1dgw== +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== dependencies: - "@jest/types" "^29.6.1" + "@jest/types" "^29.6.3" "@types/node" "*" - jest-util "^29.6.1" + jest-util "^29.7.0" jest-pnp-resolver@^1.2.2: version "1.2.2" @@ -7698,227 +7436,113 @@ jest-regex-util@^29.2.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.2.0.tgz#82ef3b587e8c303357728d0322d48bbfd2971f7b" integrity sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA== -jest-regex-util@^29.4.3: - version "29.4.3" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" - integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== - jest-regex-util@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== -jest-resolve-dependencies@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.3.1.tgz#a6a329708a128e68d67c49f38678a4a4a914c3bf" - integrity sha512-Vk0cYq0byRw2WluNmNWGqPeRnZ3p3hHmjJMp2dyyZeYIfiBskwq4rpiuGFR6QGAdbj58WC7HN4hQHjf2mpvrLA== - dependencies: - jest-regex-util "^29.2.0" - jest-snapshot "^29.3.1" - -jest-resolve-dependencies@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.1.tgz#b85b06670f987a62515bbf625d54a499e3d708f5" - integrity sha512-BbFvxLXtcldaFOhNMXmHRWx1nXQO5LoXiKSGQcA1LxxirYceZT6ch8KTE1bK3X31TNG/JbkI7OkS/ABexVahiw== - dependencies: - jest-regex-util "^29.4.3" - jest-snapshot "^29.6.1" - -jest-resolve@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.3.1.tgz#9a4b6b65387a3141e4a40815535c7f196f1a68a7" - integrity sha512-amXJgH/Ng712w3Uz5gqzFBBjxV8WFLSmNjoreBGMqxgCz5cH7swmBZzgBaCIOsvb0NbpJ0vgaSFdJqMdT+rADw== +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== dependencies: - chalk "^4.0.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" - jest-pnp-resolver "^1.2.2" - jest-util "^29.3.1" - jest-validate "^29.3.1" - resolve "^1.20.0" - resolve.exports "^1.1.0" - slash "^3.0.0" + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" -jest-resolve@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.6.1.tgz#4c3324b993a85e300add2f8609f51b80ddea39ee" - integrity sha512-AeRkyS8g37UyJiP9w3mmI/VXU/q8l/IH52vj/cDAyScDcemRbSBhfX/NMYIGilQgSVwsjxrCHf3XJu4f+lxCMg== +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== dependencies: chalk "^4.0.0" graceful-fs "^4.2.9" - jest-haste-map "^29.6.1" + jest-haste-map "^29.7.0" jest-pnp-resolver "^1.2.2" - jest-util "^29.6.1" - jest-validate "^29.6.1" + jest-util "^29.7.0" + jest-validate "^29.7.0" resolve "^1.20.0" resolve.exports "^2.0.0" slash "^3.0.0" -jest-runner@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.3.1.tgz#a92a879a47dd096fea46bb1517b0a99418ee9e2d" - integrity sha512-oFvcwRNrKMtE6u9+AQPMATxFcTySyKfLhvso7Sdk/rNpbhg4g2GAGCopiInk1OP4q6gz3n6MajW4+fnHWlU3bA== - dependencies: - "@jest/console" "^29.3.1" - "@jest/environment" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" - "@types/node" "*" - chalk "^4.0.0" - emittery "^0.13.1" - graceful-fs "^4.2.9" - jest-docblock "^29.2.0" - jest-environment-node "^29.3.1" - jest-haste-map "^29.3.1" - jest-leak-detector "^29.3.1" - jest-message-util "^29.3.1" - jest-resolve "^29.3.1" - jest-runtime "^29.3.1" - jest-util "^29.3.1" - jest-watcher "^29.3.1" - jest-worker "^29.3.1" - p-limit "^3.1.0" - source-map-support "0.5.13" - -jest-runner@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.6.1.tgz#54557087e7972d345540d622ab5bfc3d8f34688c" - integrity sha512-tw0wb2Q9yhjAQ2w8rHRDxteryyIck7gIzQE4Reu3JuOBpGp96xWgF0nY8MDdejzrLCZKDcp8JlZrBN/EtkQvPQ== +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== dependencies: - "@jest/console" "^29.6.1" - "@jest/environment" "^29.6.1" - "@jest/test-result" "^29.6.1" - "@jest/transform" "^29.6.1" - "@jest/types" "^29.6.1" + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" emittery "^0.13.1" graceful-fs "^4.2.9" - jest-docblock "^29.4.3" - jest-environment-node "^29.6.1" - jest-haste-map "^29.6.1" - jest-leak-detector "^29.6.1" - jest-message-util "^29.6.1" - jest-resolve "^29.6.1" - jest-runtime "^29.6.1" - jest-util "^29.6.1" - jest-watcher "^29.6.1" - jest-worker "^29.6.1" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" p-limit "^3.1.0" source-map-support "0.5.13" -jest-runtime@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.3.1.tgz#21efccb1a66911d6d8591276a6182f520b86737a" - integrity sha512-jLzkIxIqXwBEOZx7wx9OO9sxoZmgT2NhmQKzHQm1xwR1kNW/dn0OjxR424VwHHf1SPN6Qwlb5pp1oGCeFTQ62A== - dependencies: - "@jest/environment" "^29.3.1" - "@jest/fake-timers" "^29.3.1" - "@jest/globals" "^29.3.1" - "@jest/source-map" "^29.2.0" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" - "@types/node" "*" - chalk "^4.0.0" - cjs-module-lexer "^1.0.0" - collect-v8-coverage "^1.0.0" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" - jest-message-util "^29.3.1" - jest-mock "^29.3.1" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" - slash "^3.0.0" - strip-bom "^4.0.0" - -jest-runtime@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.6.1.tgz#8a0fc9274ef277f3d70ba19d238e64334958a0dc" - integrity sha512-D6/AYOA+Lhs5e5il8+5pSLemjtJezUr+8zx+Sn8xlmOux3XOqx4d8l/2udBea8CRPqqrzhsKUsN/gBDE/IcaPQ== - dependencies: - "@jest/environment" "^29.6.1" - "@jest/fake-timers" "^29.6.1" - "@jest/globals" "^29.6.1" - "@jest/source-map" "^29.6.0" - "@jest/test-result" "^29.6.1" - "@jest/transform" "^29.6.1" - "@jest/types" "^29.6.1" +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" glob "^7.1.3" graceful-fs "^4.2.9" - jest-haste-map "^29.6.1" - jest-message-util "^29.6.1" - jest-mock "^29.6.1" - jest-regex-util "^29.4.3" - jest-resolve "^29.6.1" - jest-snapshot "^29.6.1" - jest-util "^29.6.1" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" slash "^3.0.0" strip-bom "^4.0.0" -jest-snapshot@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.3.1.tgz#17bcef71a453adc059a18a32ccbd594b8cc4e45e" - integrity sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA== - dependencies: - "@babel/core" "^7.11.6" - "@babel/generator" "^7.7.2" - "@babel/plugin-syntax-jsx" "^7.7.2" - "@babel/plugin-syntax-typescript" "^7.7.2" - "@babel/traverse" "^7.7.2" - "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" - "@types/babel__traverse" "^7.0.6" - "@types/prettier" "^2.1.5" - babel-preset-current-node-syntax "^1.0.0" - chalk "^4.0.0" - expect "^29.3.1" - graceful-fs "^4.2.9" - jest-diff "^29.3.1" - jest-get-type "^29.2.0" - jest-haste-map "^29.3.1" - jest-matcher-utils "^29.3.1" - jest-message-util "^29.3.1" - jest-util "^29.3.1" - natural-compare "^1.4.0" - pretty-format "^29.3.1" - semver "^7.3.5" - -jest-snapshot@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.6.1.tgz#0d083cb7de716d5d5cdbe80d598ed2fbafac0239" - integrity sha512-G4UQE1QQ6OaCgfY+A0uR1W2AY0tGXUPQpoUClhWHq1Xdnx1H6JOrC2nH5lqnOEqaDgbHFgIwZ7bNq24HpB180A== +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== dependencies: "@babel/core" "^7.11.6" "@babel/generator" "^7.7.2" "@babel/plugin-syntax-jsx" "^7.7.2" "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.6.1" - "@jest/transform" "^29.6.1" - "@jest/types" "^29.6.1" - "@types/prettier" "^2.1.5" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^29.6.1" + expect "^29.7.0" graceful-fs "^4.2.9" - jest-diff "^29.6.1" - jest-get-type "^29.4.3" - jest-matcher-utils "^29.6.1" - jest-message-util "^29.6.1" - jest-util "^29.6.1" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" natural-compare "^1.4.0" - pretty-format "^29.6.1" + pretty-format "^29.7.0" semver "^7.5.3" jest-util@^29.0.0, jest-util@^29.3.1: @@ -7933,18 +7557,6 @@ jest-util@^29.0.0, jest-util@^29.3.1: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-util@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.1.tgz#c9e29a87a6edbf1e39e6dee2b4689b8a146679cb" - integrity sha512-NRFCcjc+/uO3ijUVyNOQJluf8PtGCe/W6cix36+M3cTFgiYqFOOW5MgN4JOOcvbUhcKTYVd1CvHz/LWi8d16Mg== - dependencies: - "@jest/types" "^29.6.1" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - jest-util@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" @@ -7957,56 +7569,30 @@ jest-util@^29.7.0: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.3.1.tgz#d56fefaa2e7d1fde3ecdc973c7f7f8f25eea704a" - integrity sha512-N9Lr3oYR2Mpzuelp1F8negJR3YE+L1ebk1rYA5qYo9TTY3f9OWdptLoNSPP9itOCBIRBqjt/S5XHlzYglLN67g== - dependencies: - "@jest/types" "^29.3.1" - camelcase "^6.2.0" - chalk "^4.0.0" - jest-get-type "^29.2.0" - leven "^3.1.0" - pretty-format "^29.3.1" - -jest-validate@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.6.1.tgz#765e684af6e2c86dce950aebefbbcd4546d69f7b" - integrity sha512-r3Ds69/0KCN4vx4sYAbGL1EVpZ7MSS0vLmd3gV78O+NAx3PDQQukRU5hNHPXlyqCgFY8XUk7EuTMLugh0KzahA== +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== dependencies: - "@jest/types" "^29.6.1" + "@jest/types" "^29.6.3" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^29.4.3" + jest-get-type "^29.6.3" leven "^3.1.0" - pretty-format "^29.6.1" - -jest-watcher@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.3.1.tgz#3341547e14fe3c0f79f9c3a4c62dbc3fc977fd4a" - integrity sha512-RspXG2BQFDsZSRKGCT/NiNa8RkQ1iKAjrO0//soTMWx/QUt+OcxMqMSBxz23PYGqUuWm2+m2mNNsmj0eIoOaFg== - dependencies: - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - emittery "^0.13.1" - jest-util "^29.3.1" - string-length "^4.0.1" + pretty-format "^29.7.0" -jest-watcher@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.6.1.tgz#7c0c43ddd52418af134c551c92c9ea31e5ec942e" - integrity sha512-d4wpjWTS7HEZPaaj8m36QiaP856JthRZkrgcIY/7ISoUWPIillrXM23WPboZVLbiwZBt4/qn2Jke84Sla6JhFA== +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== dependencies: - "@jest/test-result" "^29.6.1" - "@jest/types" "^29.6.1" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.13.1" - jest-util "^29.6.1" + jest-util "^29.7.0" string-length "^4.0.1" jest-worker@^29.3.1: @@ -8019,16 +7605,6 @@ jest-worker@^29.3.1: merge-stream "^2.0.0" supports-color "^8.0.0" -jest-worker@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.6.1.tgz#64b015f0e985ef3a8ad049b61fe92b3db74a5319" - integrity sha512-U+Wrbca7S8ZAxAe9L6nb6g8kPdia5hj32Puu5iOqBCMTMWFHXuK6dOV2IFrpedbTV8fjMFLdWNttQTBL6u2MRA== - dependencies: - "@types/node" "*" - jest-util "^29.6.1" - merge-stream "^2.0.0" - supports-color "^8.0.0" - jest-worker@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" @@ -8039,25 +7615,15 @@ jest-worker@^29.7.0: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.3.1.tgz#c130c0d551ae6b5459b8963747fed392ddbde122" - integrity sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA== - dependencies: - "@jest/core" "^29.3.1" - "@jest/types" "^29.3.1" - import-local "^3.0.2" - jest-cli "^29.3.1" - -jest@29.5.0: - version "29.5.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.5.0.tgz#f75157622f5ce7ad53028f2f8888ab53e1f1f24e" - integrity sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ== +jest@29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== dependencies: - "@jest/core" "^29.5.0" - "@jest/types" "^29.5.0" + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" import-local "^3.0.2" - jest-cli "^29.5.0" + jest-cli "^29.7.0" jmespath@0.16.0: version "0.16.0" @@ -8139,7 +7705,7 @@ json5@^1.0.1, json5@^1.0.2: dependencies: minimist "^1.2.0" -json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: +json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -8431,7 +7997,7 @@ lodash.set@^4.3.2: lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== lodash@4.17.21, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" @@ -8921,13 +8487,19 @@ node-cleanup@^2.1.2: resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c" integrity sha1-esGavSl+Caf3KnFUXZUbUX5N3iw= -node-datachannel@0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/node-datachannel/-/node-datachannel-0.4.3.tgz#56a18cc62df4727f4483309b7b0bb86d5173ba27" - integrity sha512-I2SYzgqNd5gX8B+hQcff0qpGGwNiHZnXJNgsFyW0UXk1A3fbC/4L1PhSKGSuc7z0+Bk3raMN939E0KroJ5CJhA== +node-datachannel@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/node-datachannel/-/node-datachannel-0.5.1.tgz#55bf24c86ffb83ffa03a1d4363965598971e5a84" + integrity sha512-3hwCrBWJqYoozwVtJNzNtISLKwa3l/XTbrPFCyhC3KCgW1IvYMHhHm5FW37p0p2oth3J6MDwCw3T/0m7DTR7lw== dependencies: + node-domexception "^2.0.1" prebuild-install "^7.0.1" +node-domexception@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-2.0.1.tgz#83b0d101123b5bbf91018fd569a58b88ae985e5b" + integrity sha512-M85rnSC7WQ7wnfQTARPT4LrK7nwCHLdDFOCcItZMhTQjyCebJH8GciKqYJNgaOFZs9nFmTmd/VMyi3OW5jA47w== + node-fetch@^2.6.1: version "2.6.11" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" @@ -9389,16 +8961,16 @@ open@^8.4.0: is-wsl "^2.2.0" optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" ora@^5.4.1: version "5.4.1" @@ -9812,12 +9384,12 @@ pretty-format@^29.0.0, pretty-format@^29.3.1: ansi-styles "^5.0.0" react-is "^18.0.0" -pretty-format@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.1.tgz#ec838c288850b7c4f9090b867c2d4f4edbfb0f3e" - integrity sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog== +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== dependencies: - "@jest/schemas" "^29.6.0" + "@jest/schemas" "^29.6.3" ansi-styles "^5.0.0" react-is "^18.0.0" @@ -10187,11 +9759,6 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve.exports@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" - integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== - resolve.exports@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" @@ -10313,13 +9880,6 @@ semver@7.3.4: dependencies: lru-cache "^6.0.0" -semver@7.x, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -10337,6 +9897,13 @@ semver@^7.0.0, semver@^7.3.7: dependencies: lru-cache "^6.0.0" +semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + semver@^7.5.3: version "7.5.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" @@ -10866,9 +10433,9 @@ synchronous-promise@^2.0.13: integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg== table@^6.0.9: - version "6.7.3" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.3.tgz#255388439715a738391bd2ee4cbca89a4d05a9b7" - integrity sha512-5DkIxeA7XERBqMwJq0aHZOdMadBx4e6eDoFRuyT5VR82J0Ycg2DwM6GfA/EQAhJ+toRTaS1lIdSQCqgrmhPnlw== + version "6.8.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" + integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== dependencies: ajv "^8.0.1" lodash.truncate "^4.4.2" @@ -11050,18 +10617,18 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== -ts-jest@29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.3.tgz#63ea93c5401ab73595440733cefdba31fcf9cb77" - integrity sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ== +ts-jest@29.1.1: + version "29.1.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.1.tgz#f58fe62c63caf7bfcc5cc6472082f79180f0815b" + integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA== dependencies: bs-logger "0.x" fast-json-stable-stringify "2.x" jest-util "^29.0.0" - json5 "^2.2.1" + json5 "^2.2.3" lodash.memoize "4.x" make-error "1.x" - semver "7.x" + semver "^7.5.3" yargs-parser "^21.0.1" ts-node@10.9.1, ts-node@^10.9.1: @@ -11211,15 +10778,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc" - integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew== - -typescript@4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typescript@5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== "typescript@^3 || ^4": version "4.8.3" @@ -11349,11 +10911,16 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -v8-compile-cache@2.3.0, v8-compile-cache@^2.0.3: +v8-compile-cache@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +v8-compile-cache@^2.0.3: + version "2.4.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" + integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== + v8-to-istanbul@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" @@ -11487,11 +11054,6 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" -word-wrap@^1.2.3: - version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== - wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"