diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index fc1de8711a..eea245c4c5 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "1.10.0", + "version": "1.11.0", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -62,7 +62,7 @@ "@aws-sdk/client-secrets-manager": "3", "@aws-sdk/s3-request-presigner": "3", "@ironfish/rust-nodejs": "1.9.0", - "@ironfish/sdk": "1.10.0", + "@ironfish/sdk": "1.11.0", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", @@ -103,4 +103,4 @@ "url": "https://github.com/iron-fish/ironfish/issues" }, "homepage": "https://ironfish.network" -} \ No newline at end of file +} diff --git a/ironfish-cli/src/commands/chain/download.ts b/ironfish-cli/src/commands/chain/download.ts index 4acd391c77..8b5b59898b 100644 --- a/ironfish-cli/src/commands/chain/download.ts +++ b/ironfish-cli/src/commands/chain/download.ts @@ -1,36 +1,19 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { - ErrorUtils, - FileUtils, - Meter, - NodeUtils, - TimeUtils, - VERSION_DATABASE_CHAIN, -} from '@ironfish/sdk' +import { ErrorUtils, FileUtils, Meter, NodeUtils, TimeUtils } from '@ironfish/sdk' import { CliUx, Flags } from '@oclif/core' -import axios from 'axios' -import crypto from 'crypto' -import fs from 'fs' import fsAsync from 'fs/promises' -import { IncomingMessage } from 'http' -import path from 'path' -import tar from 'tar' import { IronfishCommand } from '../../command' import { LocalFlags } from '../../flags' -import { SnapshotManifest } from '../../snapshot' +import { DownloadedSnapshot, getDefaultManifestUrl, SnapshotDownloader } from '../../snapshot' import { ProgressBar } from '../../types' -import { UrlUtils } from '../../utils/url' export default class Download extends IronfishCommand { static hidden = false static description = `Download and import a chain snapshot` - static defaultMainnetManifestUrl = `https://snapshots.ironfish.network/manifest.json` - static defaultTestnetManifestUrl = `https://testnet.snapshots.ironfish.network/manifest.json` - static flags = { ...LocalFlags, manifestUrl: Flags.string({ @@ -44,11 +27,11 @@ export default class Download extends IronfishCommand { required: false, description: 'Path to a downloaded snapshot file to import', }), - outputPath: Flags.string({ + output: Flags.string({ char: 'o', parse: (input: string) => Promise.resolve(input.trim()), required: false, - description: 'Output path to download the snapshot file to', + description: 'Output folder to download the snapshot file to', }), confirm: Flags.boolean({ default: false, @@ -67,49 +50,37 @@ export default class Download extends IronfishCommand { const node = await this.sdk.node() await NodeUtils.waitForOpen(node) + const nodeChainDBVersion = await node.chain.blockchainDb.getVersion() + const headSequence = node.chain.head.sequence + await node.closeDB() - const networkId = node.internal.get('networkId') - - let manifestUrl = '' - if (flags.manifestUrl) { - manifestUrl = flags.manifestUrl + let downloadedSnapshot: DownloadedSnapshot + if (flags.path) { + downloadedSnapshot = new DownloadedSnapshot(this.sdk, flags.path) } else { - if (networkId === 0) { - // testnet - manifestUrl = Download.defaultTestnetManifestUrl - } else if (networkId === 1) { - manifestUrl = Download.defaultMainnetManifestUrl - } else { + const networkId = this.sdk.internal.get('networkId') + const manifestUrl = flags.manifestUrl ?? getDefaultManifestUrl(networkId) + if (!manifestUrl) { this.log(`Manifest url for the snapshots are not available for network ID ${networkId}`) - this.exit(1) + return this.exit(1) } - } - - let snapshotPath: string - if (flags.path) { - snapshotPath = this.sdk.fileSystem.resolve(flags.path) - } else { - if (!manifestUrl || manifestUrl === '') { - this.log(`Cannot download snapshot without manifest URL`) - this.exit(1) + let dest = flags.output + if (!dest) { + await fsAsync.mkdir(this.sdk.config.tempDir, { recursive: true }) + dest = this.sdk.config.tempDir } - const manifest = (await axios.get(manifestUrl)).data + const Downloader = new SnapshotDownloader(manifestUrl, dest, nodeChainDBVersion) - if (manifest.database_version > VERSION_DATABASE_CHAIN) { - this.log( - `This snapshot is from a later database version (${manifest.database_version}) than your node (${VERSION_DATABASE_CHAIN}). Aborting import.`, - ) - this.exit(1) - } + const manifest = await Downloader.manifest() const fileSize = FileUtils.formatFileSize(manifest.file_size) const spaceRequired = FileUtils.formatFileSize(manifest.file_size * 2) if (!flags.confirm) { const confirm = await CliUx.ux.confirm( - `Download ${fileSize} snapshot to update from block ${node.chain.head.sequence} to ${manifest.block_sequence}? ` + + `Download ${fileSize} snapshot to update from block ${headSequence} to ${manifest.block_sequence}? ` + `\nAt least ${spaceRequired} of free disk space is required to download and unzip the snapshot file.` + `\nAre you sure? (Y)es / (N)o`, ) @@ -119,194 +90,51 @@ export default class Download extends IronfishCommand { } } - let snapshotUrl = UrlUtils.tryParseUrl(manifest.file_name)?.toString() - - if (!snapshotUrl) { - // Snapshot URL is not absolute so use a relative URL from the manifest - const url = new URL(manifestUrl) - const parts = UrlUtils.splitPathName(url.pathname) - parts.pop() - parts.push(manifest.file_name) - url.pathname = UrlUtils.joinPathName(parts) - snapshotUrl = url.toString() - } - - await fsAsync.mkdir(this.sdk.config.tempDir, { recursive: true }) - snapshotPath = flags.outputPath || path.join(this.sdk.config.tempDir, manifest.file_name) - - let downloaded = 0 - try { - const statResult = await fsAsync.stat(snapshotPath) - if (statResult.isFile()) { - downloaded = statResult.size - } - } catch { - downloaded = 0 - } - - if (downloaded < manifest.file_size) { - this.log(`Downloading snapshot from ${snapshotUrl} to ${snapshotPath}`) - - const bar = CliUx.ux.progress({ - barCompleteChar: '\u2588', - barIncompleteChar: '\u2591', - format: - 'Downloading snapshot: [{bar}] {percentage}% | {downloadedSize} / {fileSize} | {speed}/s | ETA: {estimate}', - }) as ProgressBar - - bar.start(manifest.file_size, 0, { - fileSize, - downloadedSize: FileUtils.formatFileSize(downloaded), - speed: '0', - estimate: TimeUtils.renderEstimate(0, 0, 0), - }) - - const speed = new Meter() - speed.start() - - const idleTimeout = 30000 - let idleLastChunk = Date.now() - const idleCancelSource = axios.CancelToken.source() - - const idleInterval = setInterval(() => { - const timeSinceLastChunk = Date.now() - idleLastChunk - - if (timeSinceLastChunk > idleTimeout) { - clearInterval(idleInterval) - - idleCancelSource.cancel( - `Download timed out after ${TimeUtils.renderSpan(timeSinceLastChunk)}`, - ) - } - }, idleTimeout) - - const response: { data: IncomingMessage } = await axios({ - method: 'GET', - responseType: 'stream', - url: snapshotUrl, - cancelToken: idleCancelSource.token, - headers: { - range: `bytes=${downloaded}-`, - }, - }) - - const resumingDownload = response.data.statusCode === 206 - const writer = fs.createWriteStream(snapshotPath, { - flags: resumingDownload ? 'a' : 'w', - }) - downloaded = resumingDownload ? downloaded : 0 - - await new Promise((resolve, reject) => { - const onWriterError = (e: unknown) => { - writer.removeListener('close', onWriterClose) - writer.removeListener('error', onWriterError) - reject(e) - } - - const onWriterClose = () => { - writer.removeListener('close', onWriterClose) - writer.removeListener('error', onWriterError) - resolve() - } - - writer.on('error', onWriterError) - writer.on('close', onWriterClose) - - response.data.on('error', (e) => { - writer.destroy(e) - }) - - response.data.on('end', () => { - writer.close() - }) + const snapshotUrl = await Downloader.snapshotURL() + const snapshotPath = await Downloader.snapshotPath() + this.log(`Downloading snapshot from ${snapshotUrl} to ${snapshotPath}`) + + const bar = CliUx.ux.progress({ + barCompleteChar: '\u2588', + barIncompleteChar: '\u2591', + format: + 'Downloading snapshot: [{bar}] {percentage}% | {downloadedSize} / {fileSize} | {speed}/s | ETA: {estimate}', + }) as ProgressBar + + bar.start(manifest.file_size, 0, { + fileSize, + downloadedSize: FileUtils.formatFileSize(0), + speed: '0', + estimate: TimeUtils.renderEstimate(0, 0, 0), + }) - response.data.on('data', (chunk: Buffer) => { - writer.write(chunk) + const speed = new Meter() + speed.start() - downloaded += chunk.length - speed.add(chunk.length) - idleLastChunk = Date.now() + await Downloader.download((prev, curr) => { + speed.add(curr - prev) - bar.update(downloaded, { - downloadedSize: FileUtils.formatFileSize(downloaded), - speed: FileUtils.formatFileSize(speed.rate1s), - estimate: TimeUtils.renderEstimate(downloaded, manifest.file_size, speed.rate1m), - }) - }) + bar.update(curr, { + downloadedSize: FileUtils.formatFileSize(curr), + speed: FileUtils.formatFileSize(speed.rate1s), + estimate: TimeUtils.renderEstimate(curr, manifest.file_size, speed.rate1m), }) - .catch((error) => { - bar.stop() - speed.stop() - - if (idleCancelSource.token.reason?.message) { - this.logger.error(idleCancelSource.token.reason?.message) - } else { - this.logger.error( - `Error while downloading snapshot file: ${ErrorUtils.renderError(error)}`, - ) - } - - this.exit(1) - }) - .finally(() => { - clearInterval(idleInterval) - }) - + }).catch((error) => { bar.stop() speed.stop() - } + this.logger.error(ErrorUtils.renderError(error)) - this.log('Verifying snapshot checksum...') - const hasher = crypto.createHash('sha256') - await new Promise((resolve, reject) => { - const stream = fs.createReadStream(snapshotPath) - stream.on('end', resolve) - stream.on('error', reject) - stream.pipe(hasher, { end: false }) + this.exit(1) }) - const checksum = hasher.digest().toString('hex') - if (checksum !== manifest.checksum) { + const path = await Downloader.verifyChecksum({ cleanup: flags.cleanup }) + if (!path) { this.log('Snapshot checksum does not match.') - if (flags.cleanup) { - await fsAsync.rm(snapshotPath) - } - this.exit(0) + return this.exit(0) } - } - - // use a standard name, 'snapshot', for the unzipped database - const snapshotDatabasePath = this.sdk.fileSystem.join(this.sdk.config.tempDir, 'snapshot') - await fsAsync.mkdir(snapshotDatabasePath, { recursive: true }) - await this.unzip(snapshotPath, snapshotDatabasePath) - - const chainDatabasePath = this.sdk.fileSystem.resolve(this.sdk.config.chainDatabasePath) - // chainDatabasePath must be empty before unzipping snapshot - // chain DB must be closed before deleting it (fixes an error on Windows) - CliUx.ux.action.start( - `Removing existing chain data at ${chainDatabasePath} before importing snapshot`, - ) - await node.closeDB() - await fsAsync.rm(chainDatabasePath, { recursive: true, force: true, maxRetries: 10 }) - CliUx.ux.action.stop('done') - - CliUx.ux.action.start( - `Moving snapshot files from ${snapshotDatabasePath} to ${chainDatabasePath}`, - ) - await fsAsync.rename(snapshotDatabasePath, chainDatabasePath) - CliUx.ux.action.stop('done') - - if (flags.cleanup) { - CliUx.ux.action.start(`Cleaning up snapshot file at ${snapshotPath}`) - await fsAsync.rm(snapshotPath) - CliUx.ux.action.stop('done') + downloadedSnapshot = new DownloadedSnapshot(this.sdk, path) } - } - - async unzip(source: string, dest: string): Promise { - let totalEntries = 0 - let extracted = 0 const progressBar = CliUx.ux.progress({ barCompleteChar: '\u2588', @@ -315,34 +143,37 @@ export default class Download extends IronfishCommand { 'Unzipping snapshot: [{bar}] {percentage}% | {value} / {total} entries | {speed}/s | ETA: {estimate}', }) as ProgressBar - const speed = new Meter() - - progressBar.start(totalEntries, 0, { + progressBar.start(0, 0, { speed: '0', estimate: TimeUtils.renderEstimate(0, 0, 0), }) - speed.start() - tar.list({ - file: source, - onentry: (_) => progressBar.setTotal(++totalEntries), - }) + const speed = new Meter() + speed.start() - await tar.extract({ - file: source, - C: dest, - strip: 1, - strict: true, - onentry: (_) => { - speed.add(1) - progressBar.update(++extracted, { + await downloadedSnapshot.unzip( + (totalEntries: number, prevExtracted: number, currExtracted: number) => { + progressBar.setTotal(totalEntries) + speed.add(currExtracted - prevExtracted) + progressBar.update(currExtracted, { speed: speed.rate1s.toFixed(2), - estimate: TimeUtils.renderEstimate(extracted, totalEntries, speed.rate1m), + estimate: TimeUtils.renderEstimate(currExtracted, totalEntries, speed.rate1m), }) }, - }) + ) - progressBar.stop() - speed.stop() + CliUx.ux.action.start( + `Replacing existing chain data at ${downloadedSnapshot.chainDatabasePath} before importing snapshot`, + ) + + await downloadedSnapshot.replaceDatabase() + + CliUx.ux.action.stop('done') + + if (flags.cleanup) { + CliUx.ux.action.start(`Cleaning up snapshot file at ${downloadedSnapshot.file}`) + await fsAsync.rm(downloadedSnapshot.file) + CliUx.ux.action.stop('done') + } } } diff --git a/ironfish-cli/src/commands/mainnet.ts b/ironfish-cli/src/commands/mainnet.ts deleted file mode 100644 index 0dba73e283..0000000000 --- a/ironfish-cli/src/commands/mainnet.ts +++ /dev/null @@ -1,97 +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 { ErrorUtils, HOST_FILE_NAME } from '@ironfish/sdk' -import { CliUx, Flags } from '@oclif/core' -import fsAsync from 'fs/promises' -import { IronfishCommand } from '../command' -import { - ConfigFlag, - ConfigFlagKey, - DataDirFlag, - DataDirFlagKey, - VerboseFlag, - VerboseFlagKey, -} from '../flags' - -export default class Mainnet extends IronfishCommand { - static description = 'Migrate Iron Fish testnet data to mainnet' - - static flags = { - [VerboseFlagKey]: VerboseFlag, - [ConfigFlagKey]: ConfigFlag, - [DataDirFlagKey]: DataDirFlag, - confirm: Flags.boolean({ - default: false, - description: 'Confirm without asking', - }), - } - - async start(): Promise { - const { flags } = await this.parse(Mainnet) - - const currentNetworkId = this.sdk.internal.get('networkId') - if (currentNetworkId === 1) { - this.log(`Data directory is already set up for mainnet.`) - this.exit(0) - } - - const chainDatabasePath = this.sdk.config.chainDatabasePath - const hostFilePath: string = this.sdk.config.files.join( - this.sdk.config.dataDir, - HOST_FILE_NAME, - ) - - const message = - '\nYou are about to migrate your Iron Fish data to mainnet.' + - '\nYour wallet, accounts, and node configuration will be saved.' + - `\n\nThis data directory will be migrated: ${this.sdk.config.dataDir}` + - `\n\nAre you sure? (Y)es / (N)o` - - const confirmed = flags.confirm || (await CliUx.ux.confirm(message)) - - if (!confirmed) { - this.log('Migration aborted.') - this.exit(0) - } - - CliUx.ux.action.start('Migrating data...') - - try { - await Promise.all([ - fsAsync.rm(chainDatabasePath, { recursive: true, force: true }), - fsAsync.rm(hostFilePath, { recursive: true, force: true }), - ]) - } catch (error: unknown) { - CliUx.ux.action.stop('error') - this.log( - '\nAn error occurred while migrating to mainnet. Please stop all running Iron Fish nodes and try again.', - ) - this.logger.debug(ErrorUtils.renderError(error, true)) - this.exit(1) - } - - // Reset the telemetry config to allow people to re-opt in - if (this.sdk.config.isSet('enableTelemetry') && this.sdk.config.get('enableTelemetry')) { - this.sdk.config.clear('enableTelemetry') - await this.sdk.config.save() - } - - this.sdk.internal.set('networkId', 1) - this.sdk.internal.set('isFirstRun', true) - this.sdk.internal.clear('telemetryNodeId') - await this.sdk.internal.save() - - // Reset walletDb stores containing chain data - const node = await this.sdk.node() - const walletDb = node.wallet.walletDb - - await walletDb.db.open() - - for (const store of walletDb.cacheStores) { - await store.clear() - } - - CliUx.ux.action.stop('Data migrated successfully.') - } -} diff --git a/ironfish-cli/src/commands/status.ts b/ironfish-cli/src/commands/status.ts index ed09eb66cb..f619cba7a5 100644 --- a/ironfish-cli/src/commands/status.ts +++ b/ironfish-cli/src/commands/status.ts @@ -69,11 +69,15 @@ export default class Status extends IronfishCommand { // eslint-disable-next-line no-constant-condition while (true) { - const value = await this.sdk.client.node.getStatus() + try { + previousResponse = (await this.sdk.client.node.getStatus()).content + } catch (e) { + break + } + statusText.clearBaseLine(0) - statusText.setContent(renderStatus(value.content, flags.all)) + statusText.setContent(renderStatus(previousResponse, flags.all)) screen.render() - previousResponse = value.content await PromiseUtils.sleep(1000) } } diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index 40e1c82e05..bfb675e753 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -12,7 +12,7 @@ import { } from '@ironfish/sdk' import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' -import { IronFlag, RemoteFlags } from '../../flags' +import { HexFlag, IronFlag, RemoteFlags } from '../../flags' import { selectAsset } from '../../utils/asset' import { promptCurrency } from '../../utils/currency' import { selectFee } from '../../utils/fees' @@ -77,7 +77,7 @@ export class Send extends IronfishCommand { 'Minimum number of block confirmations needed to include a note. Set to 0 to include all blocks.', required: false, }), - assetId: Flags.string({ + assetId: HexFlag({ char: 'i', description: 'The identifier for the asset to use when sending', }), diff --git a/ironfish-cli/src/commands/wallet/transactions.ts b/ironfish-cli/src/commands/wallet/transactions.ts index b186d54aab..7f88f922a7 100644 --- a/ironfish-cli/src/commands/wallet/transactions.ts +++ b/ironfish-cli/src/commands/wallet/transactions.ts @@ -91,12 +91,16 @@ export class TransactionsCommand extends IronfishCommand { const assetLookup = await getAssetsByIDs( client, transaction.notes.map((n) => n.assetId) || [], + account, + flags.confirmations, ) transactionRows = this.getTransactionRowsByNote(assetLookup, transaction, format) } else { const assetLookup = await getAssetsByIDs( client, transaction.assetBalanceDeltas.map((d) => d.assetId), + account, + flags.confirmations, ) transactionRows = this.getTransactionRows(assetLookup, transaction, format) } diff --git a/ironfish-cli/src/flags.ts b/ironfish-cli/src/flags.ts index 02ef5c813d..a98fd64294 100644 --- a/ironfish-cli/src/flags.ts +++ b/ironfish-cli/src/flags.ts @@ -156,3 +156,16 @@ export const parseIron = (input: string, opts: IronOpts): Promise => { } }) } + +export const HexFlag = Flags.custom({ + parse: async (input, _ctx, opts) => { + const hexRegex = /^[0-9A-Fa-f]+$/g + if (!hexRegex.test(input)) { + throw new Error( + `The value provided for ${opts.name} is an invalid format. It must be a hex string.`, + ) + } + + return Promise.resolve(input) + }, +}) diff --git a/ironfish-cli/src/snapshot.ts b/ironfish-cli/src/snapshot.ts index fdb6c16a29..b71fd2d2f5 100644 --- a/ironfish-cli/src/snapshot.ts +++ b/ironfish-cli/src/snapshot.ts @@ -1,6 +1,16 @@ /* 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 { ErrorUtils, IronfishSdk, TimeUtils } from '@ironfish/sdk' +import axios from 'axios' +import crypto from 'crypto' +import fs from 'fs' +import fsAsync from 'fs/promises' +import { IncomingMessage } from 'http' +import path from 'path' +import tar from 'tar' + export type SnapshotManifest = { block_sequence: number checksum: string @@ -9,3 +19,248 @@ export type SnapshotManifest = { timestamp: number database_version: number } + +export const DEFAULT_MAINNET_MANIFEST_URL = `https://snapshots.ironfish.network/manifest.json` +export const DEFAULT_TESTNET_MANIFEST_URL = `https://testnet.snapshots.ironfish.network/manifest.json` + +export const getDefaultManifestUrl = (networkId: number): string | null => { + switch (networkId) { + case 0: + return DEFAULT_TESTNET_MANIFEST_URL + case 1: + return DEFAULT_MAINNET_MANIFEST_URL + default: + return null + } +} + +const tryParseUrl = (url: string): URL | null => { + try { + return new URL(url) + } catch (_) { + return null + } +} + +const getSnapshotUrl = (manifestUrl: string, manifest: SnapshotManifest): string => { + const snapshotUrl = tryParseUrl(manifest.file_name)?.toString() + if (snapshotUrl) { + return snapshotUrl + } + + // Snapshot URL is not absolute so use a relative URL from the manifest + const url = new URL(manifestUrl) + const parts = url.pathname.split('/').filter((s) => !!s.trim()) + parts.pop() + parts.push(manifest.file_name) + url.pathname = parts.join('/') + return url.toString() +} + +async function matchesChecksum(file: string, checksum: string): Promise { + const hasher = crypto.createHash('sha256') + await new Promise((resolve, reject) => { + const stream = fs.createReadStream(file) + stream.on('end', resolve) + stream.on('error', reject) + stream.pipe(hasher, { end: false }) + }) + + const fileCheckSum = hasher.digest().toString('hex') + return fileCheckSum === checksum +} + +export class SnapshotDownloader { + nodeChainDBVersion: number + manifestUrl: string + dest: string + _manifest?: SnapshotManifest + + constructor(manifestUrl: string, dest: string, nodeChainDBVersion: number) { + this.nodeChainDBVersion = nodeChainDBVersion + this.manifestUrl = manifestUrl + this.dest = dest + } + + async manifest(): Promise { + if (this._manifest) { + return this._manifest + } + const manifest = (await axios.get(this.manifestUrl)).data + this._manifest = manifest + return manifest + } + + async snapshotURL(): Promise { + const manifest = await this.manifest() + return getSnapshotUrl(this.manifestUrl, manifest) + } + + async snapshotPath(): Promise { + const manifest = await this.manifest() + return path.join(this.dest, manifest.file_name) + } + + async download(onDownloadProgress: (prev: number, curr: number) => void): Promise { + const manifest = await this.manifest() + const snapshotPath = await this.snapshotPath() + + if (manifest.database_version > this.nodeChainDBVersion) { + throw new Error( + `This snapshot is from a later database version (${manifest.database_version}) than your node (${this.nodeChainDBVersion}). Aborting import.`, + ) + } + + let downloaded = 0 + + const statResult = await fsAsync.stat(snapshotPath).catch(() => null) + if (statResult?.isFile()) { + downloaded = statResult.size + } + + if (downloaded >= manifest.file_size) { + return + } + + const idleTimeout = 30000 + let idleLastChunk = Date.now() + const idleCancelSource = axios.CancelToken.source() + + const idleInterval = setInterval(() => { + const timeSinceLastChunk = Date.now() - idleLastChunk + + if (timeSinceLastChunk > idleTimeout) { + clearInterval(idleInterval) + + idleCancelSource.cancel( + `Download timed out after ${TimeUtils.renderSpan(timeSinceLastChunk)}`, + ) + } + }, idleTimeout) + + const snapshotUrl = await this.snapshotURL() + + const response: { data: IncomingMessage } = await axios({ + method: 'GET', + responseType: 'stream', + url: snapshotUrl, + cancelToken: idleCancelSource.token, + headers: { + range: `bytes=${downloaded}-`, + }, + }) + + const resumingDownload = response.data.statusCode === 206 + const writer = fs.createWriteStream(snapshotPath, { + flags: resumingDownload ? 'a' : 'w', + }) + + downloaded = resumingDownload ? downloaded : 0 + + await new Promise((resolve, reject) => { + const onWriterError = (e: unknown) => { + writer.removeListener('close', onWriterClose) + writer.removeListener('error', onWriterError) + reject(e) + } + + const onWriterClose = () => { + writer.removeListener('close', onWriterClose) + writer.removeListener('error', onWriterError) + resolve() + } + + writer.on('error', onWriterError) + writer.on('close', onWriterClose) + + response.data.on('error', (e) => { + writer.destroy(e) + }) + + response.data.on('end', () => { + writer.close() + }) + + response.data.on('data', (chunk: Buffer) => { + writer.write(chunk) + + onDownloadProgress(downloaded, downloaded + chunk.length) + downloaded += chunk.length + idleLastChunk = Date.now() + }) + }) + .catch((error) => { + if (idleCancelSource.token.reason?.message) { + throw new Error(idleCancelSource.token.reason?.message) + } else { + throw new Error( + `Error while downloading snapshot file: ${ErrorUtils.renderError(error)}`, + ) + } + }) + .finally(() => { + clearInterval(idleInterval) + }) + } + + async verifyChecksum(options: { cleanup: boolean }): Promise { + const manifest = await this.manifest() + + const destination = path.join(this.dest, manifest.file_name) + + const matches = await matchesChecksum(destination, manifest.checksum) + if (!matches) { + if (options.cleanup) { + await fsAsync.rm(destination, { recursive: true, force: true }) + } + return null + } + + return destination + } +} + +export class DownloadedSnapshot { + private sdk: IronfishSdk + readonly file: string + + constructor(sdk: IronfishSdk, file: string) { + this.sdk = sdk + this.file = file + } + + get chainDatabasePath(): string { + return this.sdk.fileSystem.resolve(this.sdk.config.chainDatabasePath) + } + + get snapshotDatabasePath(): string { + return this.sdk.fileSystem.join(this.sdk.config.tempDir, 'snapshot') + } + + async unzip( + onEntry: (totalEntries: number, prevExtracted: number, currExtracted: number) => void, + ): Promise { + await fsAsync.mkdir(this.snapshotDatabasePath, { recursive: true }) + + let totalEntries = 0 + let extracted = 0 + + tar.list({ + file: this.file, + onentry: (_) => onEntry(++totalEntries, extracted, extracted), + }) + + await tar.extract({ + file: this.file, + C: this.snapshotDatabasePath, + strip: 1, + strict: true, + onentry: (_) => onEntry(totalEntries, extracted, ++extracted), + }) + } + + async replaceDatabase(): Promise { + await fsAsync.rm(this.chainDatabasePath, { recursive: true, force: true, maxRetries: 10 }) + await fsAsync.rename(this.snapshotDatabasePath, this.chainDatabasePath) + } +} diff --git a/ironfish-cli/src/utils/asset.ts b/ironfish-cli/src/utils/asset.ts index f7496b55dc..c8b43f84c8 100644 --- a/ironfish-cli/src/utils/asset.ts +++ b/ironfish-cli/src/utils/asset.ts @@ -102,6 +102,8 @@ export async function selectAsset( const assetLookup = await getAssetsByIDs( client, balances.map((b) => b.assetId), + account, + options.confirmations, ) if (!options.showNativeAsset) { balances = balances.filter((b) => b.assetId !== Asset.nativeId().toString('hex')) @@ -164,9 +166,13 @@ export async function selectAsset( export async function getAssetsByIDs( client: Pick, assetIds: string[], + account: string | undefined, + confirmations: number | undefined, ): Promise<{ [key: string]: RpcAsset }> { assetIds = [...new Set(assetIds)] - const assets = await Promise.all(assetIds.map((id) => client.wallet.getAsset({ id }))) + const assets = await Promise.all( + assetIds.map((id) => client.wallet.getAsset({ id, account, confirmations })), + ) const assetLookup: { [key: string]: RpcAsset } = {} assets.forEach((asset) => { assetLookup[asset.content.id] = asset.content diff --git a/ironfish-cli/src/utils/url.ts b/ironfish-cli/src/utils/url.ts deleted file mode 100644 index 3a9b946f05..0000000000 --- a/ironfish-cli/src/utils/url.ts +++ /dev/null @@ -1,26 +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 { ErrorUtils } from '@ironfish/sdk' - -const tryParseUrl = (url: string): URL | null => { - try { - return new URL(url) - } catch (e) { - if (e instanceof TypeError && ErrorUtils.isNodeError(e) && e.code === 'ERR_INVALID_URL') { - return null - } - throw e - } -} - -function splitPathName(pathName: string): string[] { - return pathName.split('/').filter((s) => !!s.trim()) -} - -function joinPathName(parts: string[]): string { - return parts.join('/') -} - -export const UrlUtils = { tryParseUrl, joinPathName, splitPathName } diff --git a/ironfish/package.json b/ironfish/package.json index 8bc9ec0cb1..0cc965a3d2 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "1.10.1", + "version": "1.11.0", "description": "SDK for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", diff --git a/ironfish/src/fileStores/config.ts b/ironfish/src/fileStores/config.ts index 21e16e6523..2f267fc8cd 100644 --- a/ironfish/src/fileStores/config.ts +++ b/ironfish/src/fileStores/config.ts @@ -140,6 +140,15 @@ export type ConfigOptions = { */ minerBatchSize: number + /* + * Whether the mining manager should preemptively generate empty block + * templates, even if there are transactions in the mempool, with the goal of + * maximizing miner efficiency. This is enabled (true) by default. It may be + * disabled for testing purposes or when mining is small networks, with a low + * difficulty. + */ + preemptiveBlockMining: boolean + /** * The minimum number of block confirmations needed when computing account * balance. @@ -293,7 +302,7 @@ export type ConfigOptions = { walletNodeIpcPath: string /** - * Enable stanalone wallet process to connect to a node via TCP + * Enable standalone wallet process to connect to a node via TCP */ walletNodeTcpEnabled: boolean walletNodeTcpHost: string @@ -331,7 +340,7 @@ export const ConfigOptionsSchema: yup.ObjectSchema> = yup // to parse logPrefix logPrefix: yup.string(), blockGraffiti: yup.string(), - nodeName: yup.string(), + nodeName: yup.string().max(32), nodeWorkers: yup.number().integer().min(-1), nodeWorkersMax: yup.number().integer().min(-1), peerPort: YupUtils.isPort, @@ -350,6 +359,7 @@ export const ConfigOptionsSchema: yup.ObjectSchema> = yup transactionExpirationDelta: YupUtils.isPositiveInteger, blocksPerMessage: YupUtils.isPositiveInteger, minerBatchSize: YupUtils.isPositiveInteger, + preemptiveBlockMining: yup.boolean(), confirmations: YupUtils.isPositiveInteger, poolName: yup.string(), poolAccountName: yup.string().optional(), @@ -456,6 +466,7 @@ export class Config extends KeyStore { generateNewIdentity: false, blocksPerMessage: 25, minerBatchSize: 25000, + preemptiveBlockMining: true, poolName: 'Iron Fish Pool', poolAccountName: undefined, poolBanning: true, diff --git a/ironfish/src/index.ts b/ironfish/src/index.ts index a06ebc18de..efa7aa0697 100644 --- a/ironfish/src/index.ts +++ b/ironfish/src/index.ts @@ -6,7 +6,7 @@ export * from './assert' export * from './assets' export * from './blockchain' export * from './consensus' -export { defaultNetworkName, isDefaultNetworkId } from './defaultNetworkDefinitions' +export * from './defaultNetworkDefinitions' export * from './networkDefinition' export { DEV_GENESIS_ACCOUNT } from './genesisBlocks/devnet' export * from './chainProcessor' diff --git a/ironfish/src/mining/__fixtures__/manager.test.slow.ts.fixture b/ironfish/src/mining/__fixtures__/manager.test.slow.ts.fixture index b8afb07395..26d51e1908 100644 --- a/ironfish/src/mining/__fixtures__/manager.test.slow.ts.fixture +++ b/ironfish/src/mining/__fixtures__/manager.test.slow.ts.fixture @@ -1049,5 +1049,80 @@ } ] } + ], + "Mining manager create block template with preemptive block creation disabled does not create empty blocks when there are transactions in the mempool": [ + { + "version": 2, + "id": "cce31da7-0185-4e68-a0bf-d3d87b2e364b", + "name": "account", + "spendingKey": "05a454265e9d9eadf0040bb6da24312a76416a20039cc081e12d52e39cebaa2a", + "viewKey": "3c455d225381615009447ed27391cb65d5548ef114331d84b74d750d1c45c0a6a9abc231acc99828577b1c318c1aaca3889be6a4786d8e8333830fc1255297cf", + "incomingViewKey": "f680d4dfe8e22128139ee967892d464cf3fc095ee7427aca8235fe9c5ef76307", + "outgoingViewKey": "de6de8874c438edf91d6eb11a72b4a9b7cef2b9689369fcbabc074270f984924", + "publicAddress": "22f4a59475863eaf32c9715e3273ea97f19479a8a438f29e87fc6ee4d300d527", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:uWoOhr2GtuZBpNXMGb6lX4G7CmZCA5Bs9FXkCX2xHw0=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:FMWwpY1CfUSRE6PTebgpY9h/mgVXr+UWyEvPmZUU6fA=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1697170363864, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAACgCvyD0Lsxc/ZVecQ0CpjqEp9BELblDeNsqxGMhQAK6E1IEJ4GepWQeiX4nILpxu2B/ukfLtONP2ozenOZmg+A6zDi7srLetP9f9TJbrXReBfNEtD7MpGLO0aCInH4zVwXdp0DrpiI1tucf0Fsz8oQNCXDypOWsFoOV6Xdde3bAWnf2e3+aiCh51ZZxZv7PjabSVme7ANTgAUrpwK+th+9MbywtOjKvY75ciYzTcaP2pMjEbciHKkAL7mZ3nXxWEzyxw91WyN+78NX9osmWUdGKcJ0pN++0tayjHjx0sC/klWPYwTgRVoM4kkepKlUrIrTXf/D5YCEBj3UPSpS5fLvn6y3CtxDGlO6Ib7sQDQukal57I+wrUKCw3lNN9QYUjt1eizapztjTyJIjmW6dnY1qV/LerH41xRaawDhq6sAo7Ep+cGeZ/KkAgcsmNpGm8GQpKLKrvBBRga/GtctvYYJryuTNP61pyjLPmeLOfNsrh0Ug0PiPnv1FNY1EQo9orHfEb7bq0ALogWrjCYPUiBZqEyuNyRUjNADXLasoxKpFom6o9skNjaF3B4vupGp8lIrumXzGFZNBHxcy4KYFNqbpZsbsTc/GDrXHMIDm3DZrKB+JMg7wTG0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw5UoJq7yi+LMr5rAIHz3OfQ3/Ar4eDpjU8clwyhjkaF/pww5AnBQ2vgRXa7dbPcjZr4VSuXPN6d3E4MH3ZYoUBw==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "B1A9496B498FEDC3BD75854B4C397480411B952E16F4785A009EDED524B9614C", + "noteCommitment": { + "type": "Buffer", + "data": "base64:/s7W5FnV0WCryltMuiIJd8B7C1a6DuL30U8aT4XUGnA=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:ODRoQQrJODkZjOMQUmnyp7NZHoY/nDiWJqViEB5V3lk=" + }, + "target": "9255858786337818395603165512831024101510453493377417362192396248796027", + "randomness": "0", + "timestamp": 1697170366123, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 5, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAAjQHPC/KFaUEWi0DhqNVQgkYfLLEw72jQXMRBjhRKCaW3RsEgCy18vodVWiz2nx/wZdi+r2tQJMu62dqFZ09WXnQErOmVeA+5APjzZHHPfWXsAjjVdSMMUo6NYPpmKPPLxdHNAuG/smTadjqQZLMGO1qKZCTRiPrKMPzSN6otbgF1kbVnk/zEs3FPYIrFYWhK8KTbjdpAhaB9FAS4ZfpJ0VCfIBMFOdJ+usMjZGM7xqWp/uC85DYdbB7cRgdnmRNwszNBargSGc8XShAQgA58mA1PTCB0WLAp6rKPuCR2+CgBXM2WuoB05HgPqft0NFBb6bH+qB1QCfP2z9l0iWk5VDA0lhZ2asDBaZdvDNBCWE0cDTWHrMnUHZJKfB4nQUZtRoO4J3QEAsnJ1eflGXjru5fPE3MyhzpKcYR6WZguxqlnD+NzAQxdhKCz454hEPrQWTzkY2dtMTTrbFO6nR/Ujkzq2K06ZpOZatNIwj0ptDzc+dKf+33IAN/3weZcuZ6SC6XZXj/TjuqnigHaAyxgBXKQoi/sLgEFVCyvoz2SjjWbwY5USAxpeZImOwd8C7Kol/PwEquo9f9JGHx2TlW1ShIlTr/ODg9LEPuG25z7mez9zpUISlO/Elyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwkzZC1eAFkyutVM/bvxFE4ACcLO4ycU/QuN32eaSMTwMsGH25ZTg6+wRIhyeibsgaT+nt617NNrvRQp3rfWy3AQ==" + } + ] + } ] } \ No newline at end of file diff --git a/ironfish/src/mining/manager.test.slow.ts b/ironfish/src/mining/manager.test.slow.ts index 89598188c0..e701405102 100644 --- a/ironfish/src/mining/manager.test.slow.ts +++ b/ironfish/src/mining/manager.test.slow.ts @@ -474,6 +474,47 @@ describe('Mining manager', () => { expect(templates[0]).toEqual(templates[2]) }) + + describe('with preemptive block creation disabled', () => { + const nodeTest = createNodeTest(false, { + config: { miningForce: true, preemptiveBlockMining: false }, + }) + + it('does not create empty blocks when there are transactions in the mempool', async () => { + const { node, chain } = nodeTest + const { miningManager } = node + + const account = await useAccountFixture(nodeTest.node.wallet, 'account') + await nodeTest.node.wallet.setDefaultAccount(account.name) + + const previous = await useMinerBlockFixture(chain, 2, account, node.wallet) + await expect(chain).toAddBlock(previous) + await node.wallet.updateHead() + + const transaction = await useTxFixture(node.wallet, account, account) + + expect(node.memPool.count()).toBe(0) + node.memPool.acceptTransaction(transaction) + expect(node.memPool.count()).toBe(1) + + const block = await useMinerBlockFixture(chain, 2) + await expect(chain).toAddBlock(block) + + // Wait for the first block template + const [fullTemplate] = await collectTemplates(miningManager, 1) + + Assert.isNotUndefined(fullTemplate) + + expect(fullTemplate.header.previousBlockHash).toBe(chain.head.hash.toString('hex')) + expect(fullTemplate.transactions).toHaveLength(2) + + const minersFee = new Transaction(Buffer.from(fullTemplate.transactions[0], 'hex')) + expect(isTransactionMine(minersFee, account)).toBe(true) + + expect(fullTemplate.transactions[1]).toEqual(transaction.serialize().toString('hex')) + expect(node.memPool.count()).toBe(1) + }) + }) }) describe('submit block template', () => { diff --git a/ironfish/src/mining/manager.ts b/ironfish/src/mining/manager.ts index d3f2dc3324..e0537593cd 100644 --- a/ironfish/src/mining/manager.ts +++ b/ironfish/src/mining/manager.ts @@ -40,6 +40,7 @@ export class MiningManager { private readonly node: FullNode private readonly metrics: MetricsMonitor private readonly minersFeeCache: MinersFeeCache + private readonly preemptiveBlockMining: boolean blocksMined = 0 @@ -56,12 +57,14 @@ export class MiningManager { node: FullNode memPool: MemPool metrics: MetricsMonitor + preemptiveBlockMining?: boolean }) { this.node = options.node this.memPool = options.memPool this.chain = options.chain this.metrics = options.metrics this.minersFeeCache = new MinersFeeCache({ node: this.node }) + this.preemptiveBlockMining = options.preemptiveBlockMining ?? true this.chain.onConnectBlock.on( (block) => @@ -170,9 +173,15 @@ export class MiningManager { return } + // If preemptive block mining is enabled, stream an empty block template as + // soon as possible, so that miners do not spend idle time waiting for a + // valid block to mine. Otherwise, stream an empty block only if there are + // no pending transactions in the mempool. const emptyTemplate = await this.createNewBlockTemplate(currentBlock, account, false) this.metrics.mining_newEmptyBlockTemplate.add(BenchUtils.end(connectedAt)) - this.streamBlockTemplate(currentBlock, emptyTemplate) + if (this.preemptiveBlockMining || !this.memPool.count()) { + this.streamBlockTemplate(currentBlock, emptyTemplate) + } // The head of the chain has changed, abort working on this template if (!this.chain.head.hash.equals(currentBlock.header.hash)) { diff --git a/ironfish/src/network/peerNetwork.ts b/ironfish/src/network/peerNetwork.ts index 2459b3328d..d4262d5254 100644 --- a/ironfish/src/network/peerNetwork.ts +++ b/ironfish/src/network/peerNetwork.ts @@ -226,7 +226,7 @@ export class PeerNetwork { this.requests = new Map() if (options.name && options.name.length > 32) { - options.name = options.name.slice(32) + options.name = options.name.slice(0, 32) } this.blockFetcher = new BlockFetcher(this) diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index d61eda8a75..de0d99f5a2 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -100,7 +100,13 @@ export class FullNode { this.chain = chain this.strategy = strategy this.metrics = metrics - this.miningManager = new MiningManager({ chain, memPool, node: this, metrics }) + this.miningManager = new MiningManager({ + chain, + memPool, + node: this, + metrics, + preemptiveBlockMining: config.get('preemptiveBlockMining'), + }) this.memPool = memPool this.workerPool = workerPool this.rpc = new RpcServer(this, internal) diff --git a/ironfish/src/rpc/routes/wallet/__fixtures__/getAccountTransactions.test.ts.fixture b/ironfish/src/rpc/routes/wallet/__fixtures__/getAccountTransactions.test.ts.fixture index 983191d065..b337547f96 100644 --- a/ironfish/src/rpc/routes/wallet/__fixtures__/getAccountTransactions.test.ts.fixture +++ b/ironfish/src/rpc/routes/wallet/__fixtures__/getAccountTransactions.test.ts.fixture @@ -280,5 +280,76 @@ "type": "Buffer", "data": "base64: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" } + ], + "Route wallet/getAccountTransactions sorts transactions when sort is passed": [ + { + "version": 2, + "id": "ebc22b57-2952-4c27-ae61-1d4e93e6f17e", + "name": "default-sort", + "spendingKey": "21a17485846951c662074af1266c82e92346b6271f093ca170392feefa380d15", + "viewKey": "2e0db2d0d0416da33f22e63513db7b1cb94787b4b53465ff7c5bd296731940b5c4bc828fda78e0a1e8cce487161222b51e1efa07f9b4cc86a8fba495bd899fe0", + "incomingViewKey": "aeb967d4b83425641cee59bc8964600a5074e817fcfa83942e31d44e4a16a702", + "outgoingViewKey": "c38bdfbc6bb237489643d58cd9ba6540ba52242c60ea7d0604a5421b9337c99b", + "publicAddress": "fe55676e1816c528d4c300bd5d27f9b0e9017c7236ec3a737121ff2fed1856e9", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:pWdmPyUv8uxcNvSOTkKVpOB/vcaaDcLnKziFNHzhuDk=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:tXM/v6AhtC9AX+FkdCaiRMJbyVVI6L92R6k/UvUVuqc=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1696960192308, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAANHXq4x25ra3bGXl3nuNFa43ffrBjTlpJNmg5UsgYWrSDOUazKIh7Dax+8Z9hVe2TtC3YBZUMejKVjO+/11Moo7mjVQ+L3ehjwPj2q2PFrYGGssgIoW7O5a2bdsQ2B1anXyHP+9RHAUxAkIahAD/hUUOZ+Wis2loj8VNQ53G2vyoVCGZARWyQRI2wA+Y8H6PsoD1+PSKb0ahXK/OqhGw757nuCK+kG2yPifli9O89fPK4db24hlZe3bkwr+7fpcxu53nB7RHb5TPQb/nMVJq1n9J+dIUdGZhq82GYl/8EMNFRwqkGqQbScuGTAfo4EX/1t3us8LNfXeo1HRwOKagPWy1jY3M/ETA7naI2APycx9clp2awx836fjQ2OK8M21ZVb/8wlFbBe/m9HZf40UPQ/iqX9KsM30dK+7Vh2vHZqhYcgBiiKQXpyrMP0gAvEhZ+9HWk8FWGgvmEKHwVGcE7UGbln9dO9YJUobN1zYhRCwmHXpiz7J2vYUQZQFY2vMjzmUFyOx9ihmmKPu8IPsdhiPw0R4eSGF1pj6tQ/vy262Mzy60Efvj4haxD9BS9azsXY/cWpeMnZxbygP73CcTSdVJ1UVjioKR6pvfmRGJ1E0mvpfOCwOe0gklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwmvknEA7Ryu0Gr/YBaisbP/dAvWs/TbjipIczuOfIG7ufH3GLy6BEhGTVByFeYc/v9CIMsgIB+dbNQH+01+JLCQ==" + } + ] + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "7721ABFA06AB79C87166CB16B9E4087054F9DDEAF83B5C5BC4BF0AC1B0DD4C08", + "noteCommitment": { + "type": "Buffer", + "data": "base64:WAE2//3HOa/nKLebbMvNbqwAR8oelIDwdXWrgvWJbV4=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:YGQ2MWUTYPzJ4QDnmJ/Bjbc2+PiMeWj69itAimMTZeI=" + }, + "target": "9255858786337818395603165512831024101510453493377417362192396248796027", + "randomness": "0", + "timestamp": 1696960192847, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 5, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAJmEoTJ28SZGE+5v+KadITu7IjW0ayrkBxuV/YHRHZHGVpzrMtbzOS7p6qc6ICyoN2rx4GFl2qW+K1ZNvs27skOBU4mZ2IyHgluNWmd7FfWuy0OcDQ07jr/FTO/ye3RvRQjAJ4mlyODxPVNd7u42/HtKRaVu7qNvU1dWFVMAP3M0T8OyQsx25gmiUXp6qg3v9pmrL4ScE51PZY0i1EEFPuO3MaCjGSz0oF/P97D3yfj+AvKr66jGxv0d5S/+RFRKbBqi3t085r4/m5oZJUzGBvnkqYJp3G4/Ep5b8kzlJuqjZElNEarXsybq2iIrK2I/Z6zGtM5LBQxkZL6EC4pLPc8EOqJPVqjECP4yr9FJycaLwGYHMyC9MuwI5fw1ZPaQTzQ+VOZl8L4LH56ntPpkzr46S3BuC2GIAVUPgUOKak88EPGLK4S5RDvsgi1No4zatLVcGiJ80W9NM/BzQAErVskeR552NERcQQJ/DWmX0NMlbFmsR+P7bBr0pa02GI9sirThD/76s5zYVLoTVwloeJdYvIP84OUl0zrNdKJ0E06yRlvu7EssGHbWrQUPvjO+YKb1ROiOHl48N1//uVf3HNn6gk5Yh8ph1lt8AP6pxMZAZi93YGfh6FElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwxE+ovsqPPqMW1tSEDw3KI4BNTZEKuvjT3EPq7BIemV16VQ3RJV+6eZF7OeudcaGLUVDGccL8eP3cAt9xCb04Dg==" + } + ] + } ] } \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__fixtures__/mintAsset.test.ts.fixture b/ironfish/src/rpc/routes/wallet/__fixtures__/mintAsset.test.ts.fixture index 30807ff6c0..a676aafcd0 100644 --- a/ironfish/src/rpc/routes/wallet/__fixtures__/mintAsset.test.ts.fixture +++ b/ironfish/src/rpc/routes/wallet/__fixtures__/mintAsset.test.ts.fixture @@ -2,13 +2,30 @@ "Route wallet/mintAsset with valid parameters returns the asset identifier and transaction hash": [ { "version": 2, - "id": "f5a50f01-b126-4b00-9f07-3625b949d582", + "id": "4a5931c3-4eb1-4cb9-8854-a1d385907500", "name": "test", - "spendingKey": "ab4b4eb0a2957f7196d7f16a6f907598118a961d3e02ac4b7029adb9e36a6dd1", - "viewKey": "d301a93d3dbad98d07fcd89dfc8f17863145985772e52b47b9ce56ce4ea6888c7424e77886d135d769aa2f01ccd61e4d2ce0b039d871c621b2f47d4c3926a523", - "incomingViewKey": "e4dd6c02cac658510655b49a1a7dfce4212f5cb216595cdc5987f3b71b49a000", - "outgoingViewKey": "159ede6601a6252fb9a7e7b7f92bd43a2ba18fd9bf106a88ca8b79dce2ae162d", - "publicAddress": "39262d7a52111750a5239aa2d2d73364ba23fbea604f2c12e2880b5e70a4088f", + "spendingKey": "46c15976bb6db9fe4592b73c5691f4731ac9c9c2f33423471ae72289c33336a1", + "viewKey": "9dd5862754745dd86ee11aca388633d6e99db861f248d46fc9a74420afcd555e18abf0a495fa143d312379387cd1883c426f2113a0089a8f3f844c3338f015be", + "incomingViewKey": "6ce882f4845deda09f9684e51ed3877486f5a8187969e94c8934064a316bb106", + "outgoingViewKey": "7777c65d9dc421ad93098dc512f3f65e7ac00f349668686ff6b3c04ed8e133c6", + "publicAddress": "a81f70356eb48c5671655049f7215bffd516fae6bb1e47e6db5951be1c9c2544", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "version": 2, + "id": "db134fd5-8dfd-468d-9848-ff7fb46b31d9", + "name": "accountB", + "spendingKey": "b97043826f9c292ce3852a350af5cd4f873f640397f41bf03ae4c245efc80dfd", + "viewKey": "1fde44360fd79fd2289b44fdc3247b3da622b8083394b84f7025cf1243fd0355bd550563517436751fad9ce8b89ce592a0cc87c56a250482f3f3496035c8f784", + "incomingViewKey": "8010e6f283b880ce84db872e77d24359b96fb52718ac8a74b20b7ff153ff4602", + "outgoingViewKey": "c1a30b8e3a20d943209234ae1ed6dd6d0cb4fee7b78a6f6b36780389bfdde396", + "publicAddress": "c21167eb85e1fa433cec1b27641d5101e67e54c6e62434089e277c49cb3a2507", "createdAt": { "hash": { "type": "Buffer", @@ -23,15 +40,15 @@ "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", "noteCommitment": { "type": "Buffer", - "data": "base64:waAxpkedfQ2USM9L16v9xHjM1Qy1iqf6dTekpId6K3I=" + "data": "base64:Cgb/e459X5P0rZn3I/TuTrJ0O0mwiU+Yhprw0YABYxE=" }, "transactionCommitment": { "type": "Buffer", - "data": "base64:BRVLJ6gs0MT2uDrGROQnjo0Rop7VcEwuxhyKedRQJMk=" + "data": "base64:GzU1/penobvyomE/u0K+KQs+hKLnEGMXJa9/aYw7zVM=" }, "target": "9282972777491357380673661573939192202192629606981189395159182914949423", "randomness": "0", - "timestamp": 1695140239416, + "timestamp": 1697484127275, "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", "noteSize": 4, "work": "0" @@ -39,13 +56,13 @@ "transactions": [ { "type": "Buffer", - "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAWVaDPQTY18D+1eWNXe40Ferw7oviclQRCOv9gFsTPxeJeWPJk30FSFuJIe+kDqjbbWPBTmEZmobMXjsx0aOEEzx7tf8YK4yiXe0k4/VI2cisLBFKmuHMiGN70iiKsb1f4GZmXU1bu8gAoM1Y6ArVdPX7ktloKIvUj3tdnL0ERzQHnIYzSswwg1aVqRQkFFvBL+zfHlgp8KcJb53E56YRmR4VvNgFJT6d0yvEOIiy7kWPVt52yqkHz1IYra7zGE/PPvJOoHzXteQOAHqvsxADUllVcj7Yd4wfX6qxjznmijAj3C4iNpnkX4qV6K19gzo8DcclwoRNemElrqh/6vZdL6Oy+v80AUdBeTxnZJUKPIOR0Nk3ChKeCD4Pw7K2B0Jjwebs5tpU3a4SOIGecc8xCOStFtj0UY7/TieXKqzYLTZEunNb1UtCnCZfUGWfdttoqC9tHTdR55qT0t87AR0aAWjnAOtxe+ORwBX62C/Oafv6Hdn2Zh/kqPZ+t7SBG2GEnfMQBOXgnUSDbCeEqOqyMzJrtQVOed70vGFKT1ZJKdj1EfWiP1C65BNyRlQgHpaP48SzE/kiMe8CBZrTskLc1zNKbYAS99pT/n6ibb89PFol610A6U200Elyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwFNqMKPWBswaEx/mPkT70S7hCpeibYNxK/IiqlPZ9cp9pc5OodziWi30jleD1fZHFzMcUgUVbgjQBi4v8an0AAg==" + "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAYV3Lni2DuBVvx4P2oCgTrMElCY0MPEacvRZxyymzpxCJD3ZdokA3Aq9HJrDW5B70aZffixeQ3sAtaYHvL+ml+CkT1Gpk8U1PTvUs4v2YNMKiE+ISaio7LWbxNSEIQStjyXnmT5aOBYEuPd7elUnFTbca8Ir5TfRU/Bqfp1UtDccLk+W5/1lViZfbS9ieCVtZn3f8a+20CnyMESjpW413NanrjpHqANBv99fD7ebwBOmOVuhVv0aVo4RQfKAFGzTDjI5K12i0938N30dmR0lhThtsdn+DJqWOsKCVTDdM6+mbK7UX9u3qjQ0gS/m97lmRY2xYDlgukd3tfA2OisX11q5RSye1+pa3Z1qp+cN1fV3WGIidH5zGmeZL7F52EUoWBA5v3KLRo5UYC5fSvsBtdSdR/CeNC9OogLwIeKhDQ9Q2sRvARtGsOrZY22tHY6Og1aSfiAUJ6Q0Z8LkIA6otfeMthbT7jxM5mBfPiw3d4nzO8WYhYnxiipzIFvyDN6/hM1dcgtHed80ZvBmMr8OOr6V/vkLOlo9POJ42tQta+RCGyA5H0pzt+KHhzgkGHPNsFROBTObmttJaN49RQuKiroSX7D/g+q6RNCvevziSOnEm44spbuCSj0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwKocn50/Owj2DwdsFjov71vDAi0NWkY3DjYVLWRZErUfWu0Hq4s6xRbO2DRZhDaW4JdejfkymVZ2hpQ7m1R7WBw==" } ] }, { "type": "Buffer", - "data": "base64:AgAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqTgUd/XBiaBuVffQPFdgFzoS2KDexILtkq1cq6qj+qKmHE9GXwZDkocvvj2oTep07wFZahQiqkabcyAPkW0ty4n+G0hr04KRTZSZRoX0z+ZXunEDP0P7MW4PNmMNNOmrVIFuV67AJgyc2DuEdOkfxykG5e65YLjv/rDziisLiwCZtzouMMU9ckm2MDfrC0dorSV1+g7P51U1aVS0S5P/KMNIwBXK16PFMGKrATolm2T7wZvV3JJJPQL+nB6YyyB6TrEoOrKCk3O2O256CDiqzRL+20mSNyc/DEeUYpipqPye9OFpaMxjePFv69oyN7RIf4razNcoFRotyl/SPFiyOLbYRScMQ4Kzi34ov++Rc0uN590q9NOcNvMC9vjDDlu3rUth8VuCYap8iN/1s8ejf2ilshQIJ35GEKVB1U2R7r0I5oRVca6ZPpmvXZxa4sV2gYlifTPyhp/nM5tpr6uJhLnG4o94sMhBzKSfGC7yNNWvlJhC1H3VrP45T3CzMXDo5kK979A04qnckcdp3dVJxaIQbaE0yjwqMjBBBAVcCylEtyqR9QgowVThkRegmzMXiUq70quw6uPC+yLxxqqD1E7IdkBmuPqWIeDEf3Y/ym/XdDpGoZ2C9NVfCqSu7fAxNtGxcSXK8BxntZRJDvzvWp5DxZWjJ/7AXJ+bjjoSoypjWpkRR2uL2MiYvEZn9jbkvPyMXOY7BQ7eQd2w/xUFK+H4a0o5pjniScitlGdfaxyIYifT6vcCwLAG5zd8F6SzohVYJ0kFW6YwJeOiDbHy9540JLWr4iHr7mnWFUW2rZavdPSEngZdMIGJm6CQlm34DVlXfxjHbffYsSJTtLl8v9/j7TISZA+EmHQYJl3BHIMYSER4PqqGXPJvbCMP6rm/hRntdykU6OhYH/nD2Cg4a+Y0p74bHNcqxkYVDwrYAHqJlnwUffP2xsB+argRjJ7WrXUeelJ3s9x0OndzF2PEFZHY/fmUR1ZOSYtelIRF1ClI5qi0tczZLoj++pgTywS4ogLXnCkCI9taW50LWFzc2V0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1ldGFkYXRhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEKAAAAAAAAADkmLXpSERdQpSOaotLXM2S6I/vqYE8sEuKIC15wpAiPAP+StsByXPBdRtHNYT39QMd24lVw8pjLR3CCVEmKr1g7PKaYzjMAlr5ul5nfXbLQ2TAaeuJb4pAZdfERPQGaQgiZw494mYFGIRW/0yPPriDe7c0IySjioWTLD42GWKnG1D0tU/wN0Anycqvhx+deJevyfNTjIGWL7nTBxUOh9CoM" + "data": "base64:AgAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP1pOx0CL6wWSI2/RS5Mx/Y8VxpRpyTgFE/165r4x0C22xq28oyz6/zeW/BJVur8nAmJroGDGw5NwXLxIjqjHuggdgxoqtgJiiUXF33ZfWhKlveKAB543FLs2Ke/YdtIkUNR+p6JkD0iLhK7TALh36zOF9zO5sahbg8ATSNavqDYXx6uux6BM+gUW6zuhVriFDr/+QVpmybpwMN9mBmx3x/fDHSTjtJw4+AF6U224RmShR36g4MXXh+hBaCbaQDTbRFn0UwO2yOPSIlt+D56OjVKP/Kog8Ohb4/2nvjmevoh3pdol0/DBp0D3i/DCCHCm9oibRQHdCEUpj+5ARelcO0J9mdn3s8pKWtQW1L5bFi6vweEAn3PfEu6+7Q81ZKIPQpIS1A4E3GrahTkh2H0w/fRATOkLWcLFpRX+rGPHacA8r17umTXvH4fA+vzF8K3zf8w4GHHnSir/LRgC1WteP88LEpZUyPW7/rZZBwUDG3rE0pOu+2bvI2prRRQ7ZanOIPch8IlVLs7qOWRBDJTvmdyTOBFPgLSvYg3qG4phnvLloOKbWtjIjzYwpWTViNZs1qpA0hkv5epX1UVyGCyv/y/5BnlJEDYkzKcPzKwHK3yAIKEnjVbCTvu+dN5qcdWvIScjcLFYqcujunww0VCMAsdpysJp8MgPsyO+4y2vLHWp7VHh2b/NOmwnQHUXo/8rRiMxWWwemeDj35TzjJ6BKgiXnxhw08YprgsYBHHz+52VooqRAjWAo/G2gYEI3eucSlOBiJFXeWHJ1e9PVEvgGFiCNfRo342GsiKqLIMTlvI+Bo/URX6WCv5bUYeLDtynUMgVw1DEvaNagXbpYbsFzwAhqAJuJehkBTURNnpmiYzuenT6pdvmXeCoL78CjD3iv4ByNpfM+ta2yUYCtiu2IEOEFDX/NYAYtzkYzONhflcNkLRu0s9g6gfjxEbovPkeof8aAK7iZSx4UgQUC2wDUsUESN+efNYSqB9wNW60jFZxZVBJ9yFb/9UW+ua7Hkfm21lRvhycJURtaW50LWFzc2V0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1ldGFkYXRhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAKgfcDVutIxWcWVQSfchW//VFvrmux5H5ttZUb4cnCVEAcIRZ+uF4fpDPOwbJ2QdUQHmflTG5iQ0CJ4nfEnLOiUHtfJe8bxCDhKOzUWPFQy8VwSm+prMv3gZ2I5yivMLoCmyIvpinF8SbTvdJoQIb2n4Kq3PVeqPrC3ixMW+FF0QBR9dJDPSE2Mi7VuQj/bjdLAcQmvGCCVqRfQ+fiowlB2po/741OH3QS4IAL51Jm7L17O5Qmum29AVWMWRR4qtIAU=" } ] } \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/getAccountTransactions.test.ts b/ironfish/src/rpc/routes/wallet/getAccountTransactions.test.ts index ba49a6b4a5..7f0b56ed93 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountTransactions.test.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountTransactions.test.ts @@ -11,7 +11,10 @@ import { } from '../../../testUtilities' import { createRouteTest } from '../../../testUtilities/routeTest' import { AsyncUtils } from '../../../utils' -import { GetAccountTransactionsResponse } from './getAccountTransactions' +import { + GetAccountTransactionsRequest, + GetAccountTransactionsResponse, +} from './getAccountTransactions' describe('Route wallet/getAccountTransactions', () => { const routeTest = createRouteTest(true) @@ -177,4 +180,70 @@ describe('Route wallet/getAccountTransactions', () => { expect(got).toEqual(expected) }) + + it('sorts transactions when sort is passed', async () => { + const node = routeTest.node + const account = await useAccountFixture(node.wallet, 'default-sort') + + const blockA = await useMinerBlockFixture(routeTest.chain, undefined, account, node.wallet) + await expect(node.chain).toAddBlock(blockA) + await node.wallet.updateHead() + + const blockB = await useMinerBlockFixture(routeTest.chain, undefined, account, node.wallet) + await expect(node.chain).toAddBlock(blockB) + await node.wallet.updateHead() + + const defaultSort: GetAccountTransactionsRequest = { + account: account.name, + } + + const defaultSortResponse = routeTest.client.request< + unknown, + GetAccountTransactionsResponse + >('wallet/getAccountTransactions', defaultSort) + + const defaultSortTransactions = await AsyncUtils.materialize( + defaultSortResponse.contentStream(), + ) + expect(defaultSortTransactions).toHaveLength(2) + expect(defaultSortTransactions[0].timestamp).toBeGreaterThan( + defaultSortTransactions[1].timestamp, + ) + + const reverseSort: GetAccountTransactionsRequest = { + account: account.name, + reverse: true, + } + + const reverseSortResponse = routeTest.client.request< + unknown, + GetAccountTransactionsResponse + >('wallet/getAccountTransactions', reverseSort) + + const reverseSortTransactions = await AsyncUtils.materialize( + reverseSortResponse.contentStream(), + ) + expect(reverseSortTransactions).toHaveLength(2) + expect(reverseSortTransactions[0].timestamp).toBeGreaterThan( + reverseSortTransactions[1].timestamp, + ) + + const forwardSort: GetAccountTransactionsRequest = { + account: account.name, + reverse: false, + } + + const forwardSortResponse = routeTest.client.request< + unknown, + GetAccountTransactionsResponse + >('wallet/getAccountTransactions', forwardSort) + + const forwardSortTransactions = await AsyncUtils.materialize( + forwardSortResponse.contentStream(), + ) + expect(forwardSortTransactions).toHaveLength(2) + expect(forwardSortTransactions[0].timestamp).toBeLessThan( + forwardSortTransactions[1].timestamp, + ) + }) }) diff --git a/ironfish/src/rpc/routes/wallet/getAccountTransactions.ts b/ironfish/src/rpc/routes/wallet/getAccountTransactions.ts index 54dae54c98..4e95aa401f 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountTransactions.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountTransactions.ts @@ -23,6 +23,7 @@ export type GetAccountTransactionsRequest = { sequence?: number limit?: number offset?: number + reverse?: boolean confirmations?: number notes?: boolean spends?: boolean @@ -37,6 +38,7 @@ export const GetAccountTransactionsRequestSchema: yup.ObjectSchema { }) }) + describe('with an invalid transferOwnershipTo', () => { + it('throws a validation error', async () => { + await expect( + routeTest.client.wallet.mintAsset({ + account: 'account', + fee: '1', + metadata: '{ url: hello }', + name: 'fake-coin', + value: '100', + transferOwnershipTo: 'abcdefghijklmnopqrstuvwxyz', + }), + ).rejects.toThrow( + 'Request failed (400) validation: transferOwnershipTo must be a valid public address', + ) + }) + }) + describe('with valid parameters', () => { it('returns the asset identifier and transaction hash', async () => { const node = routeTest.node const wallet = node.wallet const account = await useAccountFixture(wallet) + const accountB = await useAccountFixture(wallet, 'accountB') const block = await useMinerBlockFixture(routeTest.chain, undefined, account, node.wallet) await expect(node.chain).toAddBlock(block) await node.wallet.updateHead() const asset = new Asset(account.publicAddress, 'mint-asset', 'metadata') + const newOwner = accountB.publicAddress const mintData = { creator: asset.creator().toString('hex'), name: asset.name().toString('utf8'), metadata: asset.metadata().toString('utf8'), value: 10n, isNewAsset: true, + transferOwnershipTo: newOwner, } const mintTransaction = await useTxFixture(wallet, account, account, async () => { @@ -92,6 +112,7 @@ describe('Route wallet/mintAsset', () => { metadata: asset.metadata().toString('utf8'), name: asset.name().toString('utf8'), value: CurrencyUtils.encode(mintData.value), + transferOwnershipTo: newOwner, }) const walletTransaction = await account.getTransaction(mintTransaction.hash()) @@ -121,6 +142,7 @@ describe('Route wallet/mintAsset', () => { name: asset.name().toString('hex'), assetName: asset.name().toString('hex'), value: mintTransaction.mints[0].value.toString(), + transferOwnershipTo: newOwner, }) }) }) diff --git a/ironfish/src/rpc/routes/wallet/mintAsset.ts b/ironfish/src/rpc/routes/wallet/mintAsset.ts index c87c0b7438..3ac8f5720c 100644 --- a/ironfish/src/rpc/routes/wallet/mintAsset.ts +++ b/ironfish/src/rpc/routes/wallet/mintAsset.ts @@ -1,11 +1,16 @@ /* 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_METADATA_LENGTH, ASSET_NAME_LENGTH } from '@ironfish/rust-nodejs' +import { + ASSET_METADATA_LENGTH, + ASSET_NAME_LENGTH, + isValidPublicAddress, +} from '@ironfish/rust-nodejs' import * as yup from 'yup' 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' @@ -23,6 +28,7 @@ export interface MintAssetRequest { confirmations?: number metadata?: string name?: string + transferOwnershipTo?: string } export const MintAssetRequestSchema: yup.ObjectSchema = yup @@ -36,6 +42,7 @@ export const MintAssetRequestSchema: yup.ObjectSchema = yup confirmations: yup.number().optional(), metadata: yup.string().optional().max(ASSET_METADATA_LENGTH), name: yup.string().optional().max(ASSET_NAME_LENGTH), + transferOwnershipTo: yup.string().optional(), }) .defined() @@ -77,6 +84,13 @@ routes.register( const expirationDelta = request.data.expirationDelta ?? node.config.get('transactionExpirationDelta') + if ( + request.data.transferOwnershipTo && + !isValidPublicAddress(request.data.transferOwnershipTo) + ) { + throw new ValidationError('transferOwnershipTo must be a valid public address') + } + let options: MintAssetOptions if (request.data.assetId) { options = { @@ -87,6 +101,7 @@ routes.register( expirationDelta, value, confirmations: request.data.confirmations, + transferOwnershipTo: request.data.transferOwnershipTo, } } else { Assert.isNotUndefined(request.data.name, 'Must provide name or identifier to mint') @@ -102,6 +117,7 @@ routes.register( expirationDelta, value, confirmations: request.data.confirmations, + transferOwnershipTo: request.data.transferOwnershipTo, } } diff --git a/ironfish/src/sdk.ts b/ironfish/src/sdk.ts index 89305be567..e5d16f069c 100644 --- a/ironfish/src/sdk.ts +++ b/ironfish/src/sdk.ts @@ -1,6 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { BoxKeyPair } from '@ironfish/rust-nodejs' import { Config, ConfigOptions, @@ -367,4 +368,15 @@ Use 'ironfish config:set' to connect to a node via TCP, TLS, or IPC.\n`) return node } + + getPrivateIdentity(): PrivateIdentity | undefined { + const networkIdentity = this.internal.get('networkIdentity') + if ( + !this.config.get('generateNewIdentity') && + networkIdentity !== undefined && + networkIdentity.length > 31 + ) { + return BoxKeyPair.fromHex(networkIdentity) + } + } } diff --git a/ironfish/src/utils/asset.ts b/ironfish/src/utils/asset.ts index c0133c27dd..5f84157057 100644 --- a/ironfish/src/utils/asset.ts +++ b/ironfish/src/utils/asset.ts @@ -6,3 +6,5 @@ import { Asset } from '@ironfish/rust-nodejs' export function isNativeIdentifier(assetId: string): boolean { return Buffer.from(assetId, 'hex').equals(Asset.nativeId()) } + +export const NativeAssetId = Asset.nativeId().toString('hex') diff --git a/ironfish/src/wallet/interfaces/mintAssetOptions.ts b/ironfish/src/wallet/interfaces/mintAssetOptions.ts index 4ea8d22e1e..9d9b43fabb 100644 --- a/ironfish/src/wallet/interfaces/mintAssetOptions.ts +++ b/ironfish/src/wallet/interfaces/mintAssetOptions.ts @@ -11,6 +11,7 @@ export type MintAssetOptions = expirationDelta: number expiration?: number confirmations?: number + transferOwnershipTo?: string } | { assetId: Buffer @@ -20,4 +21,5 @@ export type MintAssetOptions = expirationDelta: number expiration?: number confirmations?: number + transferOwnershipTo?: string } diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index b0ef0544cd..fc8be1c317 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -910,6 +910,7 @@ export class Wallet { name: asset.name.toString('utf8'), metadata: asset.metadata.toString('utf8'), value: options.value, + transferOwnershipTo: options.transferOwnershipTo, } } else { mintData = { @@ -917,6 +918,7 @@ export class Wallet { name: options.name, metadata: options.metadata, value: options.value, + transferOwnershipTo: options.transferOwnershipTo, } } diff --git a/simulator/package.json b/simulator/package.json index 0ea8503c34..2ae30d805e 100644 --- a/simulator/package.json +++ b/simulator/package.json @@ -56,7 +56,7 @@ "docs:open": "open docs/index.html" }, "dependencies": { - "@ironfish/sdk": "1.10.0", + "@ironfish/sdk": "1.11.0", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", diff --git a/yarn.lock b/yarn.lock index 7c5f3093b0..682aa85903 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1652,41 +1652,6 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== -"@ironfish/sdk@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@ironfish/sdk/-/sdk-1.10.0.tgz#615f9ad5a07a1952e2ac62de3ea5dc0b9a701743" - integrity sha512-xI1/kfDKwXX3wJuKtpx5UlbpXTvATMM6P8sPhdnyKYW1FMAiOzLcv5lj6ZC09ip8zwBdfL6/bZJjWiDAh1JEhw== - dependencies: - "@ethersproject/bignumber" "5.7.0" - "@fast-csv/format" "4.3.5" - "@ironfish/rust-nodejs" "1.9.0" - "@napi-rs/blake-hash" "1.3.3" - axios "0.21.4" - bech32 "2.0.0" - blru "0.1.6" - buffer "6.0.3" - buffer-json "2.0.0" - buffer-map "0.0.7" - bufio "1.2.0" - colors "1.4.0" - consola "2.15.0" - date-fns "2.16.1" - decimal.js "10.4.3" - fastpriorityqueue "0.7.1" - imurmurhash "0.1.4" - level-errors "2.0.1" - leveldown "5.6.0" - levelup "4.4.0" - lodash "4.17.21" - node-datachannel "0.4.0" - node-forge "1.3.1" - parse-json "5.2.0" - sqlite "4.0.23" - sqlite3 "5.1.6" - uuid "8.3.2" - ws "8.12.1" - yup "0.29.3" - "@isaacs/string-locale-compare@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz#291c227e93fd407a96ecd59879a35809120e432b"