diff --git a/.github/workflows/deploy-node-aws.yml b/.github/workflows/deploy-node-aws.yml deleted file mode 100644 index 4ddb6a1ab6..0000000000 --- a/.github/workflows/deploy-node-aws.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Deploy Node Image to AWS -on: workflow_dispatch - -jobs: - Deploy: - name: Deploy - runs-on: ubuntu-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Login to AWS Registry - run: aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $AWS_REGISTRY_URL - env: - AWS_REGISTRY_URL: ${{ secrets.AWS_NODE_REGISTRY_URL }} - - - name: Build Node Image - run: ./ironfish-cli/scripts/build-docker.sh - - - name: Deploy Node Image - run: ./ironfish-cli/scripts/deploy-docker.sh - env: - REGISTRY_URL: ${{ secrets.AWS_NODE_REGISTRY_URL }} - PACKAGE_NAME: ironfish diff --git a/.github/workflows/deploy-node-docker-image.yml b/.github/workflows/deploy-node-docker-image.yml new file mode 100644 index 0000000000..f6001d6473 --- /dev/null +++ b/.github/workflows/deploy-node-docker-image.yml @@ -0,0 +1,120 @@ +name: Deploy Node Docker Image +on: + workflow_dispatch: + inputs: + github_tag_mainnet: + description: 'GitHub:mainnet' + type: boolean + default: false + github_tag_testnet: + description: 'GitHub:testnet' + type: boolean + default: false + aws_tag_mainnet: + description: 'AWS:mainnet' + type: boolean + default: false + aws_tag_testnet: + description: 'AWS:testnet' + type: boolean + default: false + aws_tag_git_sha: + description: 'AWS:{GIT_SHA}' + type: boolean + default: false + +permissions: + contents: read + packages: write + +jobs: + Deploy: + name: Deploy + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + + - name: Login to GitHub Registry + run: echo ${GITHUB_TOKEN} | docker login -u ${GITHUB_USER} --password-stdin ghcr.io + env: + GITHUB_USER: ${{ secrets.BREW_GITHUB_USERNAME }} + GITHUB_TOKEN: ${{ secrets.BREW_GITHUB_TOKEN }} + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Login to AWS Registry + run: aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $AWS_REGISTRY_URL + env: + AWS_REGISTRY_URL: ${{ secrets.AWS_NODE_REGISTRY_URL }} + + - name: Build Node Image + run: ./ironfish-cli/scripts/build-docker.sh + + - name: Deploy Node Image to GitHub:mainnet + if: ${{ inputs.github_tag_mainnet }} + run: | + docker tag ironfish ghcr.io/iron-fish/ironfish:mainnet + docker push ghcr.io/iron-fish/ironfish:mainnet + + # If we are deploying a new public release to mainnet + # also update the docker registry :latest tag for hygiene + - name: Deploy Node Image to GitHub:latest + if: ${{ inputs.github_tag_mainnet }} + run: | + docker tag ironfish ghcr.io/iron-fish/ironfish:latest + docker push ghcr.io/iron-fish/ironfish:latest + + # Used if we are deploying a new version (e.g. v1.1) + # This is only executed when deploying a new release to mainnet + - name: Deploy Node Image to GitHub:${{ github.ref_name }} + if: ${{ inputs.github_tag_mainnet && github.event.ref_type == 'tag'}} + run: | + docker tag ironfish ghcr.io/iron-fish/ironfish:${{ github.ref_name }} + docker push ghcr.io/iron-fish/ironfish:${{ github.ref_name }} + + - name: Deploy Node Image to GitHub:testnet + if: ${{ inputs.github_tag_testnet }} + run: | + docker tag ironfish ghcr.io/iron-fish/ironfish:testnet + docker push ghcr.io/iron-fish/ironfish:testnet + + - name: Deploy Node Image to AWS:mainnet + if: ${{ inputs.aws_tag_mainnet }} + run: | + docker tag ironfish ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:mainnet + docker push ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:mainnet + + # If we are deploying a new public release to mainnet + # also update the docker registry :latest tag for hygiene + - name: Deploy Node Image to AWS:latest + if: ${{ inputs.aws_tag_mainnet }} + run: | + docker tag ironfish ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:latest + docker push ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:latest + + # Used if we are deploying a new version (e.g. v1.1) + # This is only executed when deploying a new release to mainnet + - name: Deploy Node Image to AWS:${{ github.ref_name }} + if: ${{ inputs.aws_tag_mainnet && github.event.ref_type == 'tag'}} + run: | + docker tag ironfish ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:${{ github.ref_name }} + docker push ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:${{ github.ref_name }} + + - name: Deploy Node Image to AWS:testnet + if: ${{ inputs.aws_tag_testnet }} + run: | + docker tag ironfish ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:testnet + docker push ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:testnet + + - name: Deploy Node Image to AWS:${{ github.sha }} + if: ${{ inputs.aws_tag_git_sha }} + run: | + docker tag ironfish ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:${{ github.sha }} + docker push ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:${{ github.sha }} diff --git a/.github/workflows/deploy-node-github-beta.yml b/.github/workflows/deploy-node-github-beta.yml deleted file mode 100644 index 827ae85bbd..0000000000 --- a/.github/workflows/deploy-node-github-beta.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Deploy Node Image to Github Beta -on: workflow_dispatch - -permissions: - contents: read - packages: write - -jobs: - Deploy: - name: Deploy - runs-on: ubuntu-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - - - name: Login to Github Registry - run: echo ${GITHUB_TOKEN} | docker login -u ${GITHUB_USER} --password-stdin ghcr.io - env: - GITHUB_USER: ${{ secrets.BREW_GITHUB_USERNAME }} - GITHUB_TOKEN: ${{ secrets.BREW_GITHUB_TOKEN }} - - - name: Build Node Image - run: ./ironfish-cli/scripts/build-docker.sh - - - name: Deploy Node Image to Github - run: ./ironfish-cli/scripts/deploy-docker.sh - env: - REGISTRY_URL: ghcr.io/iron-fish - PACKAGE_NAME: ironfish-beta diff --git a/.github/workflows/deploy-node-github.yml b/.github/workflows/deploy-node-github.yml deleted file mode 100644 index b8cde49e28..0000000000 --- a/.github/workflows/deploy-node-github.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Deploy Node Image to Github -on: workflow_dispatch - -permissions: - contents: read - packages: write - -jobs: - Deploy: - name: Deploy - runs-on: ubuntu-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - - - name: Login to Github Registry - run: echo ${GITHUB_TOKEN} | docker login -u ${GITHUB_USER} --password-stdin ghcr.io - env: - GITHUB_USER: ${{ secrets.BREW_GITHUB_USERNAME }} - GITHUB_TOKEN: ${{ secrets.BREW_GITHUB_TOKEN }} - - - name: Build Node Image - run: ./ironfish-cli/scripts/build-docker.sh - - - name: Deploy Node Image to Github - run: ./ironfish-cli/scripts/deploy-docker.sh - env: - REGISTRY_URL: ghcr.io/iron-fish - PACKAGE_NAME: ironfish diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 7d4c4ab6bb..f39cb004e2 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "0.1.73", + "version": "0.1.74", "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-secrets-manager": "3.276.0", "@aws-sdk/s3-request-presigner": "3.127.0", "@ironfish/rust-nodejs": "0.1.29", - "@ironfish/sdk": "0.0.50", + "@ironfish/sdk": "0.0.51", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", diff --git a/ironfish-cli/scripts/deploy-docker.sh b/ironfish-cli/scripts/deploy-docker.sh deleted file mode 100755 index 0abbe01110..0000000000 --- a/ironfish-cli/scripts/deploy-docker.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -cd "$(dirname "$0")" - -if [ -z "${REGISTRY_URL-}" ]; then - echo "Set REGISTRY_URL before running deploy-docker.sh" - exit 1 -fi - -if [ -z "${PACKAGE_NAME-}" ]; then - echo "Set PACKAGE_NAME before running deploy-docker.sh" - exit 1 -fi - - -docker tag ironfish:latest ${REGISTRY_URL}/${PACKAGE_NAME}:latest -docker push ${REGISTRY_URL}/${PACKAGE_NAME}:latest - diff --git a/ironfish-cli/src/commands/ceremony/contributions.ts b/ironfish-cli/src/commands/ceremony/contributions.ts new file mode 100644 index 0000000000..0603987f5c --- /dev/null +++ b/ironfish-cli/src/commands/ceremony/contributions.ts @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { S3Client } from '@aws-sdk/client-s3' +import { Flags } from '@oclif/core' +import { IronfishCommand } from '../../command' +import { S3Utils } from '../../utils' + +export default class CeremonyContributions extends IronfishCommand { + static description = 'List all the current contributions with names' + + static flags = { + start: Flags.integer({ + required: false, + }), + end: Flags.integer({ + required: false, + }), + } + + async start(): Promise { + const { flags } = await this.parse(CeremonyContributions) + + const r2Credentials = await S3Utils.getR2Credentials() + + if (r2Credentials === undefined) { + this.logger.log('Failed getting R2 credentials from AWS') + this.exit(0) + return + } + + const r2Client = S3Utils.getR2S3Client(r2Credentials) + + const latestParamName = await this.getLatestParamName(r2Client, 'ironfish-contributions') + const latestParamNumber = parseInt(latestParamName.split('_')[1]) + const keys: string[] = [...new Array(latestParamNumber + 1)] + .map((_, i) => i) + .filter((i) => (!flags.start || i >= flags.start) && (!flags.end || i <= flags.end)) + .map((i) => { + return 'params_' + i.toString().padStart(5, '0') + }) + + for (const key of keys) { + const { Metadata } = await S3Utils.getObjectMetadata( + r2Client, + 'ironfish-contributions', + key, + ) + this.log( + `Contribution: ${key.split('_')[1]}, Name: ${Metadata?.contributorName || '-'}, IP: ${ + Metadata?.remoteaddress || '-' + }`, + ) + } + } + + async getLatestParamName(client: S3Client, bucket: string): Promise { + const paramFileNames = await S3Utils.getBucketObjects(client, bucket) + const validParams = paramFileNames + .slice(0) + .filter((fileName) => /^params_\d{5}$/.test(fileName)) + validParams.sort() + return validParams[validParams.length - 1] + } +} diff --git a/ironfish-cli/src/commands/ceremony.ts b/ironfish-cli/src/commands/ceremony/index.ts similarity index 98% rename from ironfish-cli/src/commands/ceremony.ts rename to ironfish-cli/src/commands/ceremony/index.ts index 53cdf21762..ab24059f59 100644 --- a/ironfish-cli/src/commands/ceremony.ts +++ b/ironfish-cli/src/commands/ceremony/index.ts @@ -8,9 +8,9 @@ import axios from 'axios' import fsAsync from 'fs/promises' import path from 'path' import { pipeline } from 'stream/promises' -import { IronfishCommand } from '../command' -import { DataDirFlag, DataDirFlagKey, VerboseFlag, VerboseFlagKey } from '../flags' -import { CeremonyClient } from '../trusted-setup/client' +import { IronfishCommand } from '../../command' +import { DataDirFlag, DataDirFlagKey, VerboseFlag, VerboseFlagKey } from '../../flags' +import { CeremonyClient } from '../../trusted-setup/client' export default class Ceremony extends IronfishCommand { static description = 'Contribute randomness to the Iron Fish trusted setup' diff --git a/ironfish-cli/src/commands/service/ceremony.ts b/ironfish-cli/src/commands/ceremony/service.ts similarity index 96% rename from ironfish-cli/src/commands/service/ceremony.ts rename to ironfish-cli/src/commands/ceremony/service.ts index f327301356..b5991d6bbe 100644 --- a/ironfish-cli/src/commands/service/ceremony.ts +++ b/ironfish-cli/src/commands/ceremony/service.ts @@ -13,7 +13,7 @@ const UPLOAD_TIMEOUT_MS = 5 * 60 * 1000 const PRESIGNED_EXPIRATION_SEC = 5 * 60 const START_DATE = 1681146000000 // Monday, April 10, 2023 10:00:00 AM GMT-07:00 (Pacific Daylight Time) -export default class Ceremony extends IronfishCommand { +export default class CeremonyService extends IronfishCommand { static hidden = true static description = ` @@ -68,7 +68,7 @@ export default class Ceremony extends IronfishCommand { } async start(): Promise { - const { flags } = await this.parse(Ceremony) + const { flags } = await this.parse(CeremonyService) const DEFAULT_HOST = '0.0.0.0' const DEFAULT_PORT = 9040 diff --git a/ironfish-cli/src/commands/chain/genesisadd.ts b/ironfish-cli/src/commands/chain/genesisadd.ts new file mode 100644 index 0000000000..477fae6281 --- /dev/null +++ b/ironfish-cli/src/commands/chain/genesisadd.ts @@ -0,0 +1,203 @@ +/* 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 { MEMO_LENGTH } from '@ironfish/rust-nodejs' +import { + addGenesisTransaction, + BlockSerde, + CurrencyUtils, + GenesisBlockAllocation, + IJSON, + isValidPublicAddress, +} from '@ironfish/sdk' +import { CliUx, Flags } from '@oclif/core' +import fs from 'fs/promises' +import { IronfishCommand } from '../../command' +import { LocalFlags } from '../../flags' + +export default class GenesisAddCommand extends IronfishCommand { + static hidden = true + + static flags = { + ...LocalFlags, + account: Flags.string({ + char: 'a', + required: true, + description: 'The name of the account to reallocate from', + }), + allocations: Flags.string({ + required: true, + description: + 'A CSV file with the format address,amountInIron,memo containing genesis block allocations', + }), + totalAmount: Flags.string({ + char: 'g', + required: true, + description: 'The total prior allocation to the given account', + }), + dry: Flags.boolean({ + default: false, + description: 'Display genesis block allocations without creating the genesis block', + }), + } + + async start(): Promise { + const { flags } = await this.parse(GenesisAddCommand) + + const node = await this.sdk.node() + await node.openDB() + + const account = node.wallet.getAccountByName(flags.account) + if (account === null) { + this.log(`Account ${flags.account} does not exist, make sure it is imported first`) + this.exit(0) + return + } + + const totalAmount = CurrencyUtils.decodeIron(flags.totalAmount) + const csv = await fs.readFile(flags.allocations, 'utf-8') + const result = parseAllocationsFile(csv) + + if (!result.ok) { + this.error(result.error) + } + + const totalSupply: bigint = result.allocations.reduce((prev, cur) => { + return prev + cur.amountInOre + }, 0n) + + if (totalSupply !== totalAmount) { + this.error( + `Allocations file contains ${CurrencyUtils.encodeIron( + totalSupply, + )} $IRON, but --totalAmount expects ${flags.totalAmount} $IRON.`, + ) + } + + const allocations: GenesisBlockAllocation[] = result.allocations + + // Log genesis block info + this.log(`Genesis block will be modified with the following values in a new transaction:`) + this.log(`Allocations:`) + const columns: CliUx.Table.table.Columns = { + identity: { + header: 'ADDRESS', + get: (row: GenesisBlockAllocation) => row.publicAddress, + }, + amount: { + header: 'AMOUNT ($IRON)', + get: (row: GenesisBlockAllocation) => { + return CurrencyUtils.encodeIron(row.amountInOre) + }, + }, + memo: { + header: 'MEMO', + get: (row: GenesisBlockAllocation) => row.memo, + }, + } + + CliUx.ux.table(allocations, columns, { + printLine: (line) => this.log(line), + }) + + // Display duplicates if they exist + const duplicates = getDuplicates(allocations) + if (duplicates.length > 0) { + this.log( + `\n/!\\ Allocations contains the following duplicate addresses. This will not cause errors, but may be a mistake. /!\\`, + ) + for (const duplicate of duplicates) { + this.log(duplicate) + } + this.log('\n') + } + + // Exit if dry run, otherwise confirm + if (flags.dry) { + this.exit(0) + } else { + const result = await CliUx.ux.confirm('\nCreate new genesis block? (y)es / (n)o') + if (!result) { + this.exit(0) + } + } + + this.log('\nBuilding a genesis block...') + const { block } = await addGenesisTransaction(node, account, allocations, this.logger) + + this.log(`\nGenesis Block`) + const serialized = BlockSerde.serialize(block) + this.log(IJSON.stringify(serialized, ' ')) + } +} + +const getDuplicates = (allocations: readonly GenesisBlockAllocation[]): string[] => { + const duplicateSet = new Set() + const nonDuplicateSet = new Set() + + for (const alloc of allocations) { + if (nonDuplicateSet.has(alloc.publicAddress)) { + duplicateSet.add(alloc.publicAddress) + } else { + nonDuplicateSet.add(alloc.publicAddress) + } + } + + return [...duplicateSet] +} + +const parseAllocationsFile = ( + fileContent: string, +): { ok: true; allocations: GenesisBlockAllocation[] } | { ok: false; error: string } => { + const allocations: GenesisBlockAllocation[] = [] + + let lineNum = 0 + for (const line of fileContent.split(/[\r\n]+/)) { + lineNum++ + if (line.trim().length === 0) { + continue + } + + const [address, amountInIron, memo, ...rest] = line.split(',').map((v) => v.trim()) + + if (rest.length > 0) { + return { + ok: false, + error: `Line ${lineNum}: (${line}) contains more than 3 values.`, + } + } + + // Check address length + if (!isValidPublicAddress(address)) { + return { + ok: false, + error: `Line ${lineNum}: (${line}) has an invalid public address.`, + } + } + + // Check amount is positive and decodes as $IRON + const amountInOre = CurrencyUtils.decodeIron(amountInIron) + if (amountInOre < 0) { + return { + ok: false, + error: `Line ${lineNum}: (${line}) contains a negative $IRON amount.`, + } + } + + // Check memo length + if (Buffer.from(memo).byteLength > MEMO_LENGTH) { + return { + ok: false, + error: `Line ${lineNum}: (${line}) contains a memo with byte length > ${MEMO_LENGTH}.`, + } + } + + allocations.push({ + publicAddress: address, + amountInOre: amountInOre, + memo: memo, + }) + } + + return { ok: true, allocations } +} diff --git a/ironfish-cli/src/commands/reset.ts b/ironfish-cli/src/commands/reset.ts index 69cedf0783..68b28f8152 100644 --- a/ironfish-cli/src/commands/reset.ts +++ b/ironfish-cli/src/commands/reset.ts @@ -21,6 +21,11 @@ export default class Reset extends IronfishCommand { [VerboseFlagKey]: VerboseFlag, [ConfigFlagKey]: ConfigFlag, [DataDirFlagKey]: DataDirFlag, + networkId: Flags.integer({ + char: 'i', + default: undefined, + description: 'Network ID of an official Iron Fish network to connect to', + }), confirm: Flags.boolean({ default: false, description: 'Confirm without asking', @@ -38,11 +43,21 @@ export default class Reset extends IronfishCommand { HOST_FILE_NAME, ) + const existingId = this.sdk.internal.get('networkId') + + let networkIdMessage = '' + if (flags.networkId != null && flags.networkId !== existingId) { + networkIdMessage = `\n\nThe network ID will be changed from ${existingId} to the new value of ${flags.networkId}` + } else { + networkIdMessage = `\n\nThe network ID will stay unchanged as ${existingId}` + } + const message = '\nYou are about to destroy your local copy of the blockchain. The following directories and files will be deleted:\n' + `\nBlockchain: ${chainDatabasePath}` + `\nHosts: ${hostFilePath}` + '\nYour wallet, accounts, and keys will NOT be deleted.' + + networkIdMessage + `\n\nAre you sure? (Y)es / (N)o` const confirmed = flags.confirm || (await CliUx.ux.confirm(message)) @@ -59,7 +74,9 @@ export default class Reset extends IronfishCommand { fsAsync.rm(hostFilePath, { recursive: true, force: true }), ]) - this.sdk.internal.set('networkId', this.sdk.config.defaults.networkId) + if (flags.networkId != null && flags.networkId !== existingId) { + this.sdk.internal.set('networkId', flags.networkId) + } this.sdk.internal.set('isFirstRun', true) await this.sdk.internal.save() diff --git a/ironfish-cli/src/commands/start.ts b/ironfish-cli/src/commands/start.ts index 6476ebfb5a..ad5a8f72a5 100644 --- a/ironfish-cli/src/commands/start.ts +++ b/ironfish-cli/src/commands/start.ts @@ -22,6 +22,8 @@ import { RpcTcpPortFlagKey, RpcTcpTlsFlag, RpcTcpTlsFlagKey, + RpcUseHttpFlag, + RpcUseHttpFlagKey, RpcUseIpcFlag, RpcUseIpcFlagKey, RpcUseTcpFlag, @@ -43,6 +45,7 @@ export default class Start extends IronfishCommand { [DataDirFlagKey]: DataDirFlag, [RpcUseIpcFlagKey]: { ...RpcUseIpcFlag, allowNo: true }, [RpcUseTcpFlagKey]: { ...RpcUseTcpFlag, allowNo: true }, + [RpcUseHttpFlagKey]: { ...RpcUseHttpFlag, allowNo: true }, [RpcTcpTlsFlagKey]: RpcTcpTlsFlag, [RpcTcpHostFlagKey]: RpcTcpHostFlag, [RpcTcpPortFlagKey]: RpcTcpPortFlag, diff --git a/ironfish-cli/src/commands/wallet/prune.ts b/ironfish-cli/src/commands/wallet/prune.ts index 38fbcebc1f..5e2e25e94a 100644 --- a/ironfish-cli/src/commands/wallet/prune.ts +++ b/ironfish-cli/src/commands/wallet/prune.ts @@ -65,6 +65,10 @@ export default class PruneCommand extends IronfishCommand { } } + CliUx.ux.action.start(`Cleaning up deleted accounts`) + await node.wallet.forceCleanupDeletedAccounts() + CliUx.ux.action.stop() + if (flags.compact) { CliUx.ux.action.start(`Compacting wallet database`) await node.wallet.walletDb.db.compact() diff --git a/ironfish-cli/src/commands/wallet/transactions.ts b/ironfish-cli/src/commands/wallet/transactions.ts index cd323ab604..f2146d20d5 100644 --- a/ironfish-cli/src/commands/wallet/transactions.ts +++ b/ironfish-cli/src/commands/wallet/transactions.ts @@ -24,6 +24,10 @@ export class TransactionsCommand extends IronfishCommand { char: 't', description: 'Transaction hash to get details for', }), + sequence: Flags.integer({ + char: 's', + description: 'Block sequence to get transactions for', + }), limit: Flags.integer({ description: 'Number of latest transactions to get details for', }), @@ -52,6 +56,7 @@ export class TransactionsCommand extends IronfishCommand { const response = client.getAccountTransactionsStream({ account, hash: flags.hash, + sequence: flags.sequence, limit: flags.limit, offset: flags.offset, confirmations: flags.confirmations, diff --git a/ironfish-cli/src/utils/s3.ts b/ironfish-cli/src/utils/s3.ts index 151b8b954f..3bde79813b 100644 --- a/ironfish-cli/src/utils/s3.ts +++ b/ironfish-cli/src/utils/s3.ts @@ -11,6 +11,8 @@ import { DeleteObjectCommand, DeleteObjectCommandOutput, GetObjectCommand, + HeadObjectCommand, + HeadObjectCommandOutput, ListObjectsCommand, ListObjectsCommandInput, PutObjectCommand, @@ -263,6 +265,16 @@ export function getDownloadUrl( return `https://${bucket}.${regionString}.amazonaws.com/${key}` } +export async function getObjectMetadata( + s3: S3Client, + bucket: string, + key: string, +): Promise { + const command = new HeadObjectCommand({ Bucket: bucket, Key: key }) + const response = await s3.send(command) + return response +} + export async function getBucketObjects(s3: S3Client, bucket: string): Promise { let truncated = true let commandParams: ListObjectsCommandInput = { Bucket: bucket } diff --git a/ironfish/package.json b/ironfish/package.json index 97d9f3dca5..1e40a1bd61 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "0.0.50", + "version": "0.0.51", "description": "SDK for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -40,7 +40,7 @@ "node-forge": "1.3.1", "parse-json": "5.2.0", "sqlite": "4.0.23", - "sqlite3": "5.0.4", + "sqlite3": "5.1.6", "uuid": "8.3.2", "ws": "8.12.1", "yup": "0.29.3" diff --git a/ironfish/src/fileStores/config.ts b/ironfish/src/fileStores/config.ts index 10989a78f3..8fa29a6683 100644 --- a/ironfish/src/fileStores/config.ts +++ b/ironfish/src/fileStores/config.ts @@ -148,7 +148,7 @@ export type ConfigOptions = { /** * The name of the account that the pool will use to payout from. */ - poolAccountName: string + poolAccountName?: string /** * Should pool clients be banned for perceived bad behavior @@ -316,7 +316,7 @@ export const ConfigOptionsSchema: yup.ObjectSchema> = yup minerBatchSize: YupUtils.isPositiveInteger, confirmations: YupUtils.isPositiveInteger, poolName: yup.string(), - poolAccountName: yup.string(), + poolAccountName: yup.string().optional(), poolBanning: yup.boolean(), poolHost: yup.string().trim(), poolPort: YupUtils.isPort, @@ -381,7 +381,7 @@ export class Config extends KeyStore { enableSyncing: true, enableTelemetry: false, enableMetrics: true, - getFundsApi: 'https://api.ironfish.network/faucet_transactions', + getFundsApi: 'https://testnet.api.ironfish.network/faucet_transactions', ipcPath: files.resolve(files.join(dataDir, 'ironfish.ipc')), logLevel: '*:info', logPeerMessages: false, @@ -408,7 +408,7 @@ export class Config extends KeyStore { blocksPerMessage: 25, minerBatchSize: 25000, poolName: 'Iron Fish Pool', - poolAccountName: 'default', + poolAccountName: undefined, poolBanning: true, poolHost: DEFAULT_POOL_HOST, poolPort: DEFAULT_POOL_PORT, diff --git a/ironfish/src/genesis/__fixtures__/genesis.test.slow.ts.fixture b/ironfish/src/genesis/__fixtures__/genesis.test.slow.ts.fixture index 1b89c652b4..7ec0c56675 100644 --- a/ironfish/src/genesis/__fixtures__/genesis.test.slow.ts.fixture +++ b/ironfish/src/genesis/__fixtures__/genesis.test.slow.ts.fixture @@ -11,5 +11,40 @@ "publicAddress": "3aade08766f374726252d244e44836524d4f89b0c46c56dcb5d3da8eedc0ac33", "createdAt": "2023-03-12T18:21:15.400Z" } + ], + "addGenesisTransaction Can create a new genesis block with an added transaction": [ + { + "version": 1, + "id": "4649f9ad-b454-49d7-946b-fc4b624e2457", + "name": "account1", + "spendingKey": "d4aada789ad4ee23fd610feef896a14bab2adb27cdfb81ad862a6c5479f6137e", + "viewKey": "341e035eb513031b7ee64c16844b163c0079e61f720e02ef50f7761e65866ad17336dc7c7a21cd59092c41a7aeb5a614fcbd9d16d28cf6c56b02daa125cc2336", + "incomingViewKey": "fe14049643a4f7a6df042b1d4612b27cdbdc46432d62d12e2c2655fa768a1001", + "outgoingViewKey": "35afb0f100570a51ab6f2497a6d3e836d5a570520edb27cd93025ed3b492ce6f", + "publicAddress": "6e00ecabbf8f50570b3462822d2f817e8bfe1d681ec5c2706cd39efdb9f3bb87", + "createdAt": "2023-03-21T21:39:39.238Z" + }, + { + "version": 1, + "id": "e55d269f-d79c-49db-84db-d1e5bae5b309", + "name": "account2", + "spendingKey": "fd210152449db354ed0fbe7ea915041728790185e89a0be2e428d453ac16245f", + "viewKey": "dbf261d0b5c1941050911d801ffba3e81877ebcd2d20aafccf2612a681eaa564dbb63e7b7aa552eca5539799811fdbc35e00b0507d34ea196b45ca2b2b1f0e9e", + "incomingViewKey": "3d46dbe6482a9e494c6187a7ba41d8b7aaace8e47e77f7c086e16e1f87c8f201", + "outgoingViewKey": "6d1423a452ffa1d10c220c92e493d368ac55f8e6317954602b74ea59a7a049d2", + "publicAddress": "82af4ab5a7c4889804ee685cc1cb0669e8a08ca480fe338a5f8d5bb26a114c5b", + "createdAt": "2023-03-21T21:39:39.239Z" + }, + { + "version": 1, + "id": "e6669000-72b0-4954-875d-6868375701ed", + "name": "account3", + "spendingKey": "f2eed26802b20071715b6a2b113c7df2ea2552cf1e45c65cfe7c2375656e397b", + "viewKey": "ea00a65b1e2bb1e53a9f92e1d13a264132ef4c791f6b0d42c8778538748b15048a9af9264eabcf4fbb470ce7ad2ac8955aeba1f72131df8058f31c6b4c99679c", + "incomingViewKey": "13bb18a8b53d8cfcc1cd69b00afcadc227a00599cd8081f4d8bd32e684e19900", + "outgoingViewKey": "b206c06a5b26c3ceae1c2189835c68cb7da06adf177f414a09d28e1b525bb5c8", + "publicAddress": "3233dd96ca906d54f76625f215ab43c1202345493dce2d85d48f71a2a2304599", + "createdAt": "2023-03-21T21:39:39.239Z" + } ] } \ No newline at end of file diff --git a/ironfish/src/genesis/addGenesisTransaction.ts b/ironfish/src/genesis/addGenesisTransaction.ts new file mode 100644 index 0000000000..555bb25904 --- /dev/null +++ b/ironfish/src/genesis/addGenesisTransaction.ts @@ -0,0 +1,146 @@ +/* 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, + Note as NativeNote, + Transaction as NativeTransaction, +} from '@ironfish/rust-nodejs' +import { Logger } from '../logger' +import { IronfishNode } from '../node' +import { Block, BlockHeader } from '../primitives' +import { transactionCommitment } from '../primitives/blockheader' +import { Transaction } from '../primitives/transaction' +import { CurrencyUtils } from '../utils' +import { Account } from '../wallet' +import { GenesisBlockAllocation } from './makeGenesisBlock' + +export async function addGenesisTransaction( + node: IronfishNode, + account: Account, + allocations: GenesisBlockAllocation[], + logger: Logger, +): Promise<{ block: Block }> { + logger = logger.withTag('addGenesisTransaction') + + if (!account.spendingKey) { + throw new Error('Must be a full account, not a view account') + } + + // Sum the allocations to get the total number of coins + const allocationSum = allocations.reduce((sum, cur) => sum + cur.amountInOre, 0n) + const allocationSumInIron = CurrencyUtils.encodeIron(allocationSum) + + logger.info('Generating a transaction for distributing allocations...') + + // Get a previous note owned by the given account from the existing genesis block + let note: NativeNote | null = null + let witness = null + const genesisTransactions = await node.chain.getBlockTransactions(node.chain.genesis) + for (const { transaction, initialNoteIndex } of genesisTransactions) { + let noteIndex = -1 + for (const encryptedNote of transaction.notes) { + noteIndex += 1 + // If this account can't decrypt this note, we can't use it + const decryptedNote = encryptedNote.decryptNoteForOwner(account.incomingViewKey) + if (decryptedNote == null) { + continue + } + + // If the nullifier has already been revealed, we can't use it + const nullifier = decryptedNote.nullifier( + account.viewKey, + BigInt(initialNoteIndex + noteIndex), + ) + if (await node.chain.nullifiers.contains(nullifier)) { + continue + } + + // We want the note with the exact value + if (decryptedNote.value() !== allocationSum) { + continue + } + + witness = await node.chain.notes.witness(initialNoteIndex + noteIndex) + note = decryptedNote.takeReference() + decryptedNote.returnReference() + break + } + + if (note != null) { + break + } + } + + if (note == null) { + throw new Error( + 'The given account does not have a suitable note to spend for the new allocations', + ) + } + + if (witness == null) { + throw new Error('The witness is missing, this should not happen') + } + + if (note.value() !== allocationSum) { + throw new Error('The value of the note to spend does not match the sum of the allocations') + } + + // Create the new transaction to be appended to the new genesis block + const transaction = new NativeTransaction(account.spendingKey) + logger.info(` Generating a spend for ${allocationSumInIron} coins...`) + transaction.spend(note, witness) + + for (const alloc of allocations) { + logger.info( + ` Generating an output for ${CurrencyUtils.encodeIron(alloc.amountInOre)} coins for ${ + alloc.publicAddress + }...`, + ) + const note = new NativeNote( + alloc.publicAddress, + BigInt(alloc.amountInOre), + alloc.memo, + Asset.nativeId(), + account.publicAddress, + ) + transaction.output(note) + } + + logger.info(' Posting the transaction...') + const postedTransaction = new Transaction(transaction.post(undefined, BigInt(0))) + + logger.info('Creating the modified genesis block...') + // Get the existing genesis block + const genesisBlock = await node.chain.getBlock(node.chain.genesis) + if (genesisBlock == null) { + throw new Error('An existing genesis block was not found') + } + + // Append the new transaction + genesisBlock.transactions.push(postedTransaction) + + // Add the new notes to the merkle tree + await node.chain.notes.addBatch(postedTransaction.notes) + + // Generate a new block header for the new genesis block + const noteCommitment = await node.chain.notes.rootHash() + const noteSize = await node.chain.notes.size() + const newGenesisHeader = new BlockHeader( + 1, + genesisBlock.header.previousBlockHash, + noteCommitment, + transactionCommitment(genesisBlock.transactions), + genesisBlock.header.target, + genesisBlock.header.randomness, + genesisBlock.header.timestamp, + genesisBlock.header.graffiti, + noteSize, + ) + + genesisBlock.header = newGenesisHeader + + logger.info('Block complete.') + return { block: genesisBlock } +} diff --git a/ironfish/src/genesis/genesis.test.slow.ts b/ironfish/src/genesis/genesis.test.slow.ts index 9e51514271..1c72d272ce 100644 --- a/ironfish/src/genesis/genesis.test.slow.ts +++ b/ironfish/src/genesis/genesis.test.slow.ts @@ -7,6 +7,7 @@ import { Target } from '../primitives/target' import { IJSON } from '../serde' import { createNodeTest, useAccountFixture } from '../testUtilities' import { acceptsAllTarget } from '../testUtilities/helpers/blockchain' +import { addGenesisTransaction } from './addGenesisTransaction' import { GenesisBlockInfo, makeGenesisBlock } from './makeGenesisBlock' describe('Read genesis block', () => { @@ -59,8 +60,7 @@ describe('Create genesis block', () => { const node = nodeTest.node const chain = nodeTest.chain - const amountNumber = 5n - const amountBigint = BigInt(amountNumber) + const amount = 5n // Construct parameters for the genesis block const account = await useAccountFixture(node.wallet, 'test') @@ -69,7 +69,7 @@ describe('Create genesis block', () => { target: Target.maxTarget(), allocations: [ { - amountInOre: amountNumber, + amountInOre: amount, publicAddress: account.publicAddress, memo: 'test', }, @@ -98,8 +98,8 @@ describe('Create genesis block', () => { // Check that the balance is what's expected await expect(node.wallet.getBalance(account, Asset.nativeId())).resolves.toMatchObject({ - confirmed: amountBigint, - unconfirmed: amountBigint, + confirmed: amount, + unconfirmed: amount, }) // Ensure we can construct blocks after that block @@ -136,8 +136,8 @@ describe('Create genesis block', () => { await expect( newNode.wallet.getBalance(accountNewNode, Asset.nativeId()), ).resolves.toMatchObject({ - confirmed: amountBigint, - unconfirmed: amountBigint, + confirmed: amount, + unconfirmed: amount, }) // Ensure we can construct blocks after that block @@ -150,3 +150,171 @@ describe('Create genesis block', () => { expect(newBlock).toBeTruthy() }) }) + +describe('addGenesisTransaction', () => { + const nodeTest = createNodeTest(false, { autoSeed: false }) + let targetMeetsSpy: jest.SpyInstance + let targetSpy: jest.SpyInstance + + beforeAll(() => { + targetMeetsSpy = jest.spyOn(Target, 'meets').mockImplementation(() => true) + targetSpy = jest.spyOn(Target, 'calculateTarget').mockImplementation(acceptsAllTarget) + }) + + afterAll(() => { + targetMeetsSpy.mockClear() + targetSpy.mockClear() + }) + + it('Can create a new genesis block with an added transaction', async () => { + // Initialize the database and chain + const originalNode = nodeTest.node + const originalChain = nodeTest.chain + + // Construct parameters for the genesis block + const account1Original = await useAccountFixture(originalNode.wallet, 'account1') + const account2Original = await useAccountFixture(originalNode.wallet, 'account2') + const account3Original = await useAccountFixture(originalNode.wallet, 'account3') + + const info: GenesisBlockInfo = { + timestamp: Date.now(), + target: Target.maxTarget(), + allocations: [ + { + amountInOre: 100n, + publicAddress: account1Original.publicAddress, + memo: 'account1', + }, + { + amountInOre: 100n, + publicAddress: account2Original.publicAddress, + memo: 'account2', + }, + ], + } + + // Build the original genesis block itself + const { block: originalBlock } = await makeGenesisBlock( + originalChain, + info, + originalNode.logger, + ) + + // Add the block to the chain + const originalAddBlock = await originalChain.addBlock(originalBlock) + expect(originalAddBlock.isAdded).toBeTruthy() + + const newAllocations = [ + { + amountInOre: 50n, + publicAddress: account1Original.publicAddress, + memo: 'account1', + }, + { + amountInOre: 25n, + publicAddress: account2Original.publicAddress, + memo: 'account2', + }, + { + amountInOre: 25n, + publicAddress: account3Original.publicAddress, + memo: 'account3', + }, + ] + + // Account 1: 100 in original allocation, but 50 used for the 2nd allocation + const account1Amount = 50n + // Account 2: 100 in the original allocation, and 25 in the 2nd allocation + const account2Amount = 125n + // Account 3: 25 in the 2nd allocation + const account3Amount = 25n + + // Build the modified genesis block + const { block } = await addGenesisTransaction( + originalNode, + account1Original, + newAllocations, + originalNode.logger, + ) + + // Compare the original parameters with the new one + expect(originalBlock.header.sequence).toEqual(block.header.sequence) + expect(originalBlock.header.previousBlockHash).toEqual(block.header.previousBlockHash) + expect(originalBlock.header.target).toEqual(block.header.target) + expect(originalBlock.header.randomness).toEqual(block.header.randomness) + expect(originalBlock.header.timestamp).toEqual(block.header.timestamp) + expect(originalBlock.header.graffiti).toEqual(block.header.graffiti) + expect(originalBlock.header.noteCommitment).not.toEqual(block.header.noteCommitment) + expect(originalBlock.header.noteSize).not.toEqual(block.header.noteSize) + expect(originalBlock.header.transactionCommitment).not.toEqual( + block.header.transactionCommitment, + ) + expect(originalBlock.transactions.length).not.toEqual(block.transactions.length) + + // Balance should still be zero, since generating the block should clear out + // any notes made in the process + await expect( + originalNode.wallet.getBalance(account1Original, Asset.nativeId()), + ).resolves.toMatchObject({ + confirmed: BigInt(0), + unconfirmed: BigInt(0), + }) + + // Create a new node + const { strategy, chain, node } = await nodeTest.createSetup() + + // Import accounts + const account1 = await node.wallet.importAccount(account1Original) + const account2 = await node.wallet.importAccount(account2Original) + const account3 = await node.wallet.importAccount(account3Original) + + // Next, serialize it in the same way that the genesis command serializes it + const serialized = BlockSerde.serialize(block) + const jsonedBlock = IJSON.stringify(serialized, ' ') + + // Deserialize the block and add it to the new chain + const result = IJSON.parse(jsonedBlock) as SerializedBlock + const deserializedBlock = BlockSerde.deserialize(result) + const addedBlock = await chain.addBlock(deserializedBlock) + expect(addedBlock.isAdded).toBe(true) + + await node.wallet.updateHead() + + // Check that the balance is what's expected + await expect(node.wallet.getBalance(account1, Asset.nativeId())).resolves.toMatchObject({ + confirmed: account1Amount, + unconfirmed: account1Amount, + }) + await expect(node.wallet.getBalance(account2, Asset.nativeId())).resolves.toMatchObject({ + confirmed: account2Amount, + unconfirmed: account2Amount, + }) + await expect(node.wallet.getBalance(account3, Asset.nativeId())).resolves.toMatchObject({ + confirmed: account3Amount, + unconfirmed: account3Amount, + }) + + // Ensure we can construct blocks after that block + const minersfee = await strategy.createMinersFee( + BigInt(0), + block.header.sequence + 1, + generateKey().spendingKey, + ) + const additionalBlock = await chain.newBlock([], minersfee) + expect(additionalBlock).toBeTruthy() + + // Validate parameters again to make sure they're what's expected + expect(deserializedBlock.header.sequence).toEqual(block.header.sequence) + expect(deserializedBlock.header.previousBlockHash).toEqual(block.header.previousBlockHash) + expect(deserializedBlock.header.target).toEqual(block.header.target) + expect(deserializedBlock.header.randomness).toEqual(block.header.randomness) + expect(deserializedBlock.header.timestamp).toEqual(block.header.timestamp) + expect(deserializedBlock.header.graffiti).toEqual(block.header.graffiti) + expect(deserializedBlock.header.noteCommitment).toEqual(block.header.noteCommitment) + expect(deserializedBlock.header.noteSize).toEqual(block.header.noteSize) + expect(deserializedBlock.header.transactionCommitment).toEqual( + block.header.transactionCommitment, + ) + expect(deserializedBlock.transactions.length).toEqual(block.transactions.length) + }) +}) diff --git a/ironfish/src/genesis/index.ts b/ironfish/src/genesis/index.ts index 354339927e..1268e948a9 100644 --- a/ironfish/src/genesis/index.ts +++ b/ironfish/src/genesis/index.ts @@ -1,4 +1,5 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +export * from './addGenesisTransaction' export * from './makeGenesisBlock' diff --git a/ironfish/src/mining/__fixtures__/poolShares.test.ts.fixture b/ironfish/src/mining/__fixtures__/poolShares.test.ts.fixture new file mode 100644 index 0000000000..339a77789b --- /dev/null +++ b/ironfish/src/mining/__fixtures__/poolShares.test.ts.fixture @@ -0,0 +1,145 @@ +{ + "poolShares start throws an error if the pool account does not exist": [ + { + "version": 1, + "id": "5a1bdef3-2427-4af8-94ba-443749031624", + "name": "default", + "spendingKey": "531e8b773625fcd70991248249478a2de78686bd396e2895e401ec0135207ae7", + "viewKey": "017bcd397bb5ad6db4edb02178cf59395036de0699d954eeaa798fb3d936534e0fb716e51a77faa6371056682d21515571c472e44374d76a4a2bf5020426e864", + "incomingViewKey": "88cb1b16fe5378dfe71a3a1a404b3ba9bd088f5630e9f022e71ef116b77b4f02", + "outgoingViewKey": "f948035367cf01e31b3e97c27971a0017aad7f37436f5160562d7480f3b80b7d", + "publicAddress": "07078da20fabde17a57a6817a05ee831231297e6c921dd9922bb443ebb084c6e", + "createdAt": "2023-03-28T18:56:33.898Z" + } + ], + "poolShares start throws an error if the node has no default account": [ + { + "version": 1, + "id": "93f21f31-97d7-430a-9ad7-1fc317e2c95f", + "name": "default", + "spendingKey": "aff4cdeec3453ce802eb9762f81495d0ab9ad60312ba7ecd3eba7d7d38c58bec", + "viewKey": "100d2a739a2b3dae04fae8ab5df71f82b03debf7b20cbd84311f8e0fa3e3e18201ff5cd74e7cc7fa396747dc1bc97cc3e520da90a70bf6d814abc0c49d8ba6e0", + "incomingViewKey": "36771801fd9013e4ce11a7ead1f2e83e3cc3c08da2ab7bdbcfeec3320689d003", + "outgoingViewKey": "e229860319f5819e58b8924d5b1169d05a8f307f15841c3383bdc44952bda3a2", + "publicAddress": "6af923f6e13d44a8fae22353e777a810c72658ba1ff3376d82c7efda2e98c284", + "createdAt": "2023-03-28T18:56:33.998Z" + } + ], + "poolShares start does not check for the pool account if payouts are disabled": [ + { + "version": 1, + "id": "6b5fa229-6982-4f07-a5f9-517c630c2431", + "name": "default", + "spendingKey": "3fa790052a9ea30b6380eda7a919c67b402640aeb585ae2d336d8efc4a46f97f", + "viewKey": "29fac8553d9b6ddf72b599b8c6871cc60b8c83fe060fec86f667b7015a52bfbf553c25116a17e38e740acd750fc52e2d0684bbcfaa9755a780245afba02dc536", + "incomingViewKey": "98ae255eb7d2d3c3c09d28a10367266bbf4725a82403346c9c4753c424c33b04", + "outgoingViewKey": "e4e4fc1a7332caa8cd7f6445ad9d37baea77febd4e7a80369965f14cae1d77eb", + "publicAddress": "5de4ccf2b1e52ab262c94ac632185113c6cf02034bdb74cf67008a2499dd1f6a", + "createdAt": "2023-03-28T18:56:34.078Z" + } + ], + "poolShares shareRate": [ + { + "version": 1, + "id": "9f47abc6-f258-45b6-bcc3-bc7076678f09", + "name": "default", + "spendingKey": "bc8e7b69924a69b6042689eb43fe42cf6287c0e596f87f7d784f6357d36183af", + "viewKey": "0fc5b1132874aad550309c52d9bee3fea06063e035d1ce1e0d57a87d36a4e2965d12e0ec13ec020d467dfbd13c3e56af2794e279757a8f73549c58f68fa77516", + "incomingViewKey": "74988c1e95baba38d11e8480a67f6f19cb2ce48b5d6f66ce8cd0d3bc8028a505", + "outgoingViewKey": "99311f7f7fe2be6bd18273b4f5bcadd154186a0fd8fdfb98338376a34cec98ac", + "publicAddress": "09a07f0f139ba7cbb86f039b946af8323df15f6fa050a789da256077a66cb65f", + "createdAt": "2023-03-28T18:56:34.180Z" + } + ], + "poolShares rolloverPayoutPeriod": [ + { + "version": 1, + "id": "572e5be7-0ee4-48e1-982e-99c61698d8de", + "name": "default", + "spendingKey": "3d24ec97e2031f4aec7919db0cea6969449e8c9f68300a96aad7fee2a2812936", + "viewKey": "cc94ab8a41a8bd52cca923597482a393676960a7a953ae67e3e5f66bcf859f39e55fa2e59dfe4c0715914b47c39a16d44a5bed5c818bc70c40f071752a2ca4ad", + "incomingViewKey": "e54c03259c8aee414f0d8faf7c8a7e9d37f0283304a46e411b9ca9b03c721606", + "outgoingViewKey": "bec7ec205015c107d2b5ac3635b134a970f13ac06a4c2bb59cbcc73199a643ef", + "publicAddress": "7ddc397fd073a020c719bf437dbe79979e7b647a664c866adc802868a6941af2", + "createdAt": "2023-03-28T18:56:34.275Z" + } + ], + "poolShares blocks": [ + { + "version": 1, + "id": "afd3224f-3dc1-44ee-a103-62f01c65eb2b", + "name": "default", + "spendingKey": "f66ca62ad63ddfbee8412cf0f4402a2a10d3d91be8f7c010c647df54365075d0", + "viewKey": "6114342be6dc527548af63e1a4d84581558c3eb3caebff02550795e1e1461817191f8c909d24cdf78235741025bc7f938d7bac28fa8f409db0ccced384fab265", + "incomingViewKey": "57f5753ddb12221b035be29ca2d49d4d075df0ec12378a904052567c45ffda03", + "outgoingViewKey": "62212d0d55d0670ed529296cb110e432b9a13b29b8704fb1820ab27cd7c3bea7", + "publicAddress": "77e4c3c83f945116ea148635d784c8d14d15ee7301f96b8fdf9fed5e56fd7e5e", + "createdAt": "2023-03-28T18:56:34.359Z" + } + ], + "poolShares transactions expected flow": [ + { + "version": 1, + "id": "6a068041-e48a-47ad-86e6-95f2295e5cfd", + "name": "default", + "spendingKey": "32c0103a18348fb542899913584ab84a86bc98b1b9d1035ceb55f043aad22b7f", + "viewKey": "7eb871f4c0e4a44899aa091380628f3ef45dbb53a3835c0d199d175a93a1ac3246247647cac27508e81d12b3690fe632c031d78a7567966f467470b7090954a2", + "incomingViewKey": "40599e3b0557fa85897fd85f5209c251ce8f0b5be4e97900b9521609dfccf506", + "outgoingViewKey": "0c5e3032ec4d0cc8c2af88a453bc917cf14f6234af9bea7133335caa7fde80d5", + "publicAddress": "c75bd238d5a9af5892a2bdacb745ffb67ed4bbb2d2d6cfc12019b93db55865d8", + "createdAt": "2023-03-28T18:56:34.442Z" + } + ], + "poolShares transactions expired transactions should mark shares unpaid": [ + { + "version": 1, + "id": "aef2cffb-6358-4629-8f4e-917ce44b09a7", + "name": "default", + "spendingKey": "a01969d8b4dd2b4c30df8f464d3705293245ad6845ab89a9f2c6846471a6cf37", + "viewKey": "33859c56600e8ab286c061a4ec120085aff6007f775a0c374aefeec0fcfe3561af8a75b4415ba72fc2485b6f90f31b2dfb4e3a5a6dfea594246c19ba3e56846e", + "incomingViewKey": "7fbaa9930f4b483f820231b85fb85ac15c4b39407873bc2e2c7315db08291d03", + "outgoingViewKey": "17af824a371e13303a629434d00d8dc6d5aea5e3a164de58eccbdb84728acd39", + "publicAddress": "c10c2205292a593edb44537ffcf9c44cfaf92b059813cf7ed23ae578b1c10a87", + "createdAt": "2023-03-28T18:56:34.521Z" + } + ], + "poolShares createNewPayout": [ + { + "version": 1, + "id": "d712c3df-0db9-42a8-97f9-2f15a63bac1d", + "name": "default", + "spendingKey": "d23957848ffb6be3ddf61dec1ef77e4e81bec21d746758684be4e28db1e222de", + "viewKey": "2aab3e747787622e0fc1ba0098baa6a2d2b64b69b0dcb735c7ad6943aa404b2dbbf0f389467f59d85213576ffd84e607d84f33abb02afec509ad71894b6f0fcc", + "incomingViewKey": "b4a6b99d8bfbb934338213b167d7cc0a7d3dc415b18e49e2276a4efd5966f704", + "outgoingViewKey": "e9afa6665bb97ed0776fab1bb4e6416a462b8f0e4901baf5e894e17a982eacb8", + "publicAddress": "adb5f969d451c8bc61fbbdf060a9c3811f1e0c6f65d53b01e9380bcb1b9dafbe", + "createdAt": "2023-03-28T18:56:34.610Z" + } + ], + "poolShares sendTransaction throws an error if no account exists with accountName": [ + { + "version": 1, + "id": "237da764-3517-4918-ad72-1ebd266f1bfc", + "name": "default", + "spendingKey": "2e9291d86ca8923508d41415550652b6fb465981f92df1de76e944f0a07eb855", + "viewKey": "c2a51237d16d9fb227ad71220f9b60c57159de61b586c4f5d2e7a6a73e14fd6bf81566233e7720f7e1dac0827d1544d8872f4e98abc8749e47b9293f398387a9", + "incomingViewKey": "93427a5f8fe5a292c2d7aa843927defc7acc17c54ed779019d9d211d4cb30307", + "outgoingViewKey": "426ef116bdb3122db8f885c35530ca70cbb30d40ccbc6bdc982c762aef07cce3", + "publicAddress": "f6ce419f01fafbc5d0c414585ad60f119f3730bb0003a21620a01fe135ed6c64", + "createdAt": "2023-03-28T18:56:34.692Z" + } + ], + "poolShares sendTransaction throws an error if node has no default account": [ + { + "version": 1, + "id": "53dc3274-1b49-41f7-b09e-f750a6a43e0f", + "name": "default", + "spendingKey": "860c781548b3c93cedecab564c421c3e27b1fa3fd493a640ceed68ba4b20c210", + "viewKey": "c3ac1585ca579939767db6401bbd08b27632e74f4ad2a38b314db2d226d42dcbbd74bd426cfc08c9c820181d099e47159a257b221abc9bf083bf8a1e21f4bdc8", + "incomingViewKey": "00bbe37cf0c662b65da944fadf06c32b009670bd68908d2b0332d7380f1d4202", + "outgoingViewKey": "ce406ac2a1cdc9a0227dd31a8ef918440a42c6d07801fc8157c435b737d7bc9b", + "publicAddress": "8ffe4bd431a9cf0979016b124f1c78533eb3ace86f874c3bfbfd3868fd95963f", + "createdAt": "2023-03-28T18:56:34.799Z" + } + ] +} \ No newline at end of file diff --git a/ironfish/src/mining/pool.ts b/ironfish/src/mining/pool.ts index 8d36ecdb2c..1685f7c6e0 100644 --- a/ironfish/src/mining/pool.ts +++ b/ironfish/src/mining/pool.ts @@ -159,7 +159,6 @@ export class MiningPool { this.stopPromise = new Promise((r) => (this.stopResolve = r)) this.started = true - await this.shares.start() this.logger.info(`Starting stratum server v${String(this.stratum.version)}`) await this.stratum.start() @@ -167,6 +166,10 @@ export class MiningPool { this.logger.info('Connecting to node...') this.rpc.onClose.on(this.onDisconnectRpc) + await this.startConnectingRpc() + + await this.shares.start() + const statusInterval = this.config.get('poolStatusNotificationInterval') if (statusInterval > 0) { this.notifyStatusInterval = setInterval( @@ -175,7 +178,6 @@ export class MiningPool { ) } - await this.startConnectingRpc() void this.eventLoop() } @@ -312,7 +314,7 @@ export class MiningPool { )}/s`, ) this.webhooks.map((w) => - w.poolSubmittedBlock(hashedHeaderHex, hashRate, this.stratum.clients.size), + w.poolSubmittedBlock(hashedHeaderHex, hashRate, this.stratum.subscribed), ) } else { this.logger.info(`Block was rejected: ${result.content.reason}`) diff --git a/ironfish/src/mining/poolShares.test.ts b/ironfish/src/mining/poolShares.test.ts index 6cf0ef81af..ebcdb6f3cb 100644 --- a/ironfish/src/mining/poolShares.test.ts +++ b/ironfish/src/mining/poolShares.test.ts @@ -6,7 +6,9 @@ import { Asset } from '@ironfish/rust-nodejs' import { LogLevel } from 'consola' import { Assert } from '../assert' import { createRootLogger } from '../logger' +import { useAccountFixture } from '../testUtilities/fixtures/account' import { createRouteTest } from '../testUtilities/routeTest' +import { Account } from '../wallet' import { MiningPoolShares } from './poolShares' describe('poolShares', () => { @@ -15,6 +17,10 @@ describe('poolShares', () => { beforeEach(async () => { const logger = createRootLogger().withTag('test') + + await useAccountFixture(routeTest.node.wallet, 'default') + await routeTest.wallet.setDefaultAccount('default') + logger.level = LogLevel.Silent shares = await MiningPoolShares.init({ rpc: routeTest.client, @@ -31,6 +37,42 @@ describe('poolShares', () => { await shares.stop() }) + describe('start', () => { + let defaultAccount: Account | null + + beforeEach(() => { + defaultAccount = routeTest.node.wallet.getDefaultAccount() + }) + + afterEach(async () => { + await routeTest.node.wallet.setDefaultAccount(defaultAccount?.name ?? null) + }) + + it('throws an error if the pool account does not exist', async () => { + shares['accountName'] = 'accountDoesNotExist' + + await expect(shares.start()).rejects.toThrow(new RegExp('account not found')) + }) + + it('throws an error if the node has no default account', async () => { + await routeTest.node.wallet.setDefaultAccount(null) + + await expect(shares.start()).rejects.toThrow( + new RegExp('no account is active on the node'), + ) + }) + + it('does not check for the pool account if payouts are disabled', async () => { + shares['enablePayouts'] = false + + const accountExists = jest.spyOn(shares, 'assertAccountExists') + + await shares.start() + + expect(accountExists).not.toHaveBeenCalled() + }) + }) + it('shareRate', async () => { jest.useFakeTimers({ legacyFakeTimers: false }) @@ -271,4 +313,46 @@ describe('poolShares', () => { jest.useRealTimers() }) + + describe('sendTransaction', () => { + let defaultAccount: Account | null + + beforeEach(() => { + defaultAccount = routeTest.node.wallet.getDefaultAccount() + }) + + afterEach(async () => { + await routeTest.node.wallet.setDefaultAccount(defaultAccount?.name ?? null) + }) + + it('throws an error if no account exists with accountName', async () => { + shares['accountName'] = 'fakeAccount' + + const output = { + publicAddress: 'testPublicAddress', + amount: '42', + memo: 'for testing', + assetId: 'testAsset', + } + + await expect(shares.sendTransaction([output])).rejects.toThrow( + new RegExp('No account with name'), + ) + }) + + it('throws an error if node has no default account', async () => { + await routeTest.node.wallet.setDefaultAccount(null) + + const output = { + publicAddress: 'testPublicAddress', + amount: '42', + memo: 'for testing', + assetId: 'testAsset', + } + + await expect(shares.sendTransaction([output])).rejects.toThrow( + new RegExp('No account is currently active on the node'), + ) + }) + }) }) diff --git a/ironfish/src/mining/poolShares.ts b/ironfish/src/mining/poolShares.ts index 4b727fdae9..17f2ec994a 100644 --- a/ironfish/src/mining/poolShares.ts +++ b/ironfish/src/mining/poolShares.ts @@ -22,7 +22,7 @@ export class MiningPoolShares { private poolName: string private recentShareCutoff: number - private accountName: string + private accountName: string | undefined private constructor(options: { db: PoolDatabase @@ -69,6 +69,10 @@ export class MiningPoolShares { } async start(): Promise { + if (this.enablePayouts) { + await this.assertAccountExists() + } + await this.db.start() } @@ -277,8 +281,22 @@ export class MiningPoolShares { assetId: string }[], ): Promise { + let account = this.accountName + + if (account === undefined) { + const defaultAccount = await this.rpc.getDefaultAccount() + + if (!defaultAccount.content.account) { + throw Error( + `No account is currently active on the node. Cannot send a payout transaction.`, + ) + } + + account = defaultAccount.content.account.name + } + const transaction = await this.rpc.sendTransaction({ - account: this.accountName, + account, outputs, fee: outputs.length.toString(), expirationDelta: this.config.get('transactionExpirationDelta'), @@ -286,4 +304,24 @@ export class MiningPoolShares { return transaction.content.hash } + + async assertAccountExists(): Promise { + if (this.accountName) { + const response = await this.rpc.getAccounts() + + const accountNames = response.content.accounts + + if (accountNames.find((accountName) => accountName === this.accountName) === undefined) { + throw Error( + `Cannot send pool payouts from account '${this.accountName}': account not found.`, + ) + } + } else { + const defaultAccount = await this.rpc.getDefaultAccount() + + if (defaultAccount.content.account === null) { + throw Error(`Cannot send pool payouts: no account is active on the node.`) + } + } + } } diff --git a/ironfish/src/mining/stratum/stratumServer.ts b/ironfish/src/mining/stratum/stratumServer.ts index cf4f441b71..1ed842ba2a 100644 --- a/ironfish/src/mining/stratum/stratumServer.ts +++ b/ironfish/src/mining/stratum/stratumServer.ts @@ -325,6 +325,11 @@ export class StratumServer { client.socket.removeAllListeners() client.close() + + if (client.subscribed) { + this.subscribed-- + } + this.clients.delete(client.id) this.peers.removeConnectionCount(client) } diff --git a/ironfish/src/rpc/adapters/httpAdapter.ts b/ironfish/src/rpc/adapters/httpAdapter.ts index ee096582a2..c26f3a89a0 100644 --- a/ironfish/src/rpc/adapters/httpAdapter.ts +++ b/ironfish/src/rpc/adapters/httpAdapter.ts @@ -170,14 +170,6 @@ export class RpcHttpAdapter implements IRpcAdapter { const url = new URL(request.url, `http://${request.headers.host || 'localhost'}`) const route = url.pathname.substring(1) - if (request.method !== 'POST') { - throw new ResponseError( - `Route does not exist, Did you mean to use POST?`, - ERROR_CODES.ROUTE_NOT_FOUND, - 404, - ) - } - // TODO(daniel): clean up reading body code here a bit of possible let size = 0 const data: Buffer[] = [] diff --git a/ironfish/src/rpc/routes/events/utils.ts b/ironfish/src/rpc/routes/events/utils.ts deleted file mode 100644 index 07c6f661f0..0000000000 --- a/ironfish/src/rpc/routes/events/utils.ts +++ /dev/null @@ -1,161 +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 { BufferSet } from 'buffer-map' -import { Assert } from '../../../assert' -import { Blockchain } from '../../../blockchain' -import { VerificationResult } from '../../../consensus' -import { createRootLogger, Logger } from '../../../logger' -import { BlockHeader } from '../../../primitives/blockheader' -import { HashUtils } from '../../../utils' - -const DEFAULT_OPTIONS = { - seq: true, - work: true, - indent: '|', -} - -export async function logChain( - chain: Blockchain, - start?: number | null, - end?: number | null, - options: { - prev?: boolean - merge?: boolean - seq?: boolean - work?: boolean - indent?: string - } = DEFAULT_OPTIONS, - logger?: Logger, -): Promise { - const content = await renderChain(chain, start, end, options, logger) - - if (logger) { - logger.info(content.join('\n')) - } else { - // eslint-disable-next-line no-console - console.log(content.join('\n')) - } -} - -export async function renderChain( - chain: Blockchain, - start?: number | null, - end?: number | null, - options: { - prev?: boolean - seq?: boolean - work?: boolean - indent?: string - } = DEFAULT_OPTIONS, - logger = createRootLogger(), -): Promise { - const content: string[] = [] - - let trees: VerificationResult = { valid: true } - if (chain.head) { - const headBlock = await chain.getBlock(chain.head) - Assert.isNotNull(headBlock) - trees = await chain.verifier.verifyConnectedBlock(headBlock) - } - - content.push( - '======', - `GENESIS: ${chain.genesis.hash.toString('hex') || '-'}`, - `HEAD: ${chain.head.hash.toString('hex') || '-'}`, - `LATEST: ${chain.latest.hash.toString('hex') || '-'}`, - `TREES: ${trees.valid ? 'OK' : `ERROR: ${String(trees.reason)}`}`, - '======', - ) - - start = start || chain.genesis.sequence - end = end || chain.latest.sequence - - const roots = await chain.getHeadersAtSequence(start) - - for (const root of roots) { - await renderGraph(chain, root, end, content, options, logger) - } - - return content -} - -export async function renderGraph( - chain: Blockchain, - header: BlockHeader, - end: number, - content: string[], - options: { - prev?: boolean - seq?: boolean - work?: boolean - indent?: string - } = DEFAULT_OPTIONS, - logger = createRootLogger(), - last = true, - _only = true, - indent = '', - seen = new BufferSet(), -): Promise { - Assert.isNotNull(chain.latest) - Assert.isNotNull(chain.head) - Assert.isNotNull(chain.genesis) - - seen.add(header.hash) - - let rendered = `+- Block ${HashUtils.renderHash(header.hash)}` - - if (options.seq) { - rendered += ` (${header.sequence})` - } - if (options.prev) { - rendered += ` prev: ${HashUtils.renderHash(header.previousBlockHash)}` - } - if (options.work) { - rendered += ` work: ${header.work.toString()}` - } - - if (header.hash.equals(chain.latest.hash)) { - rendered += ' LATEST' - } - if (header.hash.equals(chain.head.hash)) { - rendered += ' HEAD' - } - if (header.hash.equals(chain.genesis.hash)) { - rendered += ' GENESIS' - } - - content.push(indent + rendered) - - if (header.sequence === end) { - return - } - - const next = await chain.getHeadersAtSequence(header.sequence + 1) - const children = next.filter((h) => h.previousBlockHash.equals(header.hash)) - const nesting = children.length >= 2 - - const indentation = nesting ? options.indent || '' : '' - indent += last ? indentation : `| ${indentation}` - - for (let i = 0; i < children.length; i++) { - const child = children[i] - - if (seen.has(child.hash)) { - logger.error( - `ERROR FOUND LOOPING CHAIN ${header.hash.toString('hex')} -> ${child.hash.toString( - 'hex', - )}`, - ) - return - } - - const last = i === children.length - 1 - const only = children.length === 1 - await renderGraph(chain, child, end, content, options, logger, last, only, indent, seen) - - if (!last) { - content.push(indent + '') - } - } -} diff --git a/ironfish/src/rpc/routes/wallet/__fixtures__/getTransactions.test.ts.fixture b/ironfish/src/rpc/routes/wallet/__fixtures__/getTransactions.test.ts.fixture new file mode 100644 index 0000000000..525e32f09a --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__fixtures__/getTransactions.test.ts.fixture @@ -0,0 +1,166 @@ +{ + "Route wallet/getAccountTransactions streams the associated transaction for a hash": [ + { + "version": 1, + "id": "e6820aae-fdb7-4f7e-ad37-4577d35d34f7", + "name": "test", + "spendingKey": "46adeed9a5fc9854c8c757f32a865b38d6befecb817569f5078f9336bbcb2518", + "viewKey": "fb951df8a32c53e9b472ea2ed2f44cefd4bdb653ce54f0604d3a7ad1d3ae872f53f5fd5850583c3ad6cdcd32aff16c997379c7fce0ac1231d513edec91ba9aab", + "incomingViewKey": "d198e1cf3763f7344ef42f193e2117f5040bdb9b432fb289d03d0a999beb8b07", + "outgoingViewKey": "21818419721f891ee52a7c3a952a02ac824d4760324c515a8e24269a6ef6bb41", + "publicAddress": "4683eb58cde053d64008f4620c0e1b3064e245600478c15203d2dfb46cb0845e", + "createdAt": "2023-03-27T20:16:01.935Z" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "AD35193FA159CFD7440A3DFDFBE7ACB720CB4A44A441AB933071E2B9EF5DE90B", + "noteCommitment": { + "type": "Buffer", + "data": "base64:cFGj8ut1SP8xlBXESnfcasMbc+6NfOnQdPyjNnLspzk=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:Qg931VQjt6K+YYgJhLr4mRHFrEkXGNg2HLq2wBBmZbw=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1679948162767, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAkUVXL6bVAz64EgrJLj0qGJcgaH/qnBC3IllD+z3kGU+H6LKJeMHeA7YGQKght9VsFDaG5sRXvZ0ucsDEkVaO9FTjjcDB6i+XrFf/f1VzFCiQLjxB3zVF4N4USOl/h+3tJ34imK4Qpq47tR6mC3Zokj9ddvvYVB2gxAZIhka9ZzEH3/6rpS8ivSr9vQnzqYSdrLeq3YZU9RyQEBPE07SbbQfEZH0Lp7mu+FX/qjpANXm1gTIFOw8yb7uO/pK1zK+NdabHo+MvBhasE6FH55bZqSfGU/H0vQJY66bA5LqfUkbNrYRk7rGj98C1KqS03DJNIK0MxwIEhXRYEKNNxGLbF+bmxy7GUWSHoAFR+Wxg1lvUB0hxMXFNQfxOk5PgZuIrwNY6Vf0/3pEu4NnH+23g67nw8kvAoCtuw6mqvAbj1kROaoCPRcSKXMebXypy1FBqMHoomwCRugmFPGA+1yoR9bHCMytSq9ZeblyvDHn5/EoyTG+w119pDZ6ihjYDiyR6PRkZTTzAq9g0/E6U3CrqyMsSrAr4PQbZGg2OH6hZVfyfgqIOR3SsXFjcscrUVSzAywtj7tnlFskar463Ao7k7zmsVrReNOaE3M7NKcV1pZRINwi/0SYTCUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwfKRNJDYkoUAbpBbaEBUKBa9l8e7gwl2N46384PU5bqb/8tgqlmE/prQx1aKbmg0Qp5wU5qbqcJTqj/iFoVICCg==" + } + ] + } + ], + "Route wallet/getAccountTransactions throws a ValidationError for invalid sequences": [ + { + "version": 1, + "id": "6501d007-44a7-450e-b8f0-d055e9db17ec", + "name": "invalid-sequence", + "spendingKey": "5ccadae2a6e6110bab6aed559ec39c3157926131fe25e51efd437934b1eadee3", + "viewKey": "0ff3ac0f0578cf0f8b96ab3de8f54f3f74f43c635f4b95fa66253c6eb5d089b9d10afa510d58a5b46d8d0864d63cd8531090fd8b9ea4309b95eaf75beccab3cb", + "incomingViewKey": "2711cbdfa86672463795b75bac77477071815b8762c0fa243e65f446fa318a06", + "outgoingViewKey": "1b1356b74634482e815742aa98658aa8495defe69a1074367819e3d66a70c27a", + "publicAddress": "a95fdcee694e300717477a970bb516549e3c37948a5a456e4dae4ba22d29ac84", + "createdAt": "2023-03-27T20:22:20.665Z" + } + ], + "Route wallet/getAccountTransactions streams back transactions for a given block sequence": [ + { + "version": 1, + "id": "e0f23e23-3adf-4c11-bf7c-dca8fe57b5b1", + "name": "valid-sequence", + "spendingKey": "b55766d1aa6a79941b54f06748860d8cb7c2f447716a1abcc8348932e8303ff2", + "viewKey": "eadcd8e6b7001ca44260e339b6534bdb250848735cdb3a5bd975f2ff252a4386937f8f07402bc7dbd6ce3058b1206ccc1f3a367ab845b5e7d629f98c4c38048e", + "incomingViewKey": "7096c396949106cee695136e3429c4511093624a499a04ac701182ead15c8903", + "outgoingViewKey": "40b83f94de4c1bd9915623ce5320a310105fe3bb0df0c1ac0b26a2415d16a9f6", + "publicAddress": "0f6e67528f733e8a043f29862a62e6c6e880f3fbc9f8ba5e218a25475f09a568", + "createdAt": "2023-03-27T20:28:00.063Z" + }, + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXAndxXKxlFxKHXi0W/9W0QeAVt/4pOZyjl/ENWieiCnDo4eoN/fpczM9UwWipcPskKBiHzGQlxWL0Mw+624qJRziL02bHXyjjeRlw0+qEWjrO1RoPflb1Bxh1FVktS/9GJTrxJ7MyYiFwUa8CWU1iS6YoeoUmh3fvdG5VNS5j8TeMB6SvKkjVOgY5Z9n2tA3LSDaFEHU4w2h40lniOZGFqvdXZMo7CaXKuo+KdsqCKH6t1joeAcRcGsH7vau81gDYeahvb/Qwz5fJYrWo6/9Sh0DLjJjczQTJlHaHLbjbMvpaaTzfOiUk8r1h64irRt+QIxUPMeuqvUIJJ7iYP22u8SNfV3MEGy/NKmp6Uyyebg4NrmXyucavcykZqUNVcck0MFPFhAK3fNZjvCWGe6o8F23j6gAvPS2iQQHA72lgwqZ9BNdnECeeft1tJP37nbGbYVUyyqy47rqBJylAmmnaYPsGkQJ7x2BsiotpaNi/dyGMW2misue6C1QLn08qLRXWIsN8JdMGRCWPeoez6cToZHerQ3wIz4bcR/Y0/rq6hA+6AUai9YpxCw7nu5UxdcEmOwUgaUzoIZfUpbam+vLYj0hyzsbKPgrKQypE8GPK75lTbpiOsReF6GQgFrE53rkMK278tnXjpmExV1IxQnwaGfKmJMIc4Bv4V+2un8abxeM+Gdvuq00u8XQTgzSUDSUOQEJCQ+g5pDYPptqZVnFNY3HqjYBC3TgPNUJRAvGHE1kvcpIJaBZGUfngCXjJ8soL+N+q8Pv8IjWDDjZUeo3KiHFQj35vckqycNVcrO/yfijk6XRSDRddM2LLlK5ymdrWX7aqEU/6H9BWg5vhWUSxjWjfwOILtCAI/3roG1gmSneL0iSur0WhfpUbqsFuXZcf0L8j/JKcj5uOOUr+v+Uf7dBjoKvtDBjmetF6jjwwUhqI9ktHaBwrbIi4ycVM24SXrG9xW+Hu0NMCyQ6jpGjQpUIpaXWFgSD25nUo9zPooEPymGKmLmxuiA8/vJ+LpeIYolR18JpWhhc3NldAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1ldGFkYXRhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAA8jjq8G3VCfy9/AHJVx0ujh1CGz2oVGoPqPAvzOKQipLFsjoaxGnUpF7MPY6faSsoF2t4voAkoPa6NQ/KPZNUGhDeuaEb5urN+G1mAXGvNOLyAjyBGWw9S8IdBraQGKVBadb41+KZhjuzQ46wvwjCBJ5Q769rYZ6crknbDeeiHCQ==" + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "ED964164271306033EDE335A790187B1A8CFE1CB9C98EF1D3C29E4E137B36D3F", + "noteCommitment": { + "type": "Buffer", + "data": "base64:3GBQZYWT51ILz91p1VwGEuhzVbTb2gk5NCuuwmYAAVA=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:TRhE1SC9sLWsOuv2GQ4sA/F6fRwutbFAwG0oi3/q180=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1679948881317, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 6, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAABzg9kMXIkUMO5MadBZFA3n/GCEHY1JA9AbAOouvFywujG/KtuEjIN5OKkACS+DqngjwVUt1CY+O9zRSH2l5qpc4gXbKwFVzqxwCNqKw8lNKJ3lYdDZtkjFKi9L/jyFsK0AHTsVve7KsZ9LlYqaErwu/MWXw6qZIwTYJ5sMgEIiAHARBbg9jR0RDfgkJDkD1vag4nTfwoT36OoPEVe/MCXXIXZr3e47IA/KFEExmme+WSLxndtf96oFnK1Pou5wcP1hUQuEhR/Ly1W75oReQ1mR2bT2rxqFrH6f4xGbmvFwBbare5xAXLXynJnAhsAsg0ksijV4I7VhEdxR0pIfwNGYjtLcW66GPt9GKPGu9/meKadCLPSyKGpJNSIdI9SJ9AabXu9o5S4MgYD+mbUl5PBYc+geu0ZlBU5RYtsF/gGre4hWkdbjr+UFjZn41zQs2GpUfTU8vLedrnLe5+JQ1T9Rl1Ny5DbGb2VsaflcKT5XbsZXSUnY0sTM+PrVkHI5WDLN4DB8VwS+A9vr4sQkIWpmIAst6iXOJr8LejUODjijbJV05Lv2BFWftVAAoGM0mLH2uxoJiT/Fu8yJSMuAPwdKYXj18qvdV5FDwLI9AaPj2fR7JHU6LAzElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwKyyO5I7zvsXWdbql4qDucBvGrrbqh0e5ElmH8Q13Koxco1Qq7+YghZxbktvPmNU+2frl8SZpUHfLaPNyouQ6BA==" + }, + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXAndxXKxlFxKHXi0W/9W0QeAVt/4pOZyjl/ENWieiCnDo4eoN/fpczM9UwWipcPskKBiHzGQlxWL0Mw+624qJRziL02bHXyjjeRlw0+qEWjrO1RoPflb1Bxh1FVktS/9GJTrxJ7MyYiFwUa8CWU1iS6YoeoUmh3fvdG5VNS5j8TeMB6SvKkjVOgY5Z9n2tA3LSDaFEHU4w2h40lniOZGFqvdXZMo7CaXKuo+KdsqCKH6t1joeAcRcGsH7vau81gDYeahvb/Qwz5fJYrWo6/9Sh0DLjJjczQTJlHaHLbjbMvpaaTzfOiUk8r1h64irRt+QIxUPMeuqvUIJJ7iYP22u8SNfV3MEGy/NKmp6Uyyebg4NrmXyucavcykZqUNVcck0MFPFhAK3fNZjvCWGe6o8F23j6gAvPS2iQQHA72lgwqZ9BNdnECeeft1tJP37nbGbYVUyyqy47rqBJylAmmnaYPsGkQJ7x2BsiotpaNi/dyGMW2misue6C1QLn08qLRXWIsN8JdMGRCWPeoez6cToZHerQ3wIz4bcR/Y0/rq6hA+6AUai9YpxCw7nu5UxdcEmOwUgaUzoIZfUpbam+vLYj0hyzsbKPgrKQypE8GPK75lTbpiOsReF6GQgFrE53rkMK278tnXjpmExV1IxQnwaGfKmJMIc4Bv4V+2un8abxeM+Gdvuq00u8XQTgzSUDSUOQEJCQ+g5pDYPptqZVnFNY3HqjYBC3TgPNUJRAvGHE1kvcpIJaBZGUfngCXjJ8soL+N+q8Pv8IjWDDjZUeo3KiHFQj35vckqycNVcrO/yfijk6XRSDRddM2LLlK5ymdrWX7aqEU/6H9BWg5vhWUSxjWjfwOILtCAI/3roG1gmSneL0iSur0WhfpUbqsFuXZcf0L8j/JKcj5uOOUr+v+Uf7dBjoKvtDBjmetF6jjwwUhqI9ktHaBwrbIi4ycVM24SXrG9xW+Hu0NMCyQ6jpGjQpUIpaXWFgSD25nUo9zPooEPymGKmLmxuiA8/vJ+LpeIYolR18JpWhhc3NldAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1ldGFkYXRhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAA8jjq8G3VCfy9/AHJVx0ujh1CGz2oVGoPqPAvzOKQipLFsjoaxGnUpF7MPY6faSsoF2t4voAkoPa6NQ/KPZNUGhDeuaEb5urN+G1mAXGvNOLyAjyBGWw9S8IdBraQGKVBadb41+KZhjuzQ46wvwjCBJ5Q769rYZ6crknbDeeiHCQ==" + } + ] + } + ], + "Route wallet/getAccountTransactions streams back all transactions by default": [ + { + "version": 1, + "id": "79438f9b-8b37-4ff6-a8fe-9db20974f82c", + "name": "default-stream", + "spendingKey": "208609ffb91991c207309dc455b3ba19a245d26af86f144cd30df12a1524039c", + "viewKey": "6f4bfd8852d2aedf812b08ddaf21fc6f1500cedefe3702bb67179f83479ce2b4ee032ea34c02e4245a351a7dae5f5199e526de5f2c9c4021ef875b1de5d6c0d4", + "incomingViewKey": "b004932248a2a8ee629ecc832fdb0eb91bfcbb6414c686309b3807dd39fc5803", + "outgoingViewKey": "bfe7b58879035b50397d0d924b7a9bb31cda087ae2e2bbdba1aa90adfb4f3400", + "publicAddress": "77cb8adcd0defbd757d45ef7d44e311f9109e170fa7fa18f83e5c3d0c06b5f23", + "createdAt": "2023-03-27T20:29:58.639Z" + }, + { + "header": { + "sequence": 4, + "previousBlockHash": "E730EB645A3B6D4058AFC653049FDCD3D8C80063C25586C8B941856FD34D8B41", + "noteCommitment": { + "type": "Buffer", + "data": "base64:aEBhNjfhS1hh49pWgKfk4orRWf+kF+G7Exnm92Rvf00=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:qdKhvfpoY7xN/dQBUHawvS2CsFnF5eTu1bCxU03cjJU=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1679948999135, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 7, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAEyLBvMmf0QG3qRI0IktY4rYoyb66AWLyW5/mqJuzhoeSx1idi2iB3YZXK2AuL6CuKQbdvqWg8EpAU0hh0YFg5GfSFT463X7qKj479SfciKOVlbK0R+AbVWbXWPzGmdqy7/PVpeFQHNkB8/w7J/kxK29uX7qjZV2mk+QSRfsnDdwVPC0+Vhfl7FiUF5C7LpZHW0VkwxoK9rWWf6G6rARqQuO+KH40FzULFjZ1TcPGUyqTqOPfwfAbnQ8pLm7k6cCNf8ivUWeKg/aLDQchLtFXf/B2/rtfMO1bNLnqQpMA2X8kx/0tuO5wgPkWhKrukxkiIsSxFaGCmYW49TWt6suONpo4EpzU/OOMUzavEPRso+0YBgOaLaQiBLTTw0jdRbMhu1C/JZ4Is+l0LEdC508NPh/9i+ejFrkO+HebTuKgJOuVIX5WRIRV2Z4eQfeE76Fdhne5zD0G4puCxhXqTEAjQBC7HcXgk7vQTqMgeeCLrT6WK4JhzVYrmmIr1j//xqlVPH89x8xAkEJBpfwWo/PgxvJ9rIaNienLQHv3A8cXn4d04xGZlsV6jz8PFlUGN3tv3aj9fIJtWp8WtyKMpAVBz9Q/DavMpZxGrBmxwjYCXzzBWb9E7Rkv9klyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwQPxsFqBJBgbjXfFHPeSzFJVMOR/I2miJwdMkTKo73Ygcdd4lQKI0qip+UM1ODkeLPiCIbJEW36fV859h1HwGAw==" + } + ] + }, + { + "header": { + "sequence": 5, + "previousBlockHash": "7DE275461320743CCC04FC0374BA437AFA5D3F88C2D990B2724041796A6965A3", + "noteCommitment": { + "type": "Buffer", + "data": "base64:jPMxqB1ne8nbdwlOof4213y68cVbeadrHXLWTM0yqT4=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:8ilAjY9/6qE+r1BjAINsNPT1S5SvhSYplVQsLT1uPCI=" + }, + "target": "880842937844725196442695540779332307793253899902937591585455087694081134", + "randomness": "0", + "timestamp": 1679948999649, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 8, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAa6pyyUvMAWXtIqt0xd17vhKSD6f06ADh7EaLy1EhILqN5ewMcgX5uu2uo7BJ3TwYHMayj8/5zdXgJn0eva8oNP6FDDnoRHAanKvwILB7SU+HZ4WJcsAKcF2pUBLACSjZBPWoIGUctyLoOUwJfVUup+ABDKIFjR2JeNc1zEorBzsNMZk7w6X4/O1p4yUzoP1ke/1PPsyvf0BnoDRhxliwc2iLoFN65aV26+jX1b1yheGicTpuxy5JxT1h+q/eAnBgWVGZfNHr3XfQfi4LSuir2NE7FwV21ECU+l5DrS4n88MWyhYYchYlHlq2yxXZnYiBh/5RArsEyZy5Hn6GNG+qrETd+DpYq1EliYyoafev+FX8roMPs0AeUmjMKD62RhkGqS2knPi1p5XG6MJ3DDtIGm8SAEesDNLlJ/sYbzjvf9HbsNWGNFzxBNtbtbs623drhRsduALr9PUbXdCh7StNFcNCLAef9vKr79uudE4S+5nP2UkJrW3wOg5tLMwh1GPYDlMWWqhyILLp2RpWpV7WYmRctREdl1QozhuocQhtS05o8PukcMl1I+cMSh4IYMHeSUoaaD2bE+GHH7XcPT6fKni7+EEgEtYWtQ46sVD9jCbRHulheNUpZUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw2R2UwFLgtsGH3Rbwh5nNMkey0Lgab4OTkGzWI+Nz+cDL/TkSdnYFuTD14PRMMKREA0Wn6wRIt3g4GrmvlGQ/CA==" + } + ] + } + ] +} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/burnAsset.test.ts b/ironfish/src/rpc/routes/wallet/burnAsset.test.ts index da97790260..32c19db60f 100644 --- a/ironfish/src/rpc/routes/wallet/burnAsset.test.ts +++ b/ironfish/src/rpc/routes/wallet/burnAsset.test.ts @@ -26,7 +26,9 @@ describe('burnAsset', () => { fee: '0', value: '100', }), - ).rejects.toThrow('value must be equal to or greater than 1') + ).rejects.toThrow( + 'Request failed (400) validation: value must be equal to or greater than 1', + ) }) }) @@ -39,7 +41,9 @@ describe('burnAsset', () => { fee: '1', value: '-1', }), - ).rejects.toThrow('value must be equal to or greater than 1') + ).rejects.toThrow( + 'Request failed (400) validation: value must be equal to or greater than 1', + ) }) }) diff --git a/ironfish/src/rpc/routes/wallet/getTransaction.ts b/ironfish/src/rpc/routes/wallet/getTransaction.ts index e11bf1ace1..b0b986f221 100644 --- a/ironfish/src/rpc/routes/wallet/getTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/getTransaction.ts @@ -4,6 +4,7 @@ import * as yup from 'yup' import { Note } from '../../../primitives/note' import { CurrencyUtils } from '../../../utils' +import { TransactionStatus, TransactionType } from '../../../wallet' import { ApiNamespace, router } from '../router' import { getAssetBalanceDeltas, @@ -22,8 +23,8 @@ export type GetAccountTransactionResponse = { account: string transaction: { hash: string - status: string - type: string + status: TransactionStatus + type: TransactionType fee: string blockHash?: string blockSequence?: number @@ -53,8 +54,8 @@ export const GetAccountTransactionResponseSchema: yup.ObjectSchema { + const routeTest = createRouteTest(true) + + it('streams the associated transaction for a hash', async () => { + const node = routeTest.node + const account = await useAccountFixture(node.wallet) + + const block = await useMinerBlockFixture(routeTest.chain, undefined, account, node.wallet) + await expect(node.chain).toAddBlock(block) + await node.wallet.updateHead() + + const response = routeTest.client.request( + 'wallet/getAccountTransactions', + { + account: account.name, + hash: block.transactions[0].hash(), + }, + ) + + const transactions = await AsyncUtils.materialize(response.contentStream()) + expect(transactions).toHaveLength(1) + expect(transactions[0].hash).toEqual(block.transactions[0].hash().toString('hex')) + }) + + it('throws a ValidationError for invalid sequences', async () => { + const node = routeTest.node + const account = await useAccountFixture(node.wallet, 'invalid-sequence') + + const response = routeTest.client + .request('wallet/getAccountTransactions', { + account: account.name, + sequence: 0, + }) + .waitForEnd() + + await expect(response).rejects.toMatchObject({ + status: 400, + }) + }) + + it('streams back transactions for a given block sequence', async () => { + const node = routeTest.node + const account = await useAccountFixture(node.wallet, 'valid-sequence') + + const asset = new Asset(account.spendingKey, 'asset', 'metadata') + const mint = await usePostTxFixture({ + node: node, + wallet: node.wallet, + from: account, + mints: [ + { + name: asset.name().toString('utf8'), + metadata: asset.metadata().toString('utf8'), + value: BigInt(10), + }, + ], + }) + + const block = await useMinerBlockFixture(routeTest.chain, undefined, account, node.wallet, [ + mint, + ]) + await expect(node.chain).toAddBlock(block) + await node.wallet.updateHead() + + const response = routeTest.client.request( + 'wallet/getAccountTransactions', + { + account: account.name, + sequence: block.header.sequence, + }, + ) + + const blockTransactionHashes = block.transactions + .map((transaction) => transaction.hash()) + .sort() + const accountTransactions = await AsyncUtils.materialize(response.contentStream()) + const accountTransactionHashes = accountTransactions + .map(({ hash }) => Buffer.from(hash, 'hex')) + .sort() + + expect(accountTransactionHashes).toEqual(blockTransactionHashes) + }) + + it('streams back all transactions by default', async () => { + const node = routeTest.node + const account = await useAccountFixture(node.wallet, 'default-stream') + + 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 response = routeTest.client.request( + 'wallet/getAccountTransactions', + { + account: account.name, + }, + ) + + const transactions = await AsyncUtils.materialize(response.contentStream()) + expect(transactions).toHaveLength(2) + }) +}) diff --git a/ironfish/src/rpc/routes/wallet/getTransactions.ts b/ironfish/src/rpc/routes/wallet/getTransactions.ts index 589770087a..87331e8b03 100644 --- a/ironfish/src/rpc/routes/wallet/getTransactions.ts +++ b/ironfish/src/rpc/routes/wallet/getTransactions.ts @@ -3,6 +3,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import * as yup from 'yup' import { IronfishNode } from '../../../node' +import { GENESIS_BLOCK_SEQUENCE } from '../../../primitives' +import { TransactionStatus, TransactionType } from '../../../wallet' import { Account } from '../../../wallet/account' import { TransactionValue } from '../../../wallet/walletdb/transactionValue' import { RpcRequest } from '../../request' @@ -13,14 +15,15 @@ import { getAccount } from './utils' export type GetAccountTransactionsRequest = { account?: string hash?: string + sequence?: number limit?: number offset?: number confirmations?: number } export type GetAccountTransactionsResponse = { - status: string - type: string + status: TransactionStatus + type: TransactionType hash: string fee: string notesCount: number @@ -37,6 +40,7 @@ export const GetAccountTransactionsRequestSchema: yup.ObjectSchema = yup .object({ - status: yup.string().defined(), - type: yup.string().defined(), + status: yup.string().oneOf(Object.values(TransactionStatus)).defined(), + type: yup.string().oneOf(Object.values(TransactionType)).defined(), hash: yup.string().defined(), fee: yup.string().defined(), notesCount: yup.number().defined(), @@ -98,7 +102,11 @@ router.register { name: 'fake-coin', value: '100', }), - ).rejects.toThrow('value must be equal to or greater than 1') + ).rejects.toThrow( + 'Request failed (400) validation: value must be equal to or greater than 1', + ) }) }) @@ -37,7 +39,9 @@ describe('mint', () => { name: 'fake-coin', value: '-1', }), - ).rejects.toThrow('value must be equal to or greater than 1') + ).rejects.toThrow( + 'Request failed (400) validation: value must be equal to or greater than 1', + ) }) }) diff --git a/ironfish/src/utils/yup.test.ts b/ironfish/src/utils/yup.test.ts index da8833f291..1789b4d8fe 100644 --- a/ironfish/src/utils/yup.test.ts +++ b/ironfish/src/utils/yup.test.ts @@ -35,6 +35,7 @@ describe('YupUtils', () => { it('currency', () => { expect(YupUtils.currency().isValidSync(CurrencyUtils.encode(6n))).toBe(true) expect(YupUtils.currency({ min: 0n }).isValidSync(CurrencyUtils.encode(-1n))).toBe(false) + expect(YupUtils.currency({ min: 0n }).isValidSync('0.1')).toBe(false) expect(YupUtils.currency().isValidSync('hello world')).toBe(false) expect(YupUtils.currency().isValidSync(0.00046)).toBe(false) }) diff --git a/ironfish/src/utils/yup.ts b/ironfish/src/utils/yup.ts index b04113e692..b8a63f004e 100644 --- a/ironfish/src/utils/yup.ts +++ b/ironfish/src/utils/yup.ts @@ -36,9 +36,16 @@ export class YupUtils { const min = options?.min schema = schema.test( - `min`, + 'min', `value must be equal to or greater than ${min.toString()}`, - (val) => val == null || CurrencyUtils.decode(val) >= min, + (val) => { + if (val == null) { + return true + } + + const [value] = CurrencyUtils.decodeTry(val) + return value != null && value >= min + }, ) } diff --git a/ironfish/src/wallet/__fixtures__/account.test.ts.fixture b/ironfish/src/wallet/__fixtures__/account.test.ts.fixture index 79ce83ef81..739757d189 100644 --- a/ironfish/src/wallet/__fixtures__/account.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/account.test.ts.fixture @@ -3721,5 +3721,166 @@ } ] } + ], + "Accounts addPendingTransaction should only save transactions to accounts involved in the transaction": [ + { + "version": 1, + "id": "b532d80b-e581-4a61-bb3a-531fd590ca2c", + "name": "a", + "spendingKey": "15bcd47611f48cc2449626304d9a06207bbb6deec1921418002a59e3ac8d53c2", + "viewKey": "91f2958e4337a1d12b678f56d4a2199048d5d84b325ca5f3b1dbd3c4abf5f640e0acb979830c314c781f211ca6f68c8c77000ba7e4fa7cdf2299cbd96ccac21b", + "incomingViewKey": "f1d9fe8822480109f6bcbb29d595d6f3dba7e396a6a09632573310d88dcb7f06", + "outgoingViewKey": "93a1c3e4865629efc480d8728fef97532fdcaab6ea41249238bac11ff488a049", + "publicAddress": "6b46e8def47e285e9d70df20cd52a816894218b2385cd40d99b1588c3d011c97", + "createdAt": "2023-03-22T18:45:17.951Z" + }, + { + "version": 1, + "id": "dc785412-7637-456d-b191-db3e9ee2ec41", + "name": "b", + "spendingKey": "08f7d74adeedda62bc1cbf74bd1a91d4838143eb6e273ac4dec0e55fcdb0df11", + "viewKey": "912039eead30827067ff2c54840f440317cc982bd8ef538b6fdd473fe46995cdc5fb40e60250f8e8585ff5b7ae5a5eb0032bd76b661c09ce7fa9b822f2914470", + "incomingViewKey": "843322669aecc1e9850e873e703fb5786e101d10d3e02c482ee0a746a3aa0405", + "outgoingViewKey": "7614a34227df4536d7f5168e8aad09cbdb76054f690e030c6acd8c7506caabd8", + "publicAddress": "f8c36799849623cea55c6eb362aaa1c91b61ab9688a996c5ac61a68e5f2946ec", + "createdAt": "2023-03-22T18:45:17.962Z" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "AD35193FA159CFD7440A3DFDFBE7ACB720CB4A44A441AB933071E2B9EF5DE90B", + "noteCommitment": { + "type": "Buffer", + "data": "base64:4ocGrsLmvh/+H7GyxtsHLom9hcxhjifMKHmPaNw1XmA=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:gzH5qQblDGE8ivqOwYIddddRRkhP+RoqSVKAVmnjPhI=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1679510719091, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAkyOEkI2Wwdlfs2s8+d1H2U+9MGQMH5wpj/lEQtYEV3OvTQzaEuRnYe4OoekTupST7eJWytdWFw4W4QBEZSJkk9UNKHwNKn97iR0lKZuUGfuLRiH13iGSTfb72zM6OtOxIpdp8QVXZ65kFbbiFkWtwkVIGm/Vd8Aq/Yf4s/ZE2hoILMwagFJ849SV7bXSs0JBhaZ3CyWBZZ5aDovpkbNZuwNwjwUjip9MtA3ja+Xan9WZ8mBfS+iAFEp8GXa+l0YxVUKaNqSM9Sh3kSftk6cSf3FVYhDbQtWPsfYK4ON6DV+7PP7zILB1PU0MalHip8MN3yLhW9E1jCMKkE4bB8eACihQ2uel4tSKkr5fscQaSOb5qxcwnGPUmXuyyd8BhRcMf+NZpo6rqX3EVogqATNVCY+F+VoFLTsbf7OARk5Y5ruGWBnx3AyxmD6bqPzauSCZLkkbHC7T1mR0a3+guFPvC221CHOHlCGZzqm3cPJzo68PtM2okWrtkq0WrgCbSjsmUPICsHAV64H0nr8C2VAEHkkT18/dsb1My5A7JANyLhHFAvaclzk75WZbN79wostsy8rUCwN2tNKncN1vgaf8D4+SHEqVWFhsBuFaWk8tueZOl2YqKRNahUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwcQAdU15jx2fo+n2Jqr5rwuLqM623/FKQ3qdPFfXLjzh6+4bgDC9bv3H3SA3v6rZQIm3MV2twpuigfYijGFUqAQ==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + } + ], + "Accounts getTransactionsBySequence returns a stream of transactions with a matching block sequence": [ + { + "version": 1, + "id": "11e907b4-3db7-4988-8b76-f8bfb873f8b1", + "name": "test", + "spendingKey": "98f59d54f73f0694859ac47ef9e9e2883818db6a672086bbdcbb916121538e14", + "viewKey": "ead0051ee9feb07e9094b93d22abbce67fd8d68783fce7cdfb8d30c3074cce5bc897e52677b6f2bc8a2c704c9cb4d9f1662d447c78d09f751084f2a93a2fb7a1", + "incomingViewKey": "8ee12966d734e39db232d7df90b1860765c0f3eefd360d2cf4e627d26c3d1605", + "outgoingViewKey": "973a3c7a565b22d1c15f719ca643a17b40b7f1da791300889913cd83291ec6ad", + "publicAddress": "37b4d6fc21976718c0c1d55cb8f6e01d5dba6b01969f483df8af2d3385ca9543", + "createdAt": "2023-03-27T18:58:34.678Z" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "AD35193FA159CFD7440A3DFDFBE7ACB720CB4A44A441AB933071E2B9EF5DE90B", + "noteCommitment": { + "type": "Buffer", + "data": "base64:hWYDlvnIbdHE/E5rqHseTqzBljHRJrW7yDu5iOkquUU=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:FfyfuONVdEQcmNbeA0+A6VJHtr6LcoqOoN/ZGegIGok=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1679943515185, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAK1HYmtGgELMG2zqlLge7FkVcS2yrFHoDjK+DhWvsqwuQ4wTm9ObsCA+xJiz1QMoUzBZW20A9eeO/9RuNWKBwL8y/5U+Fsug0mzKPVLoWScmTlQTpPFjTlLQdJRhL2UPP94a8O8CPf03uJVeh8Y36dY1MYK1JyQ6TNHiUOuEiFzQAk4SKEsGr8PipKyFjp7G1oCyM20jUgL165bcK+pmm4F4+PtWzIQbHLed2NyN6AdKSlacdcUmCzJQs3grFLMcAq9N58LhoBNKw3tLi+oFwpZJDqjGasia3dVhgWpYVF023/BdtexA5d9DxU+ncYmiLBzzOOmQzag1ohRqWsUWFlYmOvqkQdB1f0G9jXEAmGgtbjx6ueDcCMdx1fEzuwDcmrsf1fFQj888OL6B7eCL7hURmFOBeCyBRvER+4sZBLqBx1VtT4gRHWxE+WBmgK1idGzNlN4ZBZw8hXSctKVqunBc4z6TW6yGemjRhq9D/fqGtsL8v3jKWC722feuZZiSkPhpnt5HXrPvYciEU4r51A+GoHY0bCPC+0ROdDdy1KtIm4PfhnxtkDuZxNhLxG+MZCy34tpdUoKQpQtF3y8maU1nstxVuA8ZNSfnA4yyGz3td6jjcrdoJ6Elyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBb7TBWJxfbZ3Fkg6SlXpb/r1xiN56NhHNTPNl0mCPJjLjiqXH2zPsHaPdZaDuX+OsiOFyASRTl3AIx4+cU8SBQ==" + } + ] + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "98947EB3780AD0224216CB366E1D913505817C67D2547A682F97792E0BC7B8EB", + "noteCommitment": { + "type": "Buffer", + "data": "base64:4SgQOtg6l0yirkej0i3oFpp2fialQht1NB9+6UKhyz8=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:T3ECoZQAR0e9yHAjJUEMmuFG62MPGjbxRciD4Z2Li2c=" + }, + "target": "880842937844725196442695540779332307793253899902937591585455087694081134", + "randomness": "0", + "timestamp": 1679943515641, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 5, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAYlMn8HjCNv/vDqc6TizuLbB4s4CYoHSw4iUd00TIzyCqsAbUOdlJR/VA95b/6EhULwuo4bHBAj1DKfkyo/kOpN2BeBTOSqhw+tsTI1W6/LakV0kudqTVIqD8T4wobERcq3msR4SlDf/+F3SfmuB3FWLpAeYvhWVYkXG+v5ZPvaEPULaP0WNpVFWZd8YV+SPc8CHLcv/91vr6k1dPtYm8ayxNqxOY+ZM6TLdoyNNmiJuHp8eid/IA1RWTaHUrhfrcqheGIZbs52n8qt1cgL/mtSC4MCYY5UxIsgBcjwZPriohUTnkDV3JYR8325p3XQkRU3tt1zzCJssMMiIeK9KDkDdtWqP3OUQl2dN54wcU4MoJJ+iEvaHy4se6oxyU3h0ZYQmvzwPAGRF2kHtcn5StAabi/4JxlE6RNS0YkrWOUZ6wXwrPW2iBvkZwvVQO2DFbskvX8akQdkaGyML3cieMyI/VQcnpPfnODx07FYJO5W+WFtnF4E3piFD+FkP1yp1U+HSvKZTgoerMqXv7wojO0oaYEZPaCVo0rcWz6WM78HU/q2zAE5etqBdxe6OUKY9HQUqwUmaJmB9aukmM+bfYqlZPP2rRcTrQsSY0WIAqeTqlXEUNcC7BD0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw5YZOWL3aAxBW0v0s4RAbyvekLnG2hrnHSfUS70pH0oHZW24Ukt+3K5I6ulSuVH89EtbeO4omkpcduYkASUd5CQ==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + }, + { + "type": "Buffer", + "data": "base64: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" + }, + { + "header": { + "sequence": 4, + "previousBlockHash": "44B03E53E351CDA98E00AF64364F99BB1366AC5D11FA61210A0D647119478F40", + "noteCommitment": { + "type": "Buffer", + "data": "base64:Y8g6fZHKg4AHmD0Ha5JAvzCE4A0YYPe+XNSH62YCzUA=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:GOyDZIB0To76St8uWXFx29++ZzIbtcLfPyCvUlh5fZc=" + }, + "target": "878277375889837647326843029495509009809390053592540685978895509768758568", + "randomness": "0", + "timestamp": 1679943520316, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 10, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAEMqA5JXdPAO5jNCBM9K7FbM9TC5JCGYPgdhx25ZOhZ6qWrAElDN2MwMjahRG9f0ksNFC+0xIfdyscTjmX/kXgP29oDHZo+TtHFG5KubgOYexoDg+2DLRTCsINwKRmA2xq+A5oB94zXjVGOjQP0K+cASLJdt3OAUidz894Y0owNATRCfhhGfa9rxCsmfUTUJBpegSJDxCI6LAY7nd5Z0vCFlJoXkqxcdQ1zlxiR94/oOiPyttavBBzo6/U8VChdXGRfTX5ZGI/L9Z+4KDEnd4N9M28cr+I+xQuYIGkFT7Lbe3C+nCUILlRoIwwQiUzBRYH9nyXdmSWZWDbZz+9PsT0Z3wFg+y7S8ctLmNfe1ZOO0k4+1/WAMIV3lD32Y2aypJwF5B1rnr55FABn6Oxog1SjfE1DRvckwsCUqTQeCsbouZmeuI5hUNqNNDs86gMEZnkRApP+7H+RHm9z45tuPSggdmMK++5ucpZyropYiePPBru+LKKi2zxIczxB3eyjxsmroidmdkSP81yEnGkc9C9mlkM1T7oa3/u7ilUKNWGMZST9CKADh1u3hgQQrn+3vPVflFq5BRQu4dAr+uXfPjxCygVVsbo77So0gQR4cok67GbGx5q+cy70lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwlvk7axTqfGlun01Y6MMWaZronhe2qhcsDj7SeZU1nNTrzJILKTBw+oettDUmx2414AI3iLKZNBPEkwJIzI0DBQ==" + }, + { + "type": "Buffer", + "data": "base64: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" + }, + { + "type": "Buffer", + "data": "base64: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" + } + ] + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index 3cfcb3d935..6a4fd1f47b 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -2566,60 +2566,6 @@ ] } ], - "Accounts addPendingTransaction should add transactions to accounts involved in the transaction": [ - { - "version": 1, - "id": "5cc59edf-b7a9-4c21-924d-26f25139435e", - "name": "a", - "spendingKey": "9d84f9febc7f51390657d359a21c44dff1c8cdb98a98e0db7db66e2db599bfe0", - "viewKey": "c42e05bf3a17642ca3aa3b73ff61e5f581b156ab8c516e11920738195a0c03115108848d9b86f468b619a88b70d8c2079c9f9d96c09e3a1c246840c619dd29b7", - "incomingViewKey": "c9af9ff44069d3836ad16232c29e3239dac052b6a15c60a641d7839ccebcb303", - "outgoingViewKey": "eb803b1de8d43b75f76a9f70a7bd3a4c479954b2bd006aee444b8604bde00774", - "publicAddress": "6f045687d7970c450f1e300c3c1aacc805a0b7209bb0cec2521f341f4188df38", - "createdAt": "2023-03-12T18:18:50.345Z" - }, - { - "version": 1, - "id": "6e9d60d0-4e2e-458b-ae4e-7927497dde26", - "name": "b", - "spendingKey": "47260f9fe78449144156c8cdcd453ae2a0958bf594bf61018e46884ba3e30e48", - "viewKey": "1b866ea28dc2866665e5b6895b448208e3a18ef52243db78ca0c59353196b4e44d04d7cca82b6378348050e42fd84b3a6885c73c84ec0e6c94c362ead7c2cc2f", - "incomingViewKey": "62d17f60ec581ec32837ee65fa136a4f18b34e5bf727cf826dbc4879c4666507", - "outgoingViewKey": "472b2a7778566e962790c16571f825e377d0029d71cb55aef259a00c82db35df", - "publicAddress": "87c757a6c79731497a5eb2e4b645bd26d8ef8cf13b3172373f162973d829585b", - "createdAt": "2023-03-12T18:18:50.354Z" - }, - { - "header": { - "sequence": 2, - "previousBlockHash": "AD35193FA159CFD7440A3DFDFBE7ACB720CB4A44A441AB933071E2B9EF5DE90B", - "noteCommitment": { - "type": "Buffer", - "data": "base64:2aRQUtlbSi7zeXBvWm4RE1Tnz5SoEHKssRV9c+TuzkQ=" - }, - "transactionCommitment": { - "type": "Buffer", - "data": "base64:yEH/NKoF1PohKWGfr8HK1oXs7/VCYcHK+dx2tvqYZss=" - }, - "target": "883423532389192164791648750371459257913741948437809479060803100646309888", - "randomness": "0", - "timestamp": 1678645131337, - "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", - "noteSize": 4, - "work": "0" - }, - "transactions": [ - { - "type": "Buffer", - "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAR/GHHSOhrdqZGxIJvYxF6bAkEoEq4uonj+wtixfqupmjGfGi+gcJThiFwbBO+3W7ffjFVtE1xWIM0vmWidbDATzuiUA54RhVX0bi6OL4EUuMCthHMm+YGSBtWTlif53OslAWA/XWNd3sOBUEyzQG3BE1d4fqvVbvNP4/dwn/1OcJTsuT3eEBRtFrUZ5zhqLDmSMqWeZVEbidTcK0rdtJeFfurGfV10KBnwA/a/gXVTOZamX+YR17aJFoj0trFnIZa1umKKbZWDUwd2vvhfpIYpuhS89yqH7gKixgK+4Xt2ssV1t+hoQeYPqkuje5vOauXMO2IEsn2mcvd57vRTo3zngBMXYXAqH8r1COSSQNNVVcWz/rfPV6Mzcr2midDGJEko4ICW35/mFrVGUIbxUCrJxtJrJDTNWiTGbQv5t+BJWeMh559v0yruAjm22BSLF/B4CuZPdGt/IbvRRsdI6/PqJzLC0bgVIdo4NGP1cIOguCiRMfx5P17teWKI3Lme0lQ5C4pD1C/LjjKa+7KJvbfz735jj9Qg05R08dc4yMb99wEawl6huc8qFw6/OWgAI/QCaeejpC/nMOQy3HQi/D0SlWPxqMtHVFNyPU4Mspf5D+sFHSHGG0oUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwD0zy9bYoAO0e9epiHLv5wHJdrPMPO5pqkxyb3/YO2VPj2JFPx+Y0KxixDDoJ4WeNwYKL9YrqQKYKlRyrp2ssDA==" - } - ] - }, - { - "type": "Buffer", - "data": "base64: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" - } - ], "Accounts addPendingTransaction should not decrypt notes for accounts that have already seen the transaction": [ { "version": 1, diff --git a/ironfish/src/wallet/account.test.ts b/ironfish/src/wallet/account.test.ts index 13e40e64ec..4ff1a5c55d 100644 --- a/ironfish/src/wallet/account.test.ts +++ b/ironfish/src/wallet/account.test.ts @@ -452,6 +452,25 @@ describe('Accounts', () => { ) expect(unspentB).toHaveLength(0) }) + + it('should only save transactions to accounts involved in the transaction', async () => { + const { node } = await nodeTest.createSetup() + + const accountA = await useAccountFixture(node.wallet, 'a') + await useAccountFixture(node.wallet, 'b') + + const blockA1 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await expect(node.chain).toAddBlock(blockA1) + await node.wallet.updateHead() + + const saveSpy = jest.spyOn(accountA['walletDb'], 'saveTransaction') + + await useTxFixture(node.wallet, accountA, accountA) + + // tx added to accountA, but not accountB + expect(saveSpy).toHaveBeenCalledTimes(1) + expect(saveSpy.mock.lastCall?.[0]).toEqual(accountA) + }) }) describe('connectTransaction', () => { @@ -2111,4 +2130,50 @@ describe('Accounts', () => { expect(accountBTx[1].transaction.hash()).toEqualHash(sortedHashes[1]) }) }) + + describe('getTransactionsBySequence', () => { + it('returns a stream of transactions with a matching block sequence', async () => { + const { node } = nodeTest + const account = await useAccountFixture(node.wallet) + + const minerBlockA = await useMinerBlockFixture( + node.chain, + undefined, + account, + node.wallet, + ) + await node.chain.addBlock(minerBlockA) + await node.wallet.updateHead() + + const minerBlockB = await useMinerBlockFixture( + node.chain, + undefined, + account, + node.wallet, + ) + await node.chain.addBlock(minerBlockB) + await node.wallet.updateHead() + + const transactionA = await useTxFixture(node.wallet, account, account) + const transactionB = await useTxFixture(node.wallet, account, account) + + const block = await useMinerBlockFixture(node.chain, undefined, account, node.wallet, [ + transactionA, + transactionB, + ]) + await node.chain.addBlock(block) + await node.wallet.updateHead() + + const blockTransactionHashes = block.transactions + .map((transaction) => transaction.hash()) + .sort() + const accountTransactions = await AsyncUtils.materialize( + account.getTransactionsBySequence(block.header.sequence), + ) + const accountTransactionHashes = accountTransactions + .map(({ transaction }) => transaction.hash()) + .sort() + expect(accountTransactionHashes).toEqual(blockTransactionHashes) + }) + }) }) diff --git a/ironfish/src/wallet/account.ts b/ironfish/src/wallet/account.ts index b112ebfc8c..dd9feb1412 100644 --- a/ironfish/src/wallet/account.ts +++ b/ironfish/src/wallet/account.ts @@ -193,6 +193,11 @@ export class Account { await this.walletDb.deleteUnspentNoteHash(this, spentNoteHash, spentNote, tx) } + // account did not receive or spend + if (assetBalanceDeltas.size === 0) { + return + } + transactionValue = { transaction, blockHash, @@ -507,6 +512,11 @@ export class Account { await this.walletDb.deleteUnspentNoteHash(this, spentNoteHash, spentNote, tx) } + // account did not receive or spend + if (assetBalanceDeltas.size === 0) { + return + } + const transactionValue = { transaction, blockHash: null, @@ -668,16 +678,6 @@ export class Account { return this.walletDb.hasPendingTransaction(this, hash, tx) } - async hasSpend(transaction: Transaction, tx?: IDatabaseTransaction): Promise { - for (const spend of transaction.spends) { - if ((await this.getNoteHash(spend.nullifier, tx)) !== undefined) { - return true - } - } - - return false - } - getTransactions(tx?: IDatabaseTransaction): AsyncGenerator> { return this.walletDb.loadTransactions(this, tx) } @@ -686,6 +686,18 @@ export class Account { return this.walletDb.loadTransactionsByTime(this, tx) } + async *getTransactionsBySequence( + sequence: number, + tx?: IDatabaseTransaction, + ): AsyncGenerator> { + for await (const { + hash: _hash, + ...transaction + } of this.walletDb.loadTransactionsInSequenceRange(this, sequence, sequence, tx)) { + yield transaction + } + } + async *getTransactionsOrderedBySequence( tx?: IDatabaseTransaction, ): AsyncGenerator> { diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index 568a52b5e4..f2acaa3d62 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -1343,28 +1343,6 @@ describe('Accounts', () => { }) describe('addPendingTransaction', () => { - it('should add transactions to accounts involved in the transaction', async () => { - const { node } = await nodeTest.createSetup() - - const accountA = await useAccountFixture(node.wallet, 'a') - const accountB = await useAccountFixture(node.wallet, 'b') - - const blockA1 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) - await expect(node.chain).toAddBlock(blockA1) - await node.wallet.updateHead() - - const addSpyA = jest.spyOn(accountA, 'addPendingTransaction') - const addSpyB = jest.spyOn(accountB, 'addPendingTransaction') - - await useTxFixture(node.wallet, accountA, accountA) - - // tx added to accountA - expect(addSpyA).toHaveBeenCalledTimes(1) - - // tx not added to accountB - expect(addSpyB).toHaveBeenCalledTimes(0) - }) - it('should not decrypt notes for accounts that have already seen the transaction', async () => { const { node } = await nodeTest.createSetup() diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index e07cd74e42..331e848346 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -410,10 +410,6 @@ export class Wallet { const decryptedNotes = decryptedNotesByAccountId.get(account.id) ?? [] - if (decryptedNotes.length === 0 && !(await account.hasSpend(transaction))) { - continue - } - const transactionDeltas = await account.connectTransaction( blockHeader, transaction, @@ -528,10 +524,6 @@ export class Wallet { for (const account of accounts) { const decryptedNotes = decryptedNotesByAccountId.get(account.id) ?? [] - if (decryptedNotes.length === 0 && !(await account.hasSpend(transaction))) { - continue - } - await account.addPendingTransaction(transaction, decryptedNotes, this.chain.head.sequence) await this.upsertAssetsFromDecryptedNotes(account, decryptedNotes) } diff --git a/yarn.lock b/yarn.lock index 125daaf905..c93786475b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10947,10 +10947,10 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sqlite3@5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.0.4.tgz#3ddff8d360dab3f17c596690d8663f353d876187" - integrity sha512-ATvAe7JutFv/d+KTbLS58KsKn/t1raL/WGn2qZfZxwsrL/oGSP+0OlbQ2tX5jISvyu6/7JuKze3WkaiP1JAH6A== +sqlite3@5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.6.tgz#1d4fbc90fe4fbd51e952e0a90fd8f6c2b9098e97" + integrity sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw== dependencies: "@mapbox/node-pre-gyp" "^1.0.0" node-addon-api "^4.2.0"