diff --git a/.eslintignore b/.eslintignore index 2a9cbeaf..4b979dfd 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,4 +8,5 @@ coverage benchmark .eslintrc.js dist/ -test/_setup.js \ No newline at end of file +test/_setup.js +data/ diff --git a/.prettierignore b/.prettierignore index b8e1c989..0c4b2471 100644 --- a/.prettierignore +++ b/.prettierignore @@ -46,5 +46,6 @@ browsertest.build/ *.SHA256 blockchain.db/* -# Output directory -output/ \ No newline at end of file +# Output and data directory +output/ +data/ diff --git a/docs/migration.md b/docs/migration.md index 969c0bc9..001dff13 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -76,18 +76,29 @@ The Lisk Migrator v2 also allows users to download and start the Lisk Core v4.x ``` USAGE -$ lisk-migrator [-d ] [-m ] [-c ] [-o ] [-p ] [-p ] [--snapshot-time-gap ] [--auto-migrate-config] [--auto-start-lisk-core-v4] [--use-existing-snapshot] - -FLAGS - -c, --config=config Custom configuration file path. - -d, --lisk-core-v3-data-path=lisk-core-v3-data-path Path where the lisk-core v3.x instance is running. Current home directory will be considered the default if not provided. - -h, --help Shows CLI help. - -o, --output=output File path to write the genesis block json. If not provided, it will default to cwd/genesis_block.json. - -p, --page-size Maximum number of blocks to be iterated at once for computation. Default to 100000. - -s, --snapshot-height=snapshot-height (Required) The height at which the re-genesis block will be generated. Can be specified with the SNAPSHOT_HEIGHT as well. - -v, --version Shows the CLI version. - --auto-migrate-config Migrate user configuration automatically. Default to false. - --auto-start-lisk-core-v4 Start lisk-core v4 automatically. Default to false. +$ lisk-migrator [-d ] [-m ] [-c ] [-o ] [-p ] [-p ] [--snapshot-time-gap ] [--auto-migrate-config] [--auto-start-lisk-core-v4] [--snapshot-path] [--network] + +OPTIONS + -c, --config=config Custom configuration file path for Lisk Core v3.1.x. + -d, --lisk-core-v3-data-path=lisk-core-v3-data-path Path where the Lisk Core v3.x instance is running. When not supplied, defaults to the default data directory for Lisk Core. + -h, --help show CLI help + -n, --network=(mainnet|testnet) Network to be considered for the migration. Depends on the '--snapshot-path' flag. + + -o, --output=output File path to write the genesis block. If not provided, it will default to cwd/output/{v3_networkIdentifier}/genesis_block.blob. Do not use any value starting with the default data path reserved for Lisk Core: '~/.lisk/lisk-core'. + + -p, --page-size=page-size [default: 100000] Maximum number of blocks to be iterated at once for computation. Defaults to 100000. + + -s, --snapshot-height=snapshot-height (required) The height at which re-genesis block will be generated. Can be specified with SNAPSHOT_HEIGHT as well. + + -v, --version show CLI version + + --auto-migrate-config Migrate user configuration automatically. Defaults to false. + + --auto-start-lisk-core-v4 Start Lisk Core v4 automatically. Defaults to false. When using this flag, kindly open another terminal window to stop Lisk Core v3.1.x for when the migrator prompts. + + --snapshot-path=snapshot-path Path to the state snapshot to run the migration offline. It could point either to a directory or a tarball (tar.gz). + + --snapshot-url=snapshot-url URL to download the state snapshot from. Use to run the migration offline. URL must end with tar.gz. EXAMPLES lisk-migrator --snapshot-height 20931763 --lisk-core-path /path/to/data-dir diff --git a/package.json b/package.json index fd64866f..07f451c9 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@oclif/config": "1.14.0", "@oclif/errors": "1.2.2", "@oclif/plugin-help": "2.2.3", + "axios": "0.25.0", "cli-ux": "5.5.1", "debug": "4.3.1", "fs-extra": "11.1.0", diff --git a/src/assets/pos.ts b/src/assets/pos.ts index 4671e2fb..239c0431 100644 --- a/src/assets/pos.ts +++ b/src/assets/pos.ts @@ -52,24 +52,6 @@ const ceiling = (a: number, b: number) => { return Math.floor((a + b - 1) / b); }; -export const formatInt = (num: number | bigint): string => { - let buf: Buffer; - if (typeof num === 'bigint') { - if (num < BigInt(0)) { - throw new Error('Negative number cannot be formatted'); - } - buf = Buffer.alloc(8); - buf.writeBigUInt64BE(num); - } else { - if (num < 0) { - throw new Error('Negative number cannot be formatted'); - } - buf = Buffer.alloc(4); - buf.writeUInt32BE(num, 0); - } - return buf.toString('binary'); -}; - export const getValidatorKeys = async ( accounts: Account[], db: Database, diff --git a/src/index.ts b/src/index.ts index 50922f41..633ce747 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ import { Database } from '@liskhq/lisk-db'; import * as semver from 'semver'; import { Command, flags as flagsParser } from '@oclif/command'; import cli from 'cli-ux'; -import { Block } from '@liskhq/lisk-chain'; +import { BlockHeader } from '@liskhq/lisk-chain'; import { NETWORK_CONSTANT, ROUND_LENGTH, @@ -30,6 +30,7 @@ import { FILE_NAME, LISK_V3_BACKUP_DATA_DIR, LEGACY_DB_PATH, + DEFAULT_DATA_DIR, } from './constants'; import { getAPIClient } from './client'; import { @@ -62,17 +63,20 @@ import { isLiskCoreV3Running, getLiskCoreStartCommand, } from './utils/node'; -import { resolveAbsolutePath, verifyOutputPath } from './utils/path'; +import { resolveAbsolutePath, verifyOutputPath, resolveSnapshotPath } from './utils/path'; import { execAsync } from './utils/process'; +import { getBlockHeaderByHeight } from './utils/block'; import { MigratorException } from './utils/exception'; import { writeCommandsToExec } from './utils/commands'; +import { getNetworkIdentifier } from './utils/network'; +import { extractTarBall } from './utils/fs'; +import { downloadAndExtract } from './utils/download'; let configCoreV4: PartialApplicationConfig; class LiskMigrator extends Command { public static description = 'Migrate Lisk Core to latest version'; public static flags = { - // add --version flag to show CLI version version: flagsParser.version({ char: 'v' }), help: flagsParser.help({ char: 'h' }), @@ -119,6 +123,30 @@ class LiskMigrator extends Command { description: 'Maximum number of blocks to be iterated at once for computation. Defaults to 100000.', }), + 'snapshot-path': flagsParser.string({ + required: false, + env: 'SNAPSHOT_PATH', + description: + 'Path to the state snapshot to run the migration offline. It could point either to a directory or a tarball (tar.gz).', + dependsOn: ['network'], + exclusive: ['snapshot-url'], + }), + 'snapshot-url': flagsParser.string({ + required: false, + env: 'SNAPSHOT_URL', + description: + 'URL to download the state snapshot from. Use to run the migration offline. URL must end with tar.gz.', + dependsOn: ['network'], + exclusive: ['snapshot-path'], + }), + network: flagsParser.enum({ + char: 'n', + required: false, + env: 'NETWORK', + description: + "Network to be considered for the migration. Depends on the '--snapshot-path' flag.", + options: ['mainnet', 'testnet'], + }), }; public async run(): Promise { @@ -132,87 +160,119 @@ class LiskMigrator extends Command { const autoMigrateUserConfig = flags['auto-migrate-config'] ?? false; const autoStartLiskCoreV4 = flags['auto-start-lisk-core-v4']; const pageSize = Number(flags['page-size']); - - verifyOutputPath(outputPath); - - const client = await getAPIClient(liskCoreV3DataPath); - const nodeInfo = (await client.node.getNodeInfo()) as NodeInfo; - const { version: appVersion, networkIdentifier } = nodeInfo; - - cli.action.start('Verifying if backup height from node config matches snapshot height'); - if (snapshotHeight !== nodeInfo.backup.height) { + const snapshotPath = flags['snapshot-path'] + ? resolveAbsolutePath(flags['snapshot-path']) + : (flags['snapshot-path'] as string); + const snapshotURL = flags['snapshot-url'] as string; + const network = flags.network as string; + const useSnapshot = !!(snapshotPath || snapshotURL); + + // Custom flag dependency check because neither exactlyOne or relationships properties are working for network + if (network && !useSnapshot) { this.error( - `Lisk Core v3 backup height (${nodeInfo.backup.height}) does not match the expected snapshot height (${snapshotHeight}).`, + 'Either --snapshot-path= or --snapshot-url= must be provided when using --network=', ); } - cli.action.stop('Snapshot height matches backup height'); - cli.action.start( - `Verifying snapshot height to be multiples of round length i.e ${ROUND_LENGTH}`, - ); - if (snapshotHeight % ROUND_LENGTH !== 0) { + if (snapshotURL && (!snapshotURL.startsWith('http') || !snapshotURL.endsWith('tar.gz'))) { this.error( - `Invalid snapshot height provided: ${snapshotHeight}. It must be an exact multiple of round length (${ROUND_LENGTH}).`, + `Expected --snapshot-url to begin with http(s) and end with 'tar.gz' instead received ${snapshotURL}.`, ); } - cli.action.stop('Snapshot height is valid'); - const networkConstant = NETWORK_CONSTANT[networkIdentifier] as NetworkConfigLocal; - const outputDir = flags.output ? outputPath : `${outputPath}/${networkIdentifier}`; + verifyOutputPath(outputPath); + + const networkIdentifier: string = await getNetworkIdentifier(network, liskCoreV3DataPath); + const networkConstant: NetworkConfigLocal = NETWORK_CONSTANT[networkIdentifier]; + const outputDir: string = flags.output ? outputPath : `${outputPath}/${networkIdentifier}`; // Ensure the output directory is present if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); const filePathCommandsToExec = `${outputDir}/${FILE_NAME.COMMANDS_TO_EXEC}`; + const dataDir = join(__dirname, '..', DEFAULT_DATA_DIR); try { - // Asynchronously capture the node's Forging Status information at the snapshot height - // This information is necessary for the node operators to enable generator post-migration without getting PoM'd - captureForgingStatusAtSnapshotHeight(this, client, snapshotHeight, outputDir); + if (useSnapshot) { + if (snapshotURL?.startsWith('http')) { + cli.action.start(`Downloading snapshot from ${snapshotURL} to ${outputDir}`); + await downloadAndExtract(snapshotURL, outputDir, dataDir); + cli.action.stop(`Successfully downloaded snapshot from ${snapshotURL} to ${outputDir}`); + } else if (snapshotPath?.endsWith('.tar.gz')) { + cli.action.start(`Extracting snapshot to ${dataDir}`); + await extractTarBall(snapshotPath, dataDir); + cli.action.stop(`Successfully extracted snapshot to ${dataDir}`); + } + } else { + const client = await getAPIClient(liskCoreV3DataPath); + const nodeInfo = (await client.node.getNodeInfo()) as NodeInfo; + const { version: appVersion } = nodeInfo; - if (autoStartLiskCoreV4) { - if (!networkConstant) { + cli.action.start('Verifying if backup height from node config matches snapshot height'); + if (snapshotHeight !== nodeInfo.backup.height) { this.error( - `Unknown network detected. No NETWORK_CONSTANT defined for networkID: ${networkIdentifier}.`, + `Lisk Core v3 backup height mismatch. Actual: ${nodeInfo.backup.height}, Expected: ${snapshotHeight}.`, ); } - } + cli.action.stop('Snapshot height matches backup height'); - cli.action.start('Verifying Lisk Core version'); - const isLiskCoreVersionValid = semver.valid(appVersion); - if (isLiskCoreVersionValid === null) { - this.error( - `Invalid Lisk Core version detected: ${appVersion}. Minimum supported version is ${MIN_SUPPORTED_LISK_CORE_VERSION}.`, + cli.action.start( + `Verifying snapshot height to be multiples of round length i.e ${ROUND_LENGTH}`, ); - } + if (snapshotHeight % ROUND_LENGTH !== 0) { + this.error( + `Invalid snapshot height provided: ${snapshotHeight}. It must be an exact multiple of round length (${ROUND_LENGTH}).`, + ); + } + cli.action.stop('Snapshot height is valid'); - // Using 'gt' instead of 'gte' because the behavior is swapped - // i.e. 'gt' acts as 'gte' and vice-versa - if (semver.gt(`${MIN_SUPPORTED_LISK_CORE_VERSION}-rc.0`, appVersion)) { - this.error( - `Lisk Migrator is not compatible with Lisk Core version ${appVersion}. Minimum supported version is ${MIN_SUPPORTED_LISK_CORE_VERSION}.`, - ); - } - cli.action.stop(`${appVersion} detected`); + // Asynchronously capture the node's Forging Status information at the snapshot height + // This information is necessary for the node operators to enable generator post-migration without getting PoM'd + captureForgingStatusAtSnapshotHeight(this, client, snapshotHeight, outputDir); + + if (autoStartLiskCoreV4) { + if (!networkConstant) { + this.error( + `Unknown network detected. No NETWORK_CONSTANT defined for networkID: ${networkIdentifier}.`, + ); + } + } + + cli.action.start('Verifying Lisk Core version'); + const isLiskCoreVersionValid = semver.valid(appVersion); + if (isLiskCoreVersionValid === null) { + this.error( + `Invalid Lisk Core version detected: ${appVersion}. Minimum supported version is ${MIN_SUPPORTED_LISK_CORE_VERSION}.`, + ); + } - // User specified custom config file - const configV3: ApplicationConfigV3 = customConfigPath - ? await getConfig(this, liskCoreV3DataPath, networkIdentifier, customConfigPath) - : await getConfig(this, liskCoreV3DataPath, networkIdentifier); + if (semver.lt(appVersion, MIN_SUPPORTED_LISK_CORE_VERSION)) { + this.error( + `Lisk Migrator is not compatible with Lisk Core version ${appVersion}. Minimum supported version is ${MIN_SUPPORTED_LISK_CORE_VERSION}.`, + ); + } + cli.action.stop(`${appVersion} detected`); + + await observeChainHeight({ + label: 'Waiting for snapshot height to be finalized', + liskCoreV3DataPath, + height: snapshotHeight, + delay: 500, + isFinal: true, + }); + } await setTokenIDLskByNetID(networkIdentifier); await setPrevSnapshotBlockHeightByNetID(networkIdentifier); - await observeChainHeight({ - label: 'Waiting for snapshot height to be finalized', - liskCoreV3DataPath, - height: snapshotHeight, - delay: 500, - isFinal: true, - }); - // Create new DB instance based on the snapshot path cli.action.start('Creating database instance'); - const snapshotDirPath = join(liskCoreV3DataPath, SNAPSHOT_DIR); + const snapshotDirPath = await resolveSnapshotPath( + useSnapshot, + snapshotPath, + dataDir, + liskCoreV3DataPath, + ); + const db = new Database(snapshotDirPath); cli.action.stop(); @@ -231,7 +291,12 @@ class LiskMigrator extends Command { await writeGenesisAssets(genesisAssets, outputDir); cli.action.stop(); - if (autoMigrateUserConfig) { + if (autoMigrateUserConfig && !useSnapshot) { + // User specified custom config file + const configV3: ApplicationConfigV3 = customConfigPath + ? await getConfig(this, liskCoreV3DataPath, networkIdentifier, customConfigPath) + : await getConfig(this, liskCoreV3DataPath, networkIdentifier); + cli.action.start('Creating backup for old config'); await createBackup(configV3); cli.action.stop(); @@ -268,15 +333,16 @@ class LiskMigrator extends Command { cli.action.stop(); cli.action.start('Creating genesis block'); - const blockAtSnapshotHeight = ((await client.block.getByHeight( + const blockHeaderAtSnapshotHeight = (await getBlockHeaderByHeight( + db, snapshotHeight, - )) as unknown) as Block; + )) as BlockHeader; await createGenesisBlock( this, networkConstant.name, defaultConfigFilePath, outputDir, - blockAtSnapshotHeight, + blockHeaderAtSnapshotHeight, ); cli.action.stop(); @@ -285,94 +351,96 @@ class LiskMigrator extends Command { this.log(`Genesis block tar and SHA256 files have been created at: ${outputDir}.`); cli.action.stop(); - if (autoStartLiskCoreV4) { - try { - if (!autoMigrateUserConfig) { - configCoreV4 = defaultConfigV4; - } + if (!useSnapshot) { + if (autoStartLiskCoreV4) { + try { + if (!autoMigrateUserConfig) { + configCoreV4 = defaultConfigV4; + } - cli.action.start('Copying genesis block to the Lisk Core executable directory'); - const liskCoreExecPath = await execAsync('which lisk-core'); - const liskCoreV4ConfigPath = resolve( - liskCoreExecPath, - '../..', - `lib/node_modules/lisk-core/config/${networkConstant.name}`, - ); + cli.action.start('Copying genesis block to the Lisk Core executable directory'); + const liskCoreExecPath = await execAsync('which lisk-core'); + const liskCoreV4ConfigPath = resolve( + liskCoreExecPath, + '../..', + `lib/node_modules/lisk-core/config/${networkConstant.name}`, + ); - await copyGenesisBlock( - `${outputDir}/genesis_block.blob`, - `${liskCoreV4ConfigPath}/genesis_block.blob`, - ); - this.log(`Genesis block has been copied to: ${liskCoreV4ConfigPath}.`); - cli.action.stop(); + await copyGenesisBlock( + `${outputDir}/genesis_block.blob`, + `${liskCoreV4ConfigPath}/genesis_block.blob`, + ); + this.log(`Genesis block has been copied to: ${liskCoreV4ConfigPath}.`); + cli.action.stop(); - // Ask user to manually stop Lisk Core v3 and continue - const isLiskCoreV3Stopped = await cli.confirm( - "Please stop Lisk Core v3 to continue. Type 'yes' and press Enter when ready. [yes/no]", - ); + // Ask user to manually stop Lisk Core v3 and continue + const isLiskCoreV3Stopped = await cli.confirm( + "Please stop Lisk Core v3 to continue. Type 'yes' and press Enter when ready. [yes/no]", + ); - if (isLiskCoreV3Stopped) { - let numTriesLeft = 3; - while (numTriesLeft) { - numTriesLeft -= 1; + if (isLiskCoreV3Stopped) { + let numTriesLeft = 3; + while (numTriesLeft) { + numTriesLeft -= 1; - const isCoreV3Running = await isLiskCoreV3Running(liskCoreV3DataPath); - if (!isCoreV3Running) break; + const isCoreV3Running = await isLiskCoreV3Running(liskCoreV3DataPath); + if (!isCoreV3Running) break; - if (numTriesLeft >= 0) { - const isStopReconfirmed = await cli.confirm( - "Lisk Core v3 still running. Please stop the node, type 'yes' to proceed and 'no' to exit. [yes/no]", - ); - if (!isStopReconfirmed) { - throw new Error( - `Cannot proceed with Lisk Core v4 auto-start. Please continue manually. In order to access legacy blockchain information posts-migration, please copy the contents of the ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/). Exiting!!!`, + if (numTriesLeft >= 0) { + const isStopReconfirmed = await cli.confirm( + "Lisk Core v3 still running. Please stop the node, type 'yes' to proceed and 'no' to exit. [yes/no]", ); - } else if (numTriesLeft === 0 && isStopReconfirmed) { - const isCoreV3StillRunning = await isLiskCoreV3Running(liskCoreV3DataPath); - if (isCoreV3StillRunning) { + if (!isStopReconfirmed) { throw new Error( - `Cannot auto-start Lisk Core v4 as Lisk Core v3 is still running. Please continue manually. In order to access legacy blockchain information posts-migration, please copy the contents of the ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/). Exiting!!!`, + `Cannot proceed with Lisk Core v4 auto-start. Please continue manually. In order to access legacy blockchain information posts-migration, please copy the contents of the ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/). Exiting!!!`, ); + } else if (numTriesLeft === 0 && isStopReconfirmed) { + const isCoreV3StillRunning = await isLiskCoreV3Running(liskCoreV3DataPath); + if (isCoreV3StillRunning) { + throw new Error( + `Cannot auto-start Lisk Core v4 as Lisk Core v3 is still running. Please continue manually. In order to access legacy blockchain information posts-migration, please copy the contents of the ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/). Exiting!!!`, + ); + } } } } - } - const isUserConfirmed = await cli.confirm( - `Start Lisk Core with the following configuration? [yes/no] + const isUserConfirmed = await cli.confirm( + `Start Lisk Core with the following configuration? [yes/no] ${util.inspect(configCoreV4, false, 3)} `, - ); - - if (isUserConfirmed) { - cli.action.start('Starting Lisk Core v4'); - const network = networkConstant.name as string; - await startLiskCore(this, liskCoreV3DataPath, configCoreV4, network, outputDir); - this.log( - `Started Lisk Core v4 at default data directory ('${DEFAULT_LISK_CORE_PATH}').`, ); - cli.action.stop(); + + if (isUserConfirmed) { + cli.action.start('Starting Lisk Core v4'); + const networkName = networkConstant.name as string; + await startLiskCore(this, liskCoreV3DataPath, configCoreV4, networkName, outputDir); + this.log( + `Started Lisk Core v4 at default data directory ('${DEFAULT_LISK_CORE_PATH}').`, + ); + cli.action.stop(); + } else { + this.log( + 'User did not accept the migrated config. Skipping the Lisk Core v4 auto-start process.', + ); + } } else { - this.log( - 'User did not accept the migrated config. Skipping the Lisk Core v4 auto-start process.', + throw new Error( + `User did not confirm Lisk Core v3 node shutdown. Skipping the Lisk Core v4 auto-start process. Please continue manually. In order to access legacy blockchain information posts-migration, please copy the contents of the ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/). Exiting!!!`, ); } - } else { - throw new Error( - `User did not confirm Lisk Core v3 node shutdown. Skipping the Lisk Core v4 auto-start process. Please continue manually. In order to access legacy blockchain information posts-migration, please copy the contents of the ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/). Exiting!!!`, + } catch (err) { + const errorMsg = `Failed to auto-start Lisk Core v4.\nError: ${(err as Error).message}`; + throw new MigratorException( + errorMsg, + err instanceof MigratorException ? err.code : ERROR_CODE.LISK_CORE_START, ); } - } catch (err) { - const errorMsg = `Failed to auto-start Lisk Core v4.\nError: ${(err as Error).message}`; - throw new MigratorException( - errorMsg, - err instanceof MigratorException ? err.code : ERROR_CODE.LISK_CORE_START, + } else { + this.log( + `Please copy the contents of ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/) in order to access legacy blockchain information.`, ); + this.log('Please copy genesis block to the Lisk Core V4 network directory.'); } - } else { - this.log( - `Please copy the contents of ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/) in order to access legacy blockchain information.`, - ); - this.log('Please copy genesis block to the Lisk Core V4 network directory.'); } } catch (error) { const commandsToExecute: string[] = []; diff --git a/src/types.ts b/src/types.ts index 6b8515c6..a85c8903 100644 --- a/src/types.ts +++ b/src/types.ts @@ -398,3 +398,9 @@ export interface ForgingStatus { readonly maxHeightPrevoted?: number; readonly maxHeightPreviouslyForged?: number; } + +export interface FileInfo { + readonly fileName: string; + readonly fileDir: string; + readonly filePath: string; +} diff --git a/src/utils/block.ts b/src/utils/block.ts index 1a167e4b..24b9a8e3 100644 --- a/src/utils/block.ts +++ b/src/utils/block.ts @@ -15,9 +15,10 @@ import { codec, Schema } from '@liskhq/lisk-codec'; import { Database } from '@liskhq/lisk-db'; import { BlockHeader } from '@liskhq/lisk-chain'; -import { DB_KEY_BLOCKS_ID } from '../constants'; +import { DB_KEY_BLOCKS_HEIGHT, DB_KEY_BLOCKS_ID } from '../constants'; import { blockHeaderSchema } from '../schemas'; import { keyString, incrementOne } from './transaction'; +import { formatInt } from './number'; export const getDataFromDBStream = async (stream: NodeJS.ReadableStream, schema: Schema) => { const data = await new Promise[]>((resolve, reject) => { @@ -75,3 +76,14 @@ export const getBlockPublicKeySet = async ( } return result; }; + +export const getBlockHeaderByHeight = async ( + db: Database, + height: number, +): Promise => { + const stringHeight = formatInt(height); + const id = await db.get(Buffer.from(`${DB_KEY_BLOCKS_HEIGHT}:${stringHeight}`)); + const blockHeaderBuffer = await db.get(Buffer.from(`${DB_KEY_BLOCKS_ID}:${keyString(id)}`)); + const blockHeader: BlockHeader = codec.decode(blockHeaderSchema, blockHeaderBuffer); + return { ...blockHeader, id }; +}; diff --git a/src/utils/download.ts b/src/utils/download.ts new file mode 100644 index 00000000..73bc7f7d --- /dev/null +++ b/src/utils/download.ts @@ -0,0 +1,104 @@ +/* + * LiskHQ/lisk-commander + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + * + */ +import * as crypto from 'crypto'; +import * as axios from 'axios'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { extractTarBall } from './fs'; +import { FileInfo } from '../types'; + +export const getDownloadedFileInfo = (url: string, downloadDir: string): FileInfo => { + const pathWithoutProtocol = url.replace(/(^\w+:|^)\/\//, '').split('/'); + const fileName = pathWithoutProtocol.pop() as string; + const filePath = path.join(downloadDir, fileName); + + return { + fileName, + fileDir: downloadDir, + filePath, + }; +}; + +export const download = async (url: string, dir: string): Promise => { + const { filePath, fileDir } = getDownloadedFileInfo(url, dir); + + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + + fs.ensureDirSync(fileDir); + const writeStream = fs.createWriteStream(filePath); + const response = await axios.default({ + url, + method: 'GET', + responseType: 'stream', + maxContentLength: 5000, + }); + + response.data.pipe(writeStream); + + return new Promise((resolve, reject) => { + writeStream.on('finish', resolve); + writeStream.on('error', reject); + }); +}; + +export const verifyChecksum = async (filePath: string, expectedChecksum: string): Promise => { + const fileStream = fs.createReadStream(filePath); + const dataHash = crypto.createHash('sha256'); + const fileHash = await new Promise((resolve, reject) => { + fileStream.on('data', (datum: Buffer) => { + dataHash.update(datum); + }); + fileStream.on('error', error => { + reject(error); + }); + fileStream.on('end', () => { + resolve(dataHash.digest()); + }); + }); + + const fileChecksum = fileHash.toString('hex'); + if (fileChecksum !== expectedChecksum) { + throw new Error( + `File checksum: ${fileChecksum} mismatched with expected checksum: ${expectedChecksum}`, + ); + } +}; + +export const getChecksum = (url: string, dir: string): string => { + const { filePath } = getDownloadedFileInfo(url, dir); + const content = fs.readFileSync(`${filePath}.SHA256`, 'utf8'); + + if (!content) { + throw new Error(`Invalid filepath: ${filePath}`); + } + + return content.split(' ')[0]; +}; + +export const downloadAndExtract = async ( + url: string, + downloadPath: string, + extractionPath: string, +): Promise => { + await download(url, downloadPath); + await download(`${url}.SHA256`, downloadPath); + const { filePath } = getDownloadedFileInfo(url, downloadPath); + const checksum = getChecksum(url, downloadPath); + await verifyChecksum(filePath, checksum); + await extractTarBall(filePath, extractionPath); +}; diff --git a/src/utils/fs.ts b/src/utils/fs.ts index 1dec7b52..7e3f6fd4 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -110,3 +110,13 @@ export const createTarball = async (filePath: string, outputDir: string) => .then(() => resolve(true)) .catch(err => reject(err)); }); + +export const getFiles = async (directoryPath: string, options = {}): Promise => + new Promise((resolve, reject) => { + fs.readdir(directoryPath, options, (err, files) => { + if (err) { + return reject(err); + } + return resolve(files as string[]); + }); + }); diff --git a/src/utils/genesis_block.ts b/src/utils/genesis_block.ts index 085d5fef..f5167264 100644 --- a/src/utils/genesis_block.ts +++ b/src/utils/genesis_block.ts @@ -15,7 +15,8 @@ import * as crypto from 'crypto'; import * as fs from 'fs-extra'; import path from 'path'; import { Command } from '@oclif/command'; -import { Block as BlockVersion3 } from '@liskhq/lisk-chain'; + +import { BlockHeader } from '@liskhq/lisk-chain'; import { ERROR_CODE, SNAPSHOT_TIME_GAP } from '../constants'; import { GenesisAssetEntry } from '../types'; import { execAsync } from './process'; @@ -58,12 +59,12 @@ export const createGenesisBlock = async ( network: string, configFilepath: string, outputDir: string, - blockAtSnapshotHeight: BlockVersion3, + blockHeaderAtSnapshotHeight: BlockHeader, ) => { try { - const height = blockAtSnapshotHeight.header.height + 1; - const timestamp = blockAtSnapshotHeight.header.timestamp + SNAPSHOT_TIME_GAP; - const previousBlockID = blockAtSnapshotHeight.header.id.toString('hex'); + const height = blockHeaderAtSnapshotHeight.height + 1; + const timestamp = blockHeaderAtSnapshotHeight.timestamp + SNAPSHOT_TIME_GAP; + const previousBlockID = blockHeaderAtSnapshotHeight.id.toString('hex'); genesisBlockCreateCommand = `lisk-core genesis-block:create --network ${network} --config=${configFilepath} --output=${outputDir} --assets-file=${outputDir}/genesis_assets.json --height=${height} --previous-block-id=${previousBlockID} --timestamp=${timestamp} --export-json`; _this.log( diff --git a/src/utils/network.ts b/src/utils/network.ts index c4e6af53..21ecd6e3 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -13,7 +13,9 @@ */ import { createServer } from 'net'; -import { Port } from '../types'; +import { NetworkConfigLocal, NodeInfo, Port } from '../types'; +import { getAPIClient } from '../client'; +import { NETWORK_CONSTANT } from '../constants'; export const isPortAvailable = async (port: Port): Promise => new Promise((resolve, reject) => { @@ -35,3 +37,25 @@ export const isPortAvailable = async (port: Port): Promise => server.listen(port); }); + +export const getNetworkIdentifier = async ( + network: string, + liskCoreV3DataPath: string, +): Promise => { + const networkID = network + ? (() => { + const networkConstantEntry = Object.entries(NETWORK_CONSTANT).find( + ([, v]) => v.name === network, + ) as [string, NetworkConfigLocal]; + + const [networkIdentifier] = networkConstantEntry; + return networkIdentifier; + })() + : await (async () => { + const client = await getAPIClient(liskCoreV3DataPath); + const nodeInfo = (await client.node.getNodeInfo()) as NodeInfo; + return nodeInfo.networkIdentifier; + })(); + + return networkID; +}; diff --git a/src/utils/node.ts b/src/utils/node.ts index 9540b8da..533ad19b 100644 --- a/src/utils/node.ts +++ b/src/utils/node.ts @@ -146,7 +146,7 @@ const resolveLiskCoreStartCommand = async (_this: Command, network: string, conf 'Would you like to customize the Lisk Core v4 start command? [yes/no]', ); - const baseStartCommand = `lisk core start --network ${network}`; + const baseStartCommand = `lisk-core start --network ${network}`; if (!isUserConfirmed) { const defaultStartCommand = `${baseStartCommand} --config ${configPath}/config.json`; diff --git a/src/utils/number.ts b/src/utils/number.ts new file mode 100644 index 00000000..e5227efb --- /dev/null +++ b/src/utils/number.ts @@ -0,0 +1,30 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +export const formatInt = (num: number | bigint): string => { + let buf: Buffer; + if (typeof num === 'bigint') { + if (num < BigInt(0)) { + throw new Error('Negative number cannot be formatted'); + } + buf = Buffer.alloc(8); + buf.writeBigUInt64BE(num); + } else { + if (num < 0) { + throw new Error('Negative number cannot be formatted'); + } + buf = Buffer.alloc(4); + buf.writeUInt32BE(num, 0); + } + return buf.toString('binary'); +}; diff --git a/src/utils/path.ts b/src/utils/path.ts index 55ee471b..5fc385b3 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -13,7 +13,8 @@ */ import { homedir } from 'os'; import { isAbsolute, join } from 'path'; -import { DEFAULT_LISK_CORE_PATH } from '../constants'; +import { getFiles } from './fs'; +import { DEFAULT_LISK_CORE_PATH, SNAPSHOT_DIR } from '../constants'; export const resolveAbsolutePath = (path: string) => { if (isAbsolute(path)) { @@ -37,3 +38,17 @@ export const verifyOutputPath = (_outputPath: string): void | Error => { ); } }; + +export const resolveSnapshotPath = async ( + useSnapshot: boolean, + snapshotPath: string, + dataDir: string, + liskCoreV3DataPath: string, +) => { + if (!useSnapshot) return join(liskCoreV3DataPath, SNAPSHOT_DIR); + if (snapshotPath && !snapshotPath.endsWith('.tar.gz')) return snapshotPath; + + const [snapshotDirNameExtracted] = (await getFiles(dataDir)) as string[]; + const snapshotFilePathExtracted = join(dataDir, snapshotDirNameExtracted); + return snapshotFilePathExtracted; +}; diff --git a/test/unit/assets/pos.spec.ts b/test/unit/assets/pos.spec.ts index 08f98491..46fb538c 100644 --- a/test/unit/assets/pos.spec.ts +++ b/test/unit/assets/pos.spec.ts @@ -36,7 +36,6 @@ import { createStakersArrayEntry, getStakes, getPoSModuleEntry, - formatInt, } from '../../../src/assets/pos'; import { MODULE_NAME_POS } from '../../../src/constants'; @@ -309,18 +308,3 @@ describe('Build assets/pos', () => { ]); }); }); - -describe('Test formatInt method', () => { - it('should return formatted result when called with valid BigInt', async () => { - const formattedResult = formatInt(BigInt(100)); - await expect(typeof formattedResult).toBe('string'); - }); - - it('should throw error when called with negative number', async () => { - await expect(() => formatInt(-1)).toThrow(); - }); - - it('should throw error when called with negative BigInteger', async () => { - await expect(() => formatInt(BigInt(-1))).toThrow(); - }); -}); diff --git a/test/unit/createAsset.spec.ts b/test/unit/createAsset.spec.ts index b09330d4..4b9dfeab 100644 --- a/test/unit/createAsset.spec.ts +++ b/test/unit/createAsset.spec.ts @@ -44,7 +44,7 @@ import { GenesisAssetEntry, VoteWeightsWrapper, } from '../../src/types'; -import { formatInt } from '../../src/assets/pos'; +import { formatInt } from '../../src/utils/number'; const { hash } = utils; const { getKeys } = legacy; diff --git a/test/unit/fixtures/data/snapshotDir/blockchain.db b/test/unit/fixtures/data/snapshotDir/blockchain.db new file mode 100644 index 00000000..39b03ad6 Binary files /dev/null and b/test/unit/fixtures/data/snapshotDir/blockchain.db differ diff --git a/test/unit/utils/block.spec.ts b/test/unit/utils/block.spec.ts new file mode 100644 index 00000000..01f54985 --- /dev/null +++ b/test/unit/utils/block.spec.ts @@ -0,0 +1,74 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + * + */ +import { when } from 'jest-when'; + +import { Database } from '@liskhq/lisk-db'; +import { codec } from '@liskhq/lisk-codec'; +import { blockHeaderAssetSchema, blockHeaderSchema } from '@liskhq/lisk-chain'; + +import { generateBlocks } from './blocks'; +import { getBlockHeaderByHeight } from '../../../src/utils/block'; +import { formatInt } from '../../../src/utils/number'; +import { DB_KEY_BLOCKS_HEIGHT, DB_KEY_BLOCKS_ID } from '../../../src/constants'; +import { keyString } from '../../../src/utils/transaction'; + +jest.mock('@liskhq/lisk-db'); + +describe('Test getBlockHeaderByHeight method', () => { + let db: any; + let blockHeader: Buffer; + let blockID: Buffer; + + beforeAll(async () => { + db = new Database('testDB'); + const [block] = generateBlocks({ + startHeight: 150, + numberOfBlocks: 1, + }); + + const blockAssetBuffer = codec.encode(blockHeaderAssetSchema, block.header.asset); + blockHeader = codec.encode(blockHeaderSchema, { + ...block.header, + asset: blockAssetBuffer, + }); + blockID = block.header.id; + }); + + it('should return block header when called with valid height', async () => { + const blockHeight = 150; + + when(db.get) + .calledWith(Buffer.from(`${DB_KEY_BLOCKS_HEIGHT}:${formatInt(blockHeight)}`)) + .mockResolvedValue(blockID as never); + + when(db.get) + .calledWith(Buffer.from(`${DB_KEY_BLOCKS_ID}:${keyString(blockID)}`)) + .mockResolvedValue(blockHeader as never); + + const block = await getBlockHeaderByHeight(db, blockHeight); + expect(Object.getOwnPropertyNames(block)).toEqual([ + 'version', + 'timestamp', + 'height', + 'previousBlockID', + 'transactionRoot', + 'generatorPublicKey', + 'reward', + 'asset', + 'signature', + 'id', + ]); + }); +}); diff --git a/test/unit/utils/fs.spec.ts b/test/unit/utils/fs.spec.ts index 422626e0..1df4388e 100644 --- a/test/unit/utils/fs.spec.ts +++ b/test/unit/utils/fs.spec.ts @@ -23,6 +23,7 @@ import { write, copyFile, read, + getFiles, } from '../../../src/utils/fs'; import { configV3 } from '../fixtures/config'; @@ -143,3 +144,26 @@ describe('Test createTarball method', () => { await expect(createTarball('', '')).rejects.toThrow(); }); }); + +describe('Test getFiles method', () => { + it('should get files when getFiles() method is called', async () => { + const directoryPath = join(__dirname, '../fixtures'); + const files = await getFiles(directoryPath); + expect(files).toEqual( + expect.arrayContaining([ + 'blockchain.db.tar.gz', + 'config.ts', + 'customConfig.json', + 'forgingStatus.json', + 'genesis_assets.ts', + 'genesis_block.json', + 'lisk-core', + 'sub-directory', + ]), + ); + }); + + it('should throw when called with empty string', async () => { + await expect(getFiles('')).rejects.toThrow(); + }); +}); diff --git a/test/unit/utils/network.spec.ts b/test/unit/utils/network.spec.ts new file mode 100644 index 00000000..e0df6942 --- /dev/null +++ b/test/unit/utils/network.spec.ts @@ -0,0 +1,54 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + * + */ +import { resolve } from 'path'; +import { NETWORK_CONSTANT } from '../../../src/constants'; +import { NetworkConfigLocal } from '../../../src/types'; + +const clientFilePath = resolve(`${__dirname}/../../../src/client`); + +afterEach(() => { + jest.clearAllMocks(); + jest.resetModules(); +}); + +describe('Test getNetworkIdentifier method', () => { + const network = 'mainnet'; + const mainnetNetworkIdentifier = (Object.entries(NETWORK_CONSTANT).find( + ([, v]) => v.name === network, + ) as [string, NetworkConfigLocal])[0]; + + it('should return networkIdentifier when use-snapshot is true', async () => { + /* eslint-disable-next-line global-require, @typescript-eslint/no-var-requires */ + const { getNetworkIdentifier } = require('../../../src/utils/network'); + const networkIdentifier = await getNetworkIdentifier(network, './lisk/lisk-core'); + expect(networkIdentifier).toBe(mainnetNetworkIdentifier); + }); + + it('should return networkIdentifier when use-snapshot is false', async () => { + jest.mock(clientFilePath, () => ({ + getAPIClient: jest.fn().mockResolvedValueOnce({ + node: { + getNodeInfo: jest.fn().mockReturnValue({ networkIdentifier: mainnetNetworkIdentifier }), + }, + }), + })); + + /* eslint-disable-next-line global-require, @typescript-eslint/no-var-requires */ + const { getNetworkIdentifier } = require('../../../src/utils/network'); + + const networkIdentifier = await getNetworkIdentifier(null, './lisk/lisk-core'); + expect(networkIdentifier).toBe(mainnetNetworkIdentifier); + }); +}); diff --git a/test/unit/utils/number.spec.ts b/test/unit/utils/number.spec.ts new file mode 100644 index 00000000..0de94005 --- /dev/null +++ b/test/unit/utils/number.spec.ts @@ -0,0 +1,30 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + * + */ +import { formatInt } from '../../../src/utils/number'; + +describe('Test formatInt method', () => { + it('should return formatted result when called with valid BigInt', async () => { + const formattedResult = formatInt(BigInt(100)); + await expect(typeof formattedResult).toBe('string'); + }); + + it('should throw error when called with negative number', async () => { + await expect(() => formatInt(-1)).toThrow(); + }); + + it('should throw error when called with negative BigInteger', async () => { + await expect(() => formatInt(BigInt(-1))).toThrow(); + }); +}); diff --git a/test/unit/utils/path.spec.ts b/test/unit/utils/path.spec.ts index 4c96031a..6ff539cd 100644 --- a/test/unit/utils/path.spec.ts +++ b/test/unit/utils/path.spec.ts @@ -16,7 +16,11 @@ import { homedir } from 'os'; import { join } from 'path'; import { DEFAULT_LISK_CORE_PATH } from '../../../src/constants'; -import { resolveAbsolutePath, verifyOutputPath } from '../../../src/utils/path'; +import { + resolveAbsolutePath, + verifyOutputPath, + resolveSnapshotPath, +} from '../../../src/utils/path'; describe('Test resolveAbsolutePath method', () => { it('should resolve absolute path when called with valid path which contains ~', async () => { @@ -73,3 +77,44 @@ describe('Test verifyOutputPath method', () => { expect(() => verifyOutputPath(outputPath)).not.toThrow(); }); }); + +describe('Test resolveSnapshotPath method', () => { + const dataDir = join(__dirname, '../../..', 'test/unit/fixtures/data'); + const liskCoreV3DataPath = '~/.lisk/lisk-core/config/data'; + const inputSnapshotPath = join(__dirname, '../../..', 'test/unit/fixtures/data/snapshotDir'); + + it('should return valid snapshot path when useSnapshot is false', async () => { + const useSnapshot = false; + const snapshotPath = await resolveSnapshotPath( + useSnapshot, + inputSnapshotPath, + dataDir, + liskCoreV3DataPath, + ); + const expectedResult = '~/.lisk/lisk-core/config/data/data/backup'; + expect(snapshotPath).toBe(expectedResult); + }); + + it('should return valid snapshot path when useSnapshot is true and snapshotPath is available', async () => { + const useSnapshot = true; + const snapshotPath = await resolveSnapshotPath( + useSnapshot, + inputSnapshotPath, + dataDir, + liskCoreV3DataPath, + ); + expect(snapshotPath).toBe(inputSnapshotPath); + }); + + it('should return valid snapshot path when useSnapshot is true and snapshotPath ends with .tar,gz', async () => { + const useSnapshot = true; + const snapshotPath = await resolveSnapshotPath( + useSnapshot, + (undefined as unknown) as string, + dataDir, + liskCoreV3DataPath, + ); + const expectedResult = join(dataDir, 'snapshotDir'); + expect(snapshotPath).toBe(expectedResult); + }); +}); diff --git a/yarn.lock b/yarn.lock index 145a6983..0c9c234a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1223,9 +1223,9 @@ integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== "@types/node@*": - version "20.8.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.7.tgz#ad23827850843de973096edfc5abc9e922492a25" - integrity sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ== + version "20.8.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.8.tgz#adee050b422061ad5255fc38ff71b2bb96ea2a0e" + integrity sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ== dependencies: undici-types "~5.25.1" @@ -1848,6 +1848,13 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +axios@0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" + integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== + dependencies: + follow-redirects "^1.14.7" + babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" @@ -2655,9 +2662,9 @@ ed2curve@0.3.0: tweetnacl "1.x.x" electron-to-chromium@^1.4.535: - version "1.4.564" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.564.tgz#9c6ada8ec7b43c65d8629300a0916a346ac5c0c2" - integrity sha512-bGAx9+teIzL5I4esQwCMtiXtb78Ysc8xOKTPOvmafbJZ4SQ40kDO1ym3yRcGSkfaBtV81fGgHOgPoe6DsmpmkA== + version "1.4.565" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.565.tgz#205f3746a759ec3c43bce98b9eef5445f2721ea9" + integrity sha512-XbMoT6yIvg2xzcbs5hCADi0dXBh4//En3oFXmtPX+jiyyiCTiM9DGFT2SLottjpEs9Z8Mh8SqahbR96MaHfuSg== emittery@^0.8.1: version "0.8.1" @@ -3304,6 +3311,11 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== +follow-redirects@^1.14.7: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"