diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 92c98fd38c..ca601c5a55 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "0.1.64", + "version": "0.1.65", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -60,7 +60,7 @@ "@aws-sdk/client-cognito-identity": "3.215.0", "@aws-sdk/client-s3": "3.127.0", "@ironfish/rust-nodejs": "0.1.25", - "@ironfish/sdk": "0.0.41", + "@ironfish/sdk": "0.0.42", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index b893135662..d47f98af62 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -1,8 +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 { CurrencyUtils } from '@ironfish/sdk' +import { + CreateTransactionRequest, + CreateTransactionResponse, + CurrencyUtils, + RawTransactionSerde, + RpcResponseEnded, + Transaction, +} from '@ironfish/sdk' import { CliUx, Flags } from '@oclif/core' +import inquirer from 'inquirer' import { IronfishCommand } from '../../command' import { IronFlag, parseIron, RemoteFlags } from '../../flags' import { ProgressBar } from '../../types' @@ -48,6 +56,16 @@ export class Burn extends IronfishCommand { 'Minimum number of block confirmations needed to include a note. Set to 0 to include all blocks.', required: false, }), + rawTransaction: Flags.boolean({ + default: false, + description: + 'Return raw transaction. Use it to create a transaction but not post to the network', + }), + expiration: Flags.integer({ + char: 'e', + description: + 'The block sequence that the transaction can not be mined after. Set to 0 for no expiration.', + }), } async start(): Promise { @@ -108,22 +126,84 @@ export class Burn extends IronfishCommand { } let fee + let rawTransactionResponse: string if (flags.fee) { - fee = flags.fee + fee = CurrencyUtils.encode(flags.fee) + const createResponse = await client.createTransaction({ + sender: account, + receives: [], + burns: [ + { + assetId, + value: CurrencyUtils.encode(amount), + }, + ], + fee: fee, + expiration: flags.expiration, + confirmations: confirmations, + }) + rawTransactionResponse = createResponse.content.transaction } else { - const input = await CliUx.ux.prompt( - `Enter the fee amount in $IRON (min: ${CurrencyUtils.renderIron(1n)})`, + const feeRatesResponse = await client.estimateFeeRates() + const feeRates = new Set([ + feeRatesResponse.content.low ?? '1', + feeRatesResponse.content.medium ?? '1', + feeRatesResponse.content.high ?? '1', + ]) + + const feeRateNames = Object.getOwnPropertyNames(feeRatesResponse.content) + + const feeRateOptions: { value: number; name: string }[] = [] + + const createTransactionRequest: CreateTransactionRequest = { + sender: account, + receives: [], + burns: [ + { + assetId, + value: CurrencyUtils.encode(amount), + }, + ], + expiration: flags.expiration, + confirmations: confirmations, + } + + const allPromises: Promise>[] = [] + feeRates.forEach((feeRate) => { + allPromises.push( + client.createTransaction({ + ...createTransactionRequest, + feeRate: feeRate, + }), + ) + }) + + const createResponses = await Promise.all(allPromises) + createResponses.forEach((createResponse, index) => { + const rawTransactionBytes = Buffer.from(createResponse.content.transaction, 'hex') + const rawTransaction = RawTransactionSerde.deserialize(rawTransactionBytes) + + feeRateOptions.push({ + value: index, + name: `${feeRateNames[index]}: ${CurrencyUtils.renderIron(rawTransaction.fee)} IRON`, + }) + }) + + const input: { selection: number } = await inquirer.prompt<{ selection: number }>([ { - default: CurrencyUtils.renderIron(1n), - required: true, + name: 'selection', + message: `Select the fee you wish to use for this transaction`, + type: 'list', + choices: feeRateOptions, }, - ) + ]) - fee = await parseIron(input, { largerThan: 0n, flagName: 'fee' }).catch((error: Error) => - this.error(error.message), - ) + rawTransactionResponse = createResponses[input.selection].content.transaction } + const rawTransactionBytes = Buffer.from(rawTransactionResponse, 'hex') + const rawTransaction = RawTransactionSerde.deserialize(rawTransactionBytes) + if (!flags.confirm) { this.log(` You are about to burn: @@ -131,11 +211,16 @@ ${CurrencyUtils.renderIron( amount, true, assetId, -)} plus a transaction fee of ${CurrencyUtils.renderIron(fee, true)} with the account ${account} - -* This action is NOT reversible * +)} plus a transaction fee of ${CurrencyUtils.renderIron( + rawTransaction.fee, + true, + )} with the account ${account} `) + if (!flags.rawTransaction) { + this.log(`* This action is NOT reversible *\n`) + } + const confirm = await CliUx.ux.confirm('Do you confirm (Y/N)?') if (!confirm) { this.log('Transaction aborted.') @@ -143,6 +228,12 @@ ${CurrencyUtils.renderIron( } } + if (flags.rawTransaction) { + this.log(`Raw transaction: ${rawTransactionResponse}`) + this.log(`\nRun "ironfish wallet:post" to post the raw transaction. `) + this.exit(0) + } + const bar = CliUx.ux.progress({ barCompleteChar: '\u2588', barIncompleteChar: '\u2591', @@ -166,27 +257,30 @@ ${CurrencyUtils.renderIron( bar.stop() } + let transaction + try { - const result = await client.burnAsset({ - account, - assetId, - fee: CurrencyUtils.encode(fee), - value: CurrencyUtils.encode(amount), - confirmations, + const result = await client.postTransaction({ + transaction: rawTransactionResponse, + sender: account, }) stopProgressBar() - const response = result.content + const transactionBytes = Buffer.from(result.content.transaction, 'hex') + transaction = new Transaction(transactionBytes) + this.log(` -Burned asset ${response.assetId} from ${account} -Value: ${CurrencyUtils.renderIron(response.value)} +Burned asset ${assetId} from ${account} +Value: ${CurrencyUtils.renderIron(amount)} -Transaction Hash: ${response.hash} +Transaction Hash: ${transaction.hash().toString('hex')} -Find the transaction on https://explorer.ironfish.network/transaction/${ - response.hash - } (it can take a few minutes before the transaction appears in the Explorer)`) +Find the transaction on https://explorer.ironfish.network/transaction/${transaction + .hash() + .toString( + 'hex', + )}(it can take a few minutes before the transaction appears in the Explorer)`) } catch (error: unknown) { stopProgressBar() this.log(`An error occurred while burning the asset.`) diff --git a/ironfish-cli/src/commands/wallet/export.ts b/ironfish-cli/src/commands/wallet/export.ts index dc4e320532..6cff50723e 100644 --- a/ironfish-cli/src/commands/wallet/export.ts +++ b/ironfish-cli/src/commands/wallet/export.ts @@ -10,7 +10,13 @@ import jsonColorizer from 'json-colorizer' import path from 'path' import { IronfishCommand } from '../../command' import { ColorFlag, ColorFlagKey, RemoteFlags } from '../../flags' -import { LANGUAGE_KEYS, LANGUAGES, selectLanguage } from '../../utils/language' +import { + inferLanguageCode, + LANGUAGE_KEYS, + languageCodeToKey, + LANGUAGES, + selectLanguage, +} from '../../utils/language' export class ExportCommand extends IronfishCommand { static description = `Export an account` @@ -69,8 +75,14 @@ export class ExportCommand extends IronfishCommand { LANGUAGES[flags.language], ) } else if (flags.mnemonic) { - const language = await selectLanguage() - output = spendingKeyToWords(response.content.account.spendingKey, language) + let languageCode = inferLanguageCode() + if (languageCode !== null) { + CliUx.ux.info(`Detected Language as '${languageCodeToKey(languageCode)}', exporting:`) + } else { + CliUx.ux.info(`Could not detect your language, please select language for export`) + languageCode = await selectLanguage() + } + output = spendingKeyToWords(response.content.account.spendingKey, languageCode) } else if (flags.json) { output = exportPath ? JSON.stringify(response.content.account, undefined, ' ') diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index ff482c52a8..f8587f80ba 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -1,8 +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 { CurrencyUtils } from '@ironfish/sdk' +import { + CreateTransactionRequest, + CreateTransactionResponse, + CurrencyUtils, + RawTransactionSerde, + RpcResponseEnded, + Transaction, +} from '@ironfish/sdk' import { CliUx, Flags } from '@oclif/core' +import inquirer from 'inquirer' import { IronfishCommand } from '../../command' import { IronFlag, parseIron, RemoteFlags } from '../../flags' import { ProgressBar } from '../../types' @@ -60,6 +68,16 @@ export class Mint extends IronfishCommand { 'Minimum number of block confirmations needed to include a note. Set to 0 to include all blocks.', required: false, }), + rawTransaction: Flags.boolean({ + default: false, + description: + 'Return the raw transaction. Used to create a transaction but not post to the network', + }), + expiration: Flags.integer({ + char: 'e', + description: + 'The block sequence that the transaction can not be mined after. Set to 0 for no expiration.', + }), } async start(): Promise { @@ -95,6 +113,13 @@ export class Mint extends IronfishCommand { const confirmations = flags.confirmations + const expiration = flags.expiration + + if (expiration !== undefined && expiration < 0) { + this.log('Expiration sequence must be non-negative') + this.exit(1) + } + // We can assume the prompt can be skipped if at least one of metadata or // name is provided let isMintingNewAsset = Boolean(metadata || name) @@ -142,35 +167,105 @@ export class Mint extends IronfishCommand { } let fee + let rawTransactionResponse: string + if (flags.fee) { fee = flags.fee + + const createResponse = await client.createTransaction({ + sender: account, + receives: [], + mints: [ + { + assetId, + name, + metadata, + value: CurrencyUtils.encode(amount), + }, + ], + fee: CurrencyUtils.encode(fee), + expiration: expiration, + confirmations: confirmations, + }) + rawTransactionResponse = createResponse.content.transaction } else { - const input = await CliUx.ux.prompt( - `Enter the fee amount in $IRON (min: ${CurrencyUtils.renderIron(1n)})`, + const feeRatesResponse = await client.estimateFeeRates() + const feeRates = new Set([ + feeRatesResponse.content.low ?? '1', + feeRatesResponse.content.medium ?? '1', + feeRatesResponse.content.high ?? '1', + ]) + + const feeRateNames = Object.getOwnPropertyNames(feeRatesResponse.content) + + const feeRateOptions: { value: number; name: string }[] = [] + + const createTransactionRequest: CreateTransactionRequest = { + sender: account, + receives: [], + mints: [ + { + assetId, + name, + metadata, + value: CurrencyUtils.encode(amount), + }, + ], + expiration: expiration, + confirmations: confirmations, + } + + const allPromises: Promise>[] = [] + feeRates.forEach((feeRate) => { + allPromises.push( + client.createTransaction({ + ...createTransactionRequest, + feeRate: feeRate, + }), + ) + }) + + const createResponses = await Promise.all(allPromises) + createResponses.forEach((createResponse, index) => { + const rawTransactionBytes = Buffer.from(createResponse.content.transaction, 'hex') + const rawTransaction = RawTransactionSerde.deserialize(rawTransactionBytes) + + feeRateOptions.push({ + value: index, + name: `${feeRateNames[index]}: ${CurrencyUtils.renderIron(rawTransaction.fee)} IRON`, + }) + }) + + const input: { selection: number } = await inquirer.prompt<{ selection: number }>([ { - default: CurrencyUtils.renderIron(1n), - required: true, + name: 'selection', + message: `Select the fee you wish to use for this transaction`, + type: 'list', + choices: feeRateOptions, }, - ) + ]) - fee = await parseIron(input, { largerThan: 0n, flagName: 'fee' }).catch((error: Error) => - this.error(error.message), - ) + rawTransactionResponse = createResponses[input.selection].content.transaction } + const rawTransactionBytes = Buffer.from(rawTransactionResponse, 'hex') + const rawTransaction = RawTransactionSerde.deserialize(rawTransactionBytes) + if (!flags.confirm) { const nameString = name ? `Name: ${name}` : '' const metadataString = metadata ? `Metadata: ${metadata}` : '' const includeTicker = !!assetId const amountString = CurrencyUtils.renderIron(amount, includeTicker, assetId) - const feeString = CurrencyUtils.renderIron(fee, true) + const feeString = CurrencyUtils.renderIron(rawTransaction.fee, true) this.log(` You are about to mint ${nameString} ${metadataString} ${amountString} plus a transaction fee of ${feeString} with the account ${account} - -* This action is NOT reversible * `) + if (!flags.rawTransaction) { + this.log(`* This action is NOT reversible *\n`) + } + const confirm = await CliUx.ux.confirm('Do you confirm (Y/N)?') if (!confirm) { this.log('Transaction aborted.') @@ -178,6 +273,12 @@ ${amountString} plus a transaction fee of ${feeString} with the account ${accoun } } + if (flags.rawTransaction) { + this.log(`Raw transaction: ${rawTransactionResponse}`) + this.log(`\nRun "ironfish wallet:post" to post the raw transaction. `) + this.exit(0) + } + const bar = CliUx.ux.progress({ barCompleteChar: '\u2588', barIncompleteChar: '\u2591', @@ -201,30 +302,32 @@ ${amountString} plus a transaction fee of ${feeString} with the account ${accoun bar.stop() } + let transaction try { - const result = await client.mintAsset({ - account, - assetId, - fee: CurrencyUtils.encode(fee), - metadata, - name, - value: CurrencyUtils.encode(amount), - confirmations, + const result = await client.postTransaction({ + transaction: rawTransactionResponse, + sender: account, }) stopProgressBar() - const response = result.content + const transactionBytes = Buffer.from(result.content.transaction, 'hex') + transaction = new Transaction(transactionBytes) + + const minted = transaction.mints[0] + this.log(` -Minted asset ${response.name} from ${account} -Asset Identifier: ${response.assetId} -Value: ${CurrencyUtils.renderIron(response.value)} +Minted asset ${minted.asset.name().toString('hex')} from ${account} +Asset Identifier: ${minted.asset.id().toString('hex')} +Value: ${CurrencyUtils.renderIron(minted.value, true, minted.asset.id().toString('hex'))} -Transaction Hash: ${response.hash} +Transaction Hash: ${transaction.hash().toString('hex')} -Find the transaction on https://explorer.ironfish.network/transaction/${ - response.hash - } (it can take a few minutes before the transaction appears in the Explorer)`) +Find the transaction on https://explorer.ironfish.network/transaction/${transaction + .hash() + .toString( + 'hex', + )} (it can take a few minutes before the transaction appears in the Explorer)`) } catch (error: unknown) { stopProgressBar() this.log(`An error occurred while minting the asset.`) diff --git a/ironfish-cli/src/commands/wallet/post.ts b/ironfish-cli/src/commands/wallet/post.ts index dbe0caf98d..467351e3a4 100644 --- a/ironfish-cli/src/commands/wallet/post.ts +++ b/ironfish-cli/src/commands/wallet/post.ts @@ -5,6 +5,7 @@ import { CurrencyUtils, RawTransaction, RawTransactionSerde, Transaction } from import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' +import { ProgressBar } from '../../types' export class PostCommand extends IronfishCommand { static summary = 'Post a raw transaction' @@ -70,14 +71,36 @@ export class PostCommand extends IronfishCommand { } } - CliUx.ux.action.start(`Posting transaction`) + const bar = CliUx.ux.progress({ + barCompleteChar: '\u2588', + barIncompleteChar: '\u2591', + format: 'Posting the transaction: [{bar}] {percentage}% | ETA: {eta}s', + }) as ProgressBar + + bar.start() + + let value = 0 + const timer = setInterval(() => { + value++ + bar.update(value) + if (value >= bar.getTotal()) { + bar.stop() + } + }, 1000) + + const stopProgressBar = () => { + clearInterval(timer) + bar.update(100) + bar.stop() + } const response = await client.postTransaction({ transaction, sender: flags.account, offline: flags.offline, }) - CliUx.ux.action.stop() + + stopProgressBar() const posted = new Transaction(Buffer.from(response.content.transaction, 'hex')) diff --git a/ironfish-cli/src/trusted-setup/client.ts b/ironfish-cli/src/trusted-setup/client.ts index 25644860f6..0fba3b4168 100644 --- a/ironfish-cli/src/trusted-setup/client.ts +++ b/ironfish-cli/src/trusted-setup/client.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Assert, ErrorUtils, Event, Logger } from '@ironfish/sdk' +import { Assert, ErrorUtils, Event, Logger, MessageBuffer } from '@ironfish/sdk' import net from 'net' import { CeremonyClientMessage, CeremonyServerMessage } from './schema' @@ -10,6 +10,7 @@ export class CeremonyClient { readonly host: string readonly port: number readonly logger: Logger + readonly messageBuffer: MessageBuffer private stopPromise: Promise<{ stopRetries: boolean }> | null = null private stopResolve: ((params: { stopRetries: boolean }) => void) | null = null @@ -28,6 +29,7 @@ export class CeremonyClient { this.host = options.host this.port = options.port this.logger = options.logger + this.messageBuffer = new MessageBuffer('\n') this.socket = new net.Socket() this.socket.on('data', (data) => void this.onData(data)) @@ -87,30 +89,33 @@ export class CeremonyClient { } private onData(data: Buffer): void { - const message = data.toString('utf-8') - let parsedMessage - try { - parsedMessage = JSON.parse(message) as CeremonyServerMessage - } catch { - this.logger.debug(`Received unknown message: ${message}`) - return - } - - if (parsedMessage.method === 'joined') { - this.onJoined.emit({ - queueLocation: parsedMessage.queueLocation, - estimate: parsedMessage.estimate, - }) - } else if (parsedMessage.method === 'initiate-upload') { - this.onInitiateUpload.emit({ uploadLink: parsedMessage.uploadLink }) - } else if (parsedMessage.method === 'initiate-contribution') { - this.onInitiateContribution.emit(parsedMessage) - } else if (parsedMessage.method === 'contribution-verified') { - this.onContributionVerified.emit(parsedMessage) - } else if (parsedMessage.method === 'disconnect') { - this.onStopRetry.emit({ error: parsedMessage.error }) - } else { - this.logger.info(`Received message: ${message}`) + this.messageBuffer.write(data) + + for (const message of this.messageBuffer.readMessages()) { + let parsedMessage + try { + parsedMessage = JSON.parse(message) as CeremonyServerMessage + } catch { + this.logger.debug(`Received unknown message: ${message}`) + return + } + + if (parsedMessage.method === 'joined') { + this.onJoined.emit({ + queueLocation: parsedMessage.queueLocation, + estimate: parsedMessage.estimate, + }) + } else if (parsedMessage.method === 'initiate-upload') { + this.onInitiateUpload.emit({ uploadLink: parsedMessage.uploadLink }) + } else if (parsedMessage.method === 'initiate-contribution') { + this.onInitiateContribution.emit(parsedMessage) + } else if (parsedMessage.method === 'contribution-verified') { + this.onContributionVerified.emit(parsedMessage) + } else if (parsedMessage.method === 'disconnect') { + this.onStopRetry.emit({ error: parsedMessage.error }) + } else { + this.logger.info(`Received message: ${message}`) + } } } } diff --git a/ironfish-cli/src/trusted-setup/server.ts b/ironfish-cli/src/trusted-setup/server.ts index 0091853bb6..c9799609e2 100644 --- a/ironfish-cli/src/trusted-setup/server.ts +++ b/ironfish-cli/src/trusted-setup/server.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { S3Client } from '@aws-sdk/client-s3' import { verifyTransform } from '@ironfish/rust-nodejs' -import { ErrorUtils, Logger, SetTimeoutToken, YupUtils } from '@ironfish/sdk' +import { ErrorUtils, Logger, MessageBuffer, SetTimeoutToken, YupUtils } from '@ironfish/sdk' import fsAsync from 'fs/promises' import net from 'net' import path from 'path' @@ -71,6 +71,7 @@ class CeremonyServerClient { export class CeremonyServer { readonly server: net.Server readonly logger: Logger + readonly messageBuffer: MessageBuffer private stopPromise: Promise | null = null private stopResolve: (() => void) | null = null @@ -112,6 +113,7 @@ export class CeremonyServer { }) { this.logger = options.logger this.queue = [] + this.messageBuffer = new MessageBuffer('\n') this.host = options.host this.port = options.port @@ -245,60 +247,62 @@ export class CeremonyServer { private onError(client: CeremonyServerClient, e: Error): void { this.closeClient(client, e) - this.queue = this.queue.filter((c) => client.id === c.id) + this.queue = this.queue.filter((c) => client.id !== c.id) client.logger.info( `Disconnected with error '${ErrorUtils.renderError(e)}'. (${this.queue.length} total)`, ) } private async onData(client: CeremonyServerClient, data: Buffer): Promise { - const message = data.toString('utf-8') + this.messageBuffer.write(data) - const result = await YupUtils.tryValidate(CeremonyClientMessageSchema, message) - if (result.error) { - client.logger.error(`Could not parse client message: ${message}`) - this.closeClient(client, new Error(`Could not parse message`), true) - return - } + for (const message of this.messageBuffer.readMessages()) { + const result = await YupUtils.tryValidate(CeremonyClientMessageSchema, message) + if (result.error) { + client.logger.error(`Could not parse client message: ${message}`) + this.closeClient(client, new Error(`Could not parse message`), true) + return + } - const parsedMessage = result.result + const parsedMessage = result.result - client.logger.info(`Message Received: ${parsedMessage.method}`) + client.logger.info(`Message Received: ${parsedMessage.method}`) - if (parsedMessage.method === 'join' && !client.joined) { - if (Date.now() < this.startDate && parsedMessage.token !== this.token) { - this.closeClient( - client, - new Error( - `The ceremony does not start until ${new Date(this.startDate).toUTCString()}`, - ), - true, - ) - return - } + if (parsedMessage.method === 'join' && !client.joined) { + if (Date.now() < this.startDate && parsedMessage.token !== this.token) { + this.closeClient( + client, + new Error( + `The ceremony does not start until ${new Date(this.startDate).toUTCString()}`, + ), + true, + ) + return + } - this.queue.push(client) - const estimate = this.queue.length * (this.contributionTimeoutMs + this.uploadTimeoutMs) - client.join(parsedMessage.name) - client.send({ method: 'joined', queueLocation: this.queue.length, estimate }) - - client.logger.info(`Connected ${this.queue.length} total`) - void this.startNextContributor() - } else if (parsedMessage.method === 'contribution-complete') { - await this.handleContributionComplete(client).catch((e) => { - client.logger.error( - `Error handling contribution-complete: ${ErrorUtils.renderError(e)}`, - ) - this.closeClient(client, new Error(`Error generating upload url`)) - }) - } else if (parsedMessage.method === 'upload-complete') { - await this.handleUploadComplete(client).catch((e) => { - client.logger.error(`Error handling upload-complete: ${ErrorUtils.renderError(e)}`) - this.closeClient(client, new Error(`Error verifying contribution`)) - }) - } else { - client.logger.error(`Unknown method received: ${message}`) - this.closeClient(client, new Error(`Unknown method received`)) + this.queue.push(client) + const estimate = this.queue.length * (this.contributionTimeoutMs + this.uploadTimeoutMs) + client.join(parsedMessage.name) + client.send({ method: 'joined', queueLocation: this.queue.length, estimate }) + + client.logger.info(`Connected ${this.queue.length} total`) + void this.startNextContributor() + } else if (parsedMessage.method === 'contribution-complete') { + await this.handleContributionComplete(client).catch((e) => { + client.logger.error( + `Error handling contribution-complete: ${ErrorUtils.renderError(e)}`, + ) + this.closeClient(client, new Error(`Error generating upload url`)) + }) + } else if (parsedMessage.method === 'upload-complete') { + await this.handleUploadComplete(client).catch((e) => { + client.logger.error(`Error handling upload-complete: ${ErrorUtils.renderError(e)}`) + this.closeClient(client, new Error(`Error verifying contribution`)) + }) + } else { + client.logger.error(`Unknown method received: ${message}`) + this.closeClient(client, new Error(`Unknown method received`)) + } } } diff --git a/ironfish-cli/src/utils/language.ts b/ironfish-cli/src/utils/language.ts index 7cec7811e3..555cce8679 100644 --- a/ironfish-cli/src/utils/language.ts +++ b/ironfish-cli/src/utils/language.ts @@ -16,11 +16,23 @@ export const LANGUAGES = { Spanish: LanguageCode.Spanish, } as const +type LanguageCodeKey = keyof typeof LANGUAGE_CODES type LanguageKey = keyof typeof LANGUAGES export const LANGUAGE_KEYS = Object.keys(LANGUAGES) as Array export const LANGUAGE_VALUES = Object.values(LANGUAGES) as Array +const LANGUAGE_CODES = { + en: LanguageCode.English, + fr: LanguageCode.French, + it: LanguageCode.Italian, + ja: LanguageCode.Japanese, + ko: LanguageCode.Korean, + es: LanguageCode.Spanish, +} +const CHINESE_TRADITIONAL_CODES = ['zh-cht', 'zh-hant', 'zh-hk', 'zh-mo', 'zh-tw'] +const CHINESE_SIMPLIFIED_CODES = ['zh', 'zh-chs', 'zh-hans', 'zh-cn', 'zh-sg'] + export async function selectLanguage(): Promise { const response = await inquirer.prompt<{ language: LanguageKey @@ -34,3 +46,26 @@ export async function selectLanguage(): Promise { ]) return LANGUAGES[response.language] } + +export function inferLanguageCode(): LanguageCode | null { + const languageCode = Intl.DateTimeFormat().resolvedOptions().locale + if (languageCode.toLowerCase() in CHINESE_SIMPLIFIED_CODES) { + return LanguageCode.ChineseSimplified + } + if (languageCode.toLowerCase() in CHINESE_TRADITIONAL_CODES) { + return LanguageCode.ChineseTraditional + } + const simpleCode = languageCode?.split('-')[0].toLowerCase() + if (simpleCode && simpleCode in LANGUAGE_CODES) { + return LANGUAGE_CODES[simpleCode as LanguageCodeKey] + } + return null +} + +export function languageCodeToKey(code: LanguageCode): LanguageKey { + const key = Object.entries(LANGUAGES).find(([_, value]) => value === code)?.[0] + if (key) { + return key as LanguageKey + } + throw new Error(`No language key found for language code: ${code}`) +} diff --git a/ironfish/package.json b/ironfish/package.json index 6eac5c9b56..30df5d8757 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "0.0.41", + "version": "0.0.42", "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/rpc/index.ts b/ironfish/src/rpc/index.ts index 52d906eb7f..4c26cd7e40 100644 --- a/ironfish/src/rpc/index.ts +++ b/ironfish/src/rpc/index.ts @@ -7,3 +7,4 @@ export * from './response' export * from './routes' export * from './server' export * from './stream' +export * from './messageBuffer' diff --git a/ironfish/src/rpc/routes/wallet/getNotes.ts b/ironfish/src/rpc/routes/wallet/getNotes.ts index 3e58e7333f..627c14b8c4 100644 --- a/ironfish/src/rpc/routes/wallet/getNotes.ts +++ b/ironfish/src/rpc/routes/wallet/getNotes.ts @@ -2,7 +2,6 @@ * 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 * as yup from 'yup' -import { Assert } from '../../../assert' import { CurrencyUtils } from '../../../utils' import { ApiNamespace, router } from '../router' import { getAccount } from './utils' @@ -45,25 +44,30 @@ router.register => { const account = getAccount(node, request.data.account) - for await (const { note, spent, transactionHash } of account.getNotes()) { + for await (const transaction of account.getTransactionsByTime()) { if (request.closed) { break } - const transaction = await account.getTransaction(transactionHash) - Assert.isNotUndefined(transaction) + const notes = await account.getTransactionNotes(transaction.transaction) - const asset = await node.chain.getAssetById(note.assetId()) + for (const { note, spent } of notes) { + if (request.closed) { + break + } - request.stream({ - value: CurrencyUtils.encode(note.value()), - assetId: note.assetId().toString('hex'), - assetName: asset?.name.toString('hex') || '', - memo: note.memo(), - sender: note.sender(), - transactionHash: transaction.transaction.hash().toString('hex'), - spent, - }) + const asset = await node.chain.getAssetById(note.assetId()) + + request.stream({ + value: CurrencyUtils.encode(note.value()), + assetId: note.assetId().toString('hex'), + assetName: asset?.name.toString('hex') || '', + memo: note.memo(), + sender: note.sender(), + transactionHash: transaction.transaction.hash().toString('hex'), + spent, + }) + } } request.end() diff --git a/ironfish/src/wallet/__fixtures__/account.test.ts.fixture b/ironfish/src/wallet/__fixtures__/account.test.ts.fixture index 35eaf15c05..6909ef713e 100644 --- a/ironfish/src/wallet/__fixtures__/account.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/account.test.ts.fixture @@ -1314,5 +1314,167 @@ } ] } + ], + "Accounts connectTransaction should set new transaction timestamps equal to the block header timestamp": [ + { + "id": "a889c044-009a-46c1-8e63-44a39437fc7c", + "name": "accountA", + "spendingKey": "67946f64f0799104311425f6b64d0defe69978295d098782c2ffff88c870e007", + "incomingViewKey": "9a4efaf35ddb36ae551e222e525495fdeb6d830b9da7a51dcd7307a2899a5d04", + "outgoingViewKey": "3c121322ca6d05c877f690ce949d8c863439ee364cc8e9b839d1b0c9e0cb9674", + "publicAddress": "af4a1ab5eab88f2e5e051722008e77e6f9e8189e9282db72e2fe7ec212810431" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "D179D8B74987D6617267D46F4958554BA0DF02D7E5E6117DB02D6FF38FD0F6DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64://W+/tVZQ8brdkjzex6UZ/A0nGzqQoOm9TbtmPDfIAo=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:zUjcO75+znsUXeOAZwiTau5ALPInww5wKnL/vxknwnc=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1675465532435, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAjlDVgzHmchm0X0eCYZSan09wYPT3BK1Uvg5CZv1RE1SWk/zTWo/jtttxARXjj+HkMsVCmJ5flNivE+hZW69XXJ9sV0SXAKiDpxUy6dd22v+pDzcAHtVawYxLBPn/mm+h2zq35O5O/gqVz+XLO7F0YA/E0HrUAuA+O+FIEEnKUGoVGcN5b61I5hUkSiuVTT0V4djfGprtDeUyw0Apgyet9CUI4T9N6QPo/PhOuUQ0O62CKrcF8/3i0e6TvaUgv9y3Iz0J5w8Jtcd/1vVuTA1XEbTbhoyT6jTeS5khBK++j9wBihbstlWhmk+NTdMcE48XJgTMfcA/pmrGOgjfzfznG2M8lDdZur+k4xr0XTO0A0yVSMWKp2qPx6trSaO5MNIatGc7BBhos9+OAh9AYIl15+Ap4G5VTbaxubRu40Luar2ROjUNyoOw5dhy80cH7Lnq5q4lGynAg/hiagl/qNMcO94tbmCM5LD94A2605Zx96KQF9OWn9pUR0lLS1sdMRNCJV3iL7megkyU8hZWL7/u/HFPsR3vquDlOEnncv9zZ8HfzPgCGcz21RnU/IPq64wql+GnkCEIhSh2prT2vJ4ok8ER/G0bvWmxFbQSU98C9bD8rpBU00knyklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw95KCVySVMe8h4o47DFMz6gd3foIMqixTM5WU+uKAxYPrpXLxFB49nf8pY3QM5cSOPdT0uYvaNFPh+pdyq2XyAA==" + } + ] + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "B4718411696B7077570E35A64929B4C09D39C3C6D0D80F35A25BFAF43861A4B0", + "noteCommitment": { + "type": "Buffer", + "data": "base64:8K7WJhmBnQBb7h8ij+z6AJb2wX0r/yX903j6IRpgCjw=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:8MWmJsE2tP3gkSBtV6HT8bbxAeXWL2ivbEg+/rxwRGw=" + }, + "target": "880842937844725196442695540779332307793253899902937591585455087694081134", + "randomness": "0", + "timestamp": 1675465532993, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 5, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAfucUg8DnYxw9OOnt2njKIw2NHpG4aUZVn3TdEGC/TUKhWd30WPJpNm8P6IloJEDW0F9LWZ+8F7jNHx54ZurmOETgFkJrvy/yKiSlQvsXt/WhvG+DooRSMRjCHCpF44/TbRaN3wO9klZO/SPyhO6A1cPg4ciXdA7lcnQ0tt+z01cPTC9puL8/k2rD9f1hWfULE2Gq0UYcpG6Z/1gyf4QzZqR3RQR2qUIywb+CARKi6emVPv7fo/Fr63/1q/7YJ/W2PmWAl+hFDy+HjDQcVIazoTxyL8AjCPDspHqRLHzZwJusWESPZ9Iair1xJXf5+hhlOSxOkzSOT488gUeTCofvs4BWy2alUkmml4ITh9BJfq5HTCobsWclmSvxuWITms41FWwXdKSlpuc92PykMZVhWWhIHRq2+c+rEniFbjQ3JyrprN++s7RPWeupFangVXFQLdz2bO8Hab7oaZd+5nSrXaxeZDqhIsWhZUuPNRDqUaw/oWFFOzpMHPqbJ6O91FTLSfBf+NnZxnSSdmY8Wsk+hqfFqttGBFAQR4AvSL0iwmTI1jtEs7x/fhh0BzzGBdnrmXrPpxb3NtDzYGMN4A1jVgLtFVB+53Z73zwctaqguB3Vaq+xl6pNL0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwpoU71GWZM24Suek+FNk+eZ359e3G7LinNp49LXNvj1ixt+XWNntqXBGmJXr/fVUOPIr3I3v9AEXllggVirJXBQ==" + } + ] + }, + { + "header": { + "sequence": 4, + "previousBlockHash": "ED2A504BB3C6F2B8337F08AFE2155A5D9A84AB54CDCAC7A2E645468C244A8CEF", + "noteCommitment": { + "type": "Buffer", + "data": "base64:lffntJoY0efzzNzUpz2dOKfohMjQnG2fvMedp3QW4Ck=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:3yi9Fwpjc/X38Td5xR+Dtkz5UmSL+EvzWsll+3TFPgQ=" + }, + "target": "878277375889837647326843029495509009809390053592540685978895509768758568", + "randomness": "0", + "timestamp": 1675465535659, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 8, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/2vKiP////8AAAAAzCv6o1KeMPT1Mhuwn/I8kafG0LPLgv0tDWvxwQsYgpeqhXLAyFHkSs2cDfleT4gf+iIe9EkWFpvelZTMXUS92LJeQFN8s0G2JDd1E/mnN+CGvExxtAADPtZ5ozqyshLe5Bd7CSDehqzJPP4tRnovo13JMvipfyBTJjqMaZjGsRkIS12tL+0YKCuzb0KjB3UhOsE9ew3i1nTSKamZ4rys5gPi/AuHlXKjIpMAm4SdRNeX1p6CdraJuFWNPQFC3pHX8f7TC3Gz8w/KIfGh1rUDHpii44ChcEqJAaVG61yKGX8Duk6zpKmTgKm/jcLxN3dmDQmWeSVb1uSxBryJyaAkI8g/p31Q9GPkd0SZDKkRzTaw7rvVOeNF6sNMRqwO23gQdFL78lR++Qc5wNsC0lE2MoQOC2XV2Uhv1LIzwhKFb5rUb6AGiPZcv4W5nJIgZGdXCbQ7MFoXR1iFc9e3WhQ77Oh13t2D+Ixh8W8t/6js0+qiM97281VTFs4etsdu6Pm0gh2uThdotiUF2Oxn0b26yk/+LCkNNKOlUNwX4o2APG0YsIMS45TPf0u0W8ECMFCzKHxYpZztgxujLrQJKRjLKaPDDQMmqq0rCx7aLIdArBp5G+c7eUQjGUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw51JLDKzHrUKwGFAJIJsKtu/FQRb+zQjNwl7MO2Gii2qO6ppdAmOiN5ezVRAGTBnhpBsRYTZcjNJNB/r4eXuYAQ==" + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAApoPpXhkCBAzcmNe/yBgdpggupoNXZ/IKSZ3TK9h9FIK4Ri7lGnHfDhur2VI+2UQ748zQ78Wya5U+vhjVodD5iJk5+N+oyMSD8Rk4Ih2ApcuUTc+LpeJXKvn7OrpUHM72iiRaKx2E+tN447jtn5FIzhBWDIxwqj+MLITUNzKeK3QPHfAMx/tLsn3xH1wg+8WQadYeYjxYDDlcQ+EbG25QzWltcg4Cc/p2vBnE1XvWd8OBtHOK/GNimxJBGKyK8if0ZX0WNe5i6LIImjg2EPV75Z/ghJEaI1jOie72SoZn8DiDLFTMWwUzUpy7ctT+9pCRCrr+3tkebFbA7lM/g5gcE/Cu1iYZgZ0AW+4fIo/s+gCW9sF9K/8l/dN4+iEaYAo8BQAAAIE6YsM3fNcLl8fasufP+rLFkDcJjsBbvK96Of0dGkPA28Rm2x78wMNb6PBFp2P7yWywkQSGFEzb6h15WDg+2yxFVivSKsBI8PGc1ym8Kumli1VVVJQibUoB+5uddmCzBpkUxmIKY73cmcKlgGCyxCr+bSUT9DdKWMz4eU/xiVcAZWoQCDUxrFS5/aFkPivFSraCmdV2d/7i/flzHZ13oaG9iugtJvyYkZynXVAx2LdmJ9o1IznQ3KhG7BDwqhukihCw+kjAeXjXSoF1u2sxqzz6ujpunCA1Xq1vb3sMT6bnYgPeZKtaVLzpf+vpBJx+qqKaDnfO7ooDkByji9U2meauJzxZvIkC7+1xaRq1FXXzuTK2Iya9na+3OEUPp/51HmQIrUHAXdGhsMJTBSOhtzNL+WWZWAaJ2PwjEhkkx8w1UQDDIUzJmnmR4foxsFpD3mjJBSvfHIuy0pzav4aF4m255yqLr300aVUMbiped7n/f+JzYdubMyYXZ+vvZN2MRntR9llEiP5y3rJtEVyIf9jJjEigX2Xwta+CTS5xNgqXVfC9nwOeXU1hQ+tN4oxJVeZhyT+TBxHGv/hO3Ypk10bMs+vbAV+p06mu/htLYkIVvcPCJElMQK+LGh5xWNGiQmVUiLf3MjNT8uQTSm77U5rwQKrn6cHV2rHvtuA54F/ooFsEJu/K3GycqNTIOYEfqAEgQbFC/JZRWSpbWn/YYXHLXoL1PM1NE/C2YZqS/4X7RK/rWBoMxd+VwV1EiOtXL7nglqypvdFQpsKw9/nX4Y/vwh2xL3CsVEClqFovCBrhOoe9ghU+5zeLPdI7KmPK1T5fySFZIVRFafR6VsTzBrBjQa6oD6Dcap/dQnDpa0uDcEmJQ6TVHEOMj5JKyXtNvW2X/yxvOFLIkJXKNffbMEVrU7OI3ZjffTbtMoiUakD7ADbmf5ahd80U17rXUHtr767j4GnZRUFFi9Q7l4x+JbOYEgCRST1PQSQALo2DoeHz3R+Y3riTQ1e5Q4e4Rq5xWedZzZY4v9Umz7oTnIkJjQ+JD3wHxlnw1fuOA9Y1geZA0Dpt/Z7x/VGWyWV58qmK7Q36fMiRGQ3JHXgeDDddzCmocC5pVH3q1zTOD/TdgCYJ3TkOxCMqcHAEBIzXhn/+qoRj+m6dl+UP5njGR8wvX4NBZGA/wAQ31ZQBaSnotSpPJdv6uw/phKaJlK+VQqG2OGUYSGQp8A+hgRpq+TjgXRRqJRgd9mHeyu77m7r8257+V8L/sbt+E9y7S4JsIwcjzcka8enDe7IwlZ9vtyFgcO7iyxX8oDrqHyfl3yPdutjKR7XE5tzHDI6BJ3Yys/eZmv2GwqOW/lPrro+jDrbJORhcAlqIx7YGdn8jPmrWtwm1WgKbdclqmTG4Y/mf2HnwxBBqKBWhM38EJJnGYgsyDkab0GfheQtQFB0V6LgFQyu8rEFGxdmXhme4CuBVmJgGowsIoATM7OZ6Qz4HcIu+cchLrDFd5XT4zAaqZLNiMu0NH0JSYipRwJ/eGSm23bPMqu8n8sVx8DFYChiEX5E9qA2rNZsmyXn4jW0exMc+XSipjgK8pFdV9qIrwNbvDuFkDA==" + } + ] + } + ], + "Accounts connectTransaction should set preserve pending transaction timestamps": [ + { + "id": "0b82c0cd-6cd9-4212-ac2d-e09b5061d1cc", + "name": "accountA", + "spendingKey": "d821ed0881d727390949897a06a3c22e3eeb66a7dd84ea03807cac02147f078c", + "incomingViewKey": "5bea3a814d6d582fbe7e97fa13c20e8e002fd6dcf3ae03fa4929ddc91db17301", + "outgoingViewKey": "4c8c8924ba83ae6fe7908928e810b3bee2b5ea6a1800d1a99605c8838369cca3", + "publicAddress": "c4a266a299cdba6743cd76005f02c7ae0b401728f2f1ed53cd1174e5a3afe19c" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "D179D8B74987D6617267D46F4958554BA0DF02D7E5E6117DB02D6FF38FD0F6DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:c2xMWGQXT9uRfaFTd5wXUa7Lm9BFPCHDF1AhutOgNyU=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:LJbusJDL+F18zmTd6Dq1cjBH6KD1nW2GIvNais3ojXU=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1675465841474, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAfMYHHXTkiT2/Klh2bfE7xEJXgaK1uOPGMqlg724tp2uHrQd2M0ZbIPofi0/MDbLKyDZM8G4LzIhjLRa7p8+7XS69CTnyLS6OnWMcf0A+WDuxnJVj9y3F64IVorbG6QrtZvN+icgDKx0BZIPcLIia4WtUIYVFbs/fdrfaGlCDW/oLhhrlM91ZXhVuVxLYIQAv3zQ6KtLybjdUYGu8Fujy7coWfWkVobW/n98c+PLEKcKQsGm1LZI6jERFoJigyTj8bNze6LNoRJ/7ZaLQcglp9ppO71Lg361iXBadCehNLdJZRgthP3uVD9Bzxxmw5AhEKTDhEjdDSHFjNLHa0Pw0aS40WsTPieDQQyPH1NLILUDSbU46+Q/hHHNh9Eh2yzpzbKFNnl4MRtA4oeAmUfmfIG4w6IG997Fq7flRjh5m8mgSBK/DuNhPCngLgYWFpAzMDeZzPkd2XB2v8IlBhGh96Bgy16+00mcz6SaqXZuDIElNeTBoJz30uyQdN0ckAgkvpWHBnHMcN9AxR6yPrPQKySLJ+Q54keGBfDeKqf5lbpovxF6GdyVML0qMHYKqbx3iBHysAZuw7kQ1okUBZo7HJ2bwhVYWNXm+k1aPnWrcDIllE/ryiIpTfklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBJtHTbnDjP4xL8lBGOA3ra19IQ+u7PyjOEnZ6GtKyylns3GLaNkZmVTDiwDnfOtFbUiKQx3+MfKtqQOJ6HClBg==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "D8088A9FF8D7DFCD404475018A849E5523A960CC514BA12B03E9A8CF73E54C23", + "noteCommitment": { + "type": "Buffer", + "data": "base64:a1ycUM4DfHXiosHjavlUo2bnpK2gzs5hV/SGyi9/EUw=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:JX1yVo7rxKx6rahVi59inQ570imIvO/OR0VtO72rxiA=" + }, + "target": "880842937844725196442695540779332307793253899902937591585455087694081134", + "randomness": "0", + "timestamp": 1675465844055, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 7, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAoBU/cuasOdmau7EAnieNObCouJV5FGNjVZzG9D6U4iC3FmmdjOIgBI2V2K2CJZBLJptbTt32QCTa/Dt6K/EjMZX/3R04B5r1Vq0h7a/26dapnuBe6z7oJ065cLUeZPcchq7KVULeeuGvNr4cddJ/RmvCmFJyUxw2BKwessljz5IN9jw1+UyLTubkMhxp2oO8m8tCdkkMMiGzcoTmPgHI+dq7ZFhmBkGZgDBVz01DBlWxqv2WkRThUmVvfAc+Li3nKcb2gMDMUgpIzlr6aYhtx5XEYr3DpPAMdtmIfiPAekyqHhtYxxgVSnzxkSfwpCZ26u9eFOg2P/ig9c1M9dyZ4B7cRibVSGyZlYW7+VpkL2l9y54u61I4BDz0/16ovK80hquVhLxhKpAZU2Bzb1YKptJSx+F77TsmXsnnhMvmMAl4UnmDhZIOOyGgkJ/QvOgOLb0AGem7cDXi25QpAqRfPaOtARKC5ZWgC3DfY0vpy2rwwstsgTGL9aX03l4hfMMiSZIWhmchyCKBz5BTP47M79D+aQwvTSy9udm+SEJFJyawBtLY8PDQa8yw1dMgRZfCu3d9//CUHjBgRZhWxkbuTXYvjMVYiadtDg/WC8PNbXSqV/B6p58dh0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw6PQYH0OciQRLmelIgVLSSD2JeNI0b8lnPEhbZ/eyYzyFs3eGbSAL4fN4e5u2au0n4Cgs/kdAesyJYqhVaT7nDQ==" + }, + { + "type": "Buffer", + "data": "base64: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" + } + ] + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/account.test.ts b/ironfish/src/wallet/account.test.ts index c134f0fa02..c18e8be32f 100644 --- a/ironfish/src/wallet/account.test.ts +++ b/ironfish/src/wallet/account.test.ts @@ -8,6 +8,7 @@ import { createNodeTest, useAccountFixture, useBlockWithTx, + useBlockWithTxs, useMinerBlockFixture, useMintBlockFixture, useTxFixture, @@ -490,6 +491,57 @@ describe('Accounts', () => { expect(sequenceIndexEntry).toBeNull() }) + + it('should set new transaction timestamps equal to the block header timestamp', async () => { + const { node } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + + const block2 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block2) + await node.wallet.updateHead() + + const { block: block3, transactions } = await useBlockWithTxs(node, 1, accountA) + await node.chain.addBlock(block3) + await node.wallet.updateHead() + + expect(transactions.length).toBe(1) + + const transactionRecord = await accountA.getTransaction(transactions[0].hash()) + + Assert.isNotUndefined(transactionRecord) + + expect(transactionRecord.timestamp).toEqual(block3.header.timestamp) + }) + + it('should set preserve pending transaction timestamps', async () => { + const { node } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + + const block2 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block2) + await node.wallet.updateHead() + + const transaction = await useTxFixture(node.wallet, accountA, accountA) + + const pendingRecord = await accountA.getTransaction(transaction.hash()) + Assert.isNotUndefined(pendingRecord) + + expect(pendingRecord.sequence).toBeNull() + + const block3 = await useMinerBlockFixture(node.chain, 3, accountA, undefined, [ + transaction, + ]) + await node.chain.addBlock(block3) + await node.wallet.updateHead() + + const connectedRecord = await accountA.getTransaction(transaction.hash()) + Assert.isNotUndefined(connectedRecord) + + expect(connectedRecord.sequence).toEqual(block3.header.sequence) + expect(connectedRecord.timestamp).toEqual(pendingRecord.timestamp) + }) }) describe('disconnectTransaction', () => { diff --git a/ironfish/src/wallet/account.ts b/ironfish/src/wallet/account.ts index dd39b6e74e..77c98c574c 100644 --- a/ironfish/src/wallet/account.ts +++ b/ironfish/src/wallet/account.ts @@ -137,7 +137,7 @@ export class Account { const sequence = blockHeader.sequence const assetBalanceDeltas = new AssetBalances() let submittedSequence = sequence - let timestamp = new Date() + let timestamp = blockHeader.timestamp await this.walletDb.db.withTransaction(tx, async (tx) => { const transactionValue = await this.getTransaction(transaction.hash(), tx)