From df337a8be90b1cba842dc45544aa5b5638e37cb4 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Fri, 24 Feb 2023 12:02:32 -0800 Subject: [PATCH 1/8] adds 'available' balance to wallet (#3515) * adds unspentNoteHashes store to walletDb (#3349) the unspentNoteHashes datastore is an index on note hashes that supports efficiently looking up the values of notes that are confirmed in a given sequence range and have not been spent in another transaction. this query pattern supports computing the 'available' balance for an account and asset by summing the values of all notes that are confirmed and unspent. the datastore uses a five-part compound key: - account prefix - asset ID - sequence - value - note hash the stored value is always null the datastore is updated according to these rules: - when a pending transaction is created - remove entries for any note that is spent in the transaction - when a transaction is connected - add entries for all output notes (if they have not been spent in a pending transaction) - remove entries for any note that is spent in the transaction - when a transaction is disconnected - remove entries for all output notes - when a transaction expires - add entries for any notes spent in the expired transaction this store will be used to calculate the 'available' or 'spendable' balance that an account has for a given asset. to calculate the balance: - determine the maximum sequence for confirmed notes - iterate over all keys in unspentNoteHashes in the range: - gte: account prefix, asset id, sequence = 1 - lt: account prefix, asset id, sequence = max sequence + 1 - sum the value stored in the fourth part of each key * adds backfill for unspentNoteHashes (#3355) * adds backfill for unspentNoteHashes iterates over all notes for each account if a note is on the chain and has not been spent the migration adds an entry to the unspentNoteHashes datastore * renames BigIntBEEncoding to BigU64BEEncoding * renames migration to Migration023 * future-proofs migration for unspentNoteHashes defines schemaNew with new datastore for unspentNoteHashes defines schemaOld with schema and encodings for accounts and decryptedNotes rewrites migration to use old and new stores to operate on database stores directly instead of using Account methods * uses createDB instead of initialized walletDb * calculates available balance from unspent notes (#3354) uses the unspentNoteHashes store to calculate the available balance the available balance is the amount of an asset that an account currently has available to spend in a new transaction. the available balance is equal to the sum of the values of all notes for the asset that are confirmed on the chain and have not been spent in any confirmed, unconfirmed, or pending transaction adds 'loadUnspentNoteValues' to walletDb to iterate over the values of unspent notes for a given asset calculates the available balance by using 'loadUnspentNoteValues' to sum the values of all unspent notes with confirmed sequences * adds available balance to cli output (#3359) * adds available balance to cli output passes available balance through RPC endpoints includes the 'available' balance in all balance outputs from the 'wallet:balance' and 'wallet:balances' commands displays 'Available Balance' instead of the confirmed balance when running 'wallet:balance' * checks available balance instead of confirmed instead of using the confirmed balance in 'send' and the faucet service command, uses the available balance to determine how much can be sent * uses available balance instead of confirmed in asset selector * encodes available balance in getBalanaces response * bumps migration to 024 converts migration structure to follow 000-template * regenerates fixtures for new tests --- ironfish-cli/src/commands/service/faucet.ts | 6 +- ironfish-cli/src/commands/wallet/balance.ts | 18 +- ironfish-cli/src/commands/wallet/balances.ts | 10 +- ironfish-cli/src/utils/asset.ts | 2 +- .../src/migrations/data/024-unspent-notes.ts | 72 +++ .../data/024-unspent-notes/new/index.ts | 47 ++ .../024-unspent-notes/old/AccountValue.ts | 84 +++ .../old/decryptedNoteValue.ts | 129 ++++ .../data/024-unspent-notes/old/index.ts | 42 ++ .../data/024-unspent-notes/stores.ts | 16 + ironfish/src/migrations/data/index.ts | 2 + ironfish/src/rpc/routes/wallet/getBalance.ts | 3 + .../src/rpc/routes/wallet/getBalances.test.ts | 3 + ironfish/src/rpc/routes/wallet/getBalances.ts | 3 + .../rpc/routes/wallet/sendTransaction.test.ts | 6 + ironfish/src/storage/database/encoding.ts | 13 + .../__fixtures__/account.test.ts.fixture | 591 ++++++++++++++++++ ironfish/src/wallet/account.test.ts | 294 +++++++++ ironfish/src/wallet/account.ts | 51 +- ironfish/src/wallet/wallet.ts | 2 + ironfish/src/wallet/walletdb/walletdb.ts | 117 +++- 21 files changed, 1499 insertions(+), 12 deletions(-) create mode 100644 ironfish/src/migrations/data/024-unspent-notes.ts create mode 100644 ironfish/src/migrations/data/024-unspent-notes/new/index.ts create mode 100644 ironfish/src/migrations/data/024-unspent-notes/old/AccountValue.ts create mode 100644 ironfish/src/migrations/data/024-unspent-notes/old/decryptedNoteValue.ts create mode 100644 ironfish/src/migrations/data/024-unspent-notes/old/index.ts create mode 100644 ironfish/src/migrations/data/024-unspent-notes/stores.ts diff --git a/ironfish-cli/src/commands/service/faucet.ts b/ironfish-cli/src/commands/service/faucet.ts index e6836190da..5f0e8913bc 100644 --- a/ironfish-cli/src/commands/service/faucet.ts +++ b/ironfish-cli/src/commands/service/faucet.ts @@ -149,12 +149,12 @@ export default class Faucet extends IronfishCommand { const response = await client.getAccountBalance({ account }) - if (BigInt(response.content.confirmed) < BigInt(FAUCET_AMOUNT + FAUCET_FEE)) { + if (BigInt(response.content.available) < BigInt(FAUCET_AMOUNT + FAUCET_FEE)) { if (!this.warnedFund) { this.log( `Faucet has insufficient funds. Needs ${FAUCET_AMOUNT + FAUCET_FEE} but has ${ - response.content.confirmed - }. Waiting on more funds.`, + response.content.available + } available to spend. Waiting on more funds.`, ) this.warnedFund = true diff --git a/ironfish-cli/src/commands/wallet/balance.ts b/ironfish-cli/src/commands/wallet/balance.ts index a006d660a0..5e17714ae4 100644 --- a/ironfish-cli/src/commands/wallet/balance.ts +++ b/ironfish-cli/src/commands/wallet/balance.ts @@ -64,6 +64,9 @@ export class BalanceCommand extends IronfishCommand { this.log(`Account: ${response.content.account}`) this.log(`Head Hash: ${response.content.blockHash || 'NULL'}`) this.log(`Head Sequence: ${response.content.sequence || 'NULL'}`) + this.log( + `Available: ${CurrencyUtils.renderIron(response.content.available, true, assetId)}`, + ) this.log( `Confirmed: ${CurrencyUtils.renderIron(response.content.confirmed, true, assetId)}`, ) @@ -77,13 +80,20 @@ export class BalanceCommand extends IronfishCommand { } this.log(`Account: ${response.content.account}`) - this.log(`Balance: ${CurrencyUtils.renderIron(response.content.confirmed, true, assetId)}`) + this.log( + `Available Balance: ${CurrencyUtils.renderIron( + response.content.available, + true, + assetId, + )}`, + ) } explainBalance(response: GetBalanceResponse, assetId: string): void { const unconfirmed = CurrencyUtils.decode(response.unconfirmed) const confirmed = CurrencyUtils.decode(response.confirmed) const pending = CurrencyUtils.decode(response.pending) + const available = CurrencyUtils.decode(response.available) const unconfirmedDelta = unconfirmed - confirmed const pendingDelta = pending - unconfirmed @@ -97,7 +107,11 @@ export class BalanceCommand extends IronfishCommand { ) this.log('') - this.log(`Your confirmed balance is made of notes on the chain that are safe to spend`) + this.log(`Your available balance is made of notes on the chain that are safe to spend`) + this.log(`Available: ${CurrencyUtils.renderIron(available, true, assetId)}`) + this.log('') + + this.log('Your confirmed balance includes all notes from transactions on the chain') this.log(`Confirmed: ${CurrencyUtils.renderIron(confirmed, true, assetId)}`) this.log('') diff --git a/ironfish-cli/src/commands/wallet/balances.ts b/ironfish-cli/src/commands/wallet/balances.ts index 6c3b0f8ba7..e67476b932 100644 --- a/ironfish-cli/src/commands/wallet/balances.ts +++ b/ironfish-cli/src/commands/wallet/balances.ts @@ -50,15 +50,19 @@ export class BalancesCommand extends IronfishCommand { assetId: { header: 'Asset Id', }, - confirmed: { - header: 'Confirmed Balance', - get: (row) => CurrencyUtils.renderIron(row.confirmed), + available: { + header: 'Available Balance', + get: (row) => CurrencyUtils.renderIron(row.available), }, } if (flags.all) { columns = { ...columns, + confirmed: { + header: 'Confirmed Balance', + get: (row) => CurrencyUtils.renderIron(row.confirmed), + }, unconfirmed: { header: 'Unconfirmed Balance', get: (row) => CurrencyUtils.renderIron(row.unconfirmed), diff --git a/ironfish-cli/src/utils/asset.ts b/ironfish-cli/src/utils/asset.ts index d77c32e4a6..03ce73b22f 100644 --- a/ironfish-cli/src/utils/asset.ts +++ b/ironfish-cli/src/utils/asset.ts @@ -48,7 +48,7 @@ export async function selectAsset( const choices = balances.map((balance) => { const assetName = BufferUtils.toHuman(Buffer.from(balance.assetName, 'hex')) const name = `${balance.assetId} (${assetName}) (${CurrencyUtils.renderIron( - balance.confirmed, + balance.available, )})` const value = { diff --git a/ironfish/src/migrations/data/024-unspent-notes.ts b/ironfish/src/migrations/data/024-unspent-notes.ts new file mode 100644 index 0000000000..cc5ba1eea4 --- /dev/null +++ b/ironfish/src/migrations/data/024-unspent-notes.ts @@ -0,0 +1,72 @@ +/* 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 { Logger } from '../../logger' +import { IronfishNode } from '../../node' +import { IDatabase, IDatabaseTransaction } from '../../storage' +import { createDB } from '../../storage/utils' +import { Account } from '../../wallet' +import { Migration } from '../migration' +import { GetStores } from './024-unspent-notes/stores' + +export class Migration024 extends Migration { + path = __filename + + prepare(node: IronfishNode): IDatabase { + return createDB({ location: node.config.walletDatabasePath }) + } + + async forward( + node: IronfishNode, + db: IDatabase, + tx: IDatabaseTransaction | undefined, + logger: Logger, + ): Promise { + const stores = GetStores(db) + + const accounts = [] + + for await (const accountValue of stores.old.accounts.getAllValuesIter()) { + accounts.push( + new Account({ + ...accountValue, + walletDb: node.wallet.walletDb, + }), + ) + } + + logger.info(`Indexing unspent notes for ${accounts.length} accounts`) + + for (const account of accounts) { + let unspentNotes = 0 + + logger.info(` Indexing unspent notes for account ${account.name}`) + for await (const [[, noteHash], note] of stores.old.decryptedNotes.getAllIter( + undefined, + account.prefixRange, + )) { + if (note.sequence === null || note.spent) { + continue + } + + await stores.new.unspentNoteHashes.put( + [ + account.prefix, + [note.note.assetId(), [note.sequence, [note.note.value(), noteHash]]], + ], + null, + ) + unspentNotes++ + } + + logger.info(` Indexed ${unspentNotes} unspent notes for account ${account.name}`) + } + } + + async backward(node: IronfishNode, db: IDatabase): Promise { + const stores = GetStores(db) + + await stores.new.unspentNoteHashes.clear() + } +} diff --git a/ironfish/src/migrations/data/024-unspent-notes/new/index.ts b/ironfish/src/migrations/data/024-unspent-notes/new/index.ts new file mode 100644 index 0000000000..d95430f0a7 --- /dev/null +++ b/ironfish/src/migrations/data/024-unspent-notes/new/index.ts @@ -0,0 +1,47 @@ +/* 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 { + BigU64BEEncoding, + BufferEncoding, + IDatabase, + IDatabaseStore, + NULL_ENCODING, + PrefixEncoding, + U32_ENCODING_BE, +} from '../../../../storage' +import { Account } from '../../../../wallet' + +export function GetNewStores(db: IDatabase): { + unspentNoteHashes: IDatabaseStore<{ + key: [Account['prefix'], [Buffer, [number, [bigint, Buffer]]]] + value: null + }> +} { + const unspentNoteHashes: IDatabaseStore<{ + key: [Account['prefix'], [Buffer, [number, [bigint, Buffer]]]] + value: null + }> = db.addStore({ + name: 'un', + keyEncoding: new PrefixEncoding( + new BufferEncoding(), // account prefix + new PrefixEncoding( + new BufferEncoding(), // asset ID + new PrefixEncoding( + U32_ENCODING_BE, // sequence + new PrefixEncoding( + new BigU64BEEncoding(), // value + new BufferEncoding(), // note hash + 8, + ), + 4, + ), + 32, + ), + 4, + ), + valueEncoding: NULL_ENCODING, + }) + + return { unspentNoteHashes } +} diff --git a/ironfish/src/migrations/data/024-unspent-notes/old/AccountValue.ts b/ironfish/src/migrations/data/024-unspent-notes/old/AccountValue.ts new file mode 100644 index 0000000000..dc2fd64a3b --- /dev/null +++ b/ironfish/src/migrations/data/024-unspent-notes/old/AccountValue.ts @@ -0,0 +1,84 @@ +/* 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 { PUBLIC_ADDRESS_LENGTH } from '@ironfish/rust-nodejs' +import bufio from 'bufio' +import { IDatabaseEncoding } from '../../../../storage' + +const KEY_LENGTH = 32 +const VIEW_KEY_LENGTH = 64 +const VERSION_LENGTH = 2 + +export interface AccountValue { + version: number + id: string + name: string + spendingKey: string | null + viewKey: string + incomingViewKey: string + outgoingViewKey: string + publicAddress: string +} + +export class AccountValueEncoding implements IDatabaseEncoding { + serialize(value: AccountValue): Buffer { + const bw = bufio.write(this.getSize(value)) + let flags = 0 + flags |= Number(!!value.spendingKey) << 0 + bw.writeU8(flags) + bw.writeU16(value.version) + bw.writeVarString(value.id, 'utf8') + bw.writeVarString(value.name, 'utf8') + if (value.spendingKey) { + bw.writeBytes(Buffer.from(value.spendingKey, 'hex')) + } + bw.writeBytes(Buffer.from(value.viewKey, 'hex')) + bw.writeBytes(Buffer.from(value.incomingViewKey, 'hex')) + bw.writeBytes(Buffer.from(value.outgoingViewKey, 'hex')) + bw.writeBytes(Buffer.from(value.publicAddress, 'hex')) + + return bw.render() + } + + deserialize(buffer: Buffer): AccountValue { + const reader = bufio.read(buffer, true) + const flags = reader.readU8() + const version = reader.readU16() + const hasSpendingKey = flags & (1 << 0) + const id = reader.readVarString('utf8') + const name = reader.readVarString('utf8') + const spendingKey = hasSpendingKey ? reader.readBytes(KEY_LENGTH).toString('hex') : null + const viewKey = reader.readBytes(VIEW_KEY_LENGTH).toString('hex') + const incomingViewKey = reader.readBytes(KEY_LENGTH).toString('hex') + const outgoingViewKey = reader.readBytes(KEY_LENGTH).toString('hex') + const publicAddress = reader.readBytes(PUBLIC_ADDRESS_LENGTH).toString('hex') + + return { + version, + id, + name, + viewKey, + incomingViewKey, + outgoingViewKey, + spendingKey, + publicAddress, + } + } + + getSize(value: AccountValue): number { + let size = 0 + size += 1 + size += VERSION_LENGTH + size += bufio.sizeVarString(value.id, 'utf8') + size += bufio.sizeVarString(value.name, 'utf8') + if (value.spendingKey) { + size += KEY_LENGTH + } + size += VIEW_KEY_LENGTH + size += KEY_LENGTH + size += KEY_LENGTH + size += PUBLIC_ADDRESS_LENGTH + + return size + } +} diff --git a/ironfish/src/migrations/data/024-unspent-notes/old/decryptedNoteValue.ts b/ironfish/src/migrations/data/024-unspent-notes/old/decryptedNoteValue.ts new file mode 100644 index 0000000000..7761330a9d --- /dev/null +++ b/ironfish/src/migrations/data/024-unspent-notes/old/decryptedNoteValue.ts @@ -0,0 +1,129 @@ +/* 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 { DECRYPTED_NOTE_LENGTH } from '@ironfish/rust-nodejs' +import bufio from 'bufio' +import { Note } from '../../../../primitives/note' +import { IDatabaseEncoding } from '../../../../storage' + +export interface DecryptedNoteValue { + accountId: string + note: Note + spent: boolean + transactionHash: Buffer + // These fields are populated once the note's transaction is on the main chain + index: number | null + nullifier: Buffer | null + blockHash: Buffer | null + sequence: number | null +} + +export class DecryptedNoteValueEncoding implements IDatabaseEncoding { + serialize(value: DecryptedNoteValue): Buffer { + const { accountId, nullifier, index, note, spent, transactionHash, blockHash, sequence } = + value + const bw = bufio.write(this.getSize(value)) + + let flags = 0 + flags |= Number(!!index) << 0 + flags |= Number(!!nullifier) << 1 + flags |= Number(spent) << 2 + flags |= Number(!!blockHash) << 3 + flags |= Number(!!sequence) << 4 + bw.writeU8(flags) + + bw.writeVarString(accountId, 'utf8') + bw.writeBytes(note.serialize()) + bw.writeHash(transactionHash) + + if (index) { + bw.writeU32(index) + } + if (nullifier) { + bw.writeHash(nullifier) + } + if (blockHash) { + bw.writeHash(blockHash) + } + if (sequence) { + bw.writeU32(sequence) + } + + return bw.render() + } + + deserialize(buffer: Buffer): DecryptedNoteValue { + const reader = bufio.read(buffer, true) + + const flags = reader.readU8() + const hasIndex = flags & (1 << 0) + const hasNullifier = flags & (1 << 1) + const spent = Boolean(flags & (1 << 2)) + const hasBlockHash = flags & (1 << 3) + const hasSequence = flags & (1 << 4) + + const accountId = reader.readVarString('utf8') + const serializedNote = reader.readBytes(DECRYPTED_NOTE_LENGTH) + const transactionHash = reader.readHash() + + let index = null + if (hasIndex) { + index = reader.readU32() + } + + let nullifier = null + if (hasNullifier) { + nullifier = reader.readHash() + } + + let blockHash = null + if (hasBlockHash) { + blockHash = reader.readHash() + } + + let sequence = null + if (hasSequence) { + sequence = reader.readU32() + } + + const note = new Note(serializedNote) + + return { + accountId, + index, + nullifier, + note, + spent, + transactionHash, + blockHash, + sequence, + } + } + + getSize(value: DecryptedNoteValue): number { + let size = 1 + size += bufio.sizeVarString(value.accountId, 'utf8') + size += DECRYPTED_NOTE_LENGTH + + // transaction hash + size += 32 + + if (value.index) { + size += 4 + } + + if (value.nullifier) { + size += 32 + } + + if (value.blockHash) { + size += 32 + } + + if (value.sequence) { + size += 4 + } + + return size + } +} diff --git a/ironfish/src/migrations/data/024-unspent-notes/old/index.ts b/ironfish/src/migrations/data/024-unspent-notes/old/index.ts new file mode 100644 index 0000000000..274d1c06c7 --- /dev/null +++ b/ironfish/src/migrations/data/024-unspent-notes/old/index.ts @@ -0,0 +1,42 @@ +/* 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 { NoteEncryptedHash } from '../../../../primitives/noteEncrypted' +import { + BufferEncoding, + IDatabase, + IDatabaseStore, + PrefixEncoding, + StringEncoding, +} from '../../../../storage' +import { Account } from '../../../../wallet' +import { AccountValue, AccountValueEncoding } from './AccountValue' +import { DecryptedNoteValue, DecryptedNoteValueEncoding } from './decryptedNoteValue' + +export function GetOldStores(db: IDatabase): { + accounts: IDatabaseStore<{ key: string; value: AccountValue }> + decryptedNotes: IDatabaseStore<{ + key: [Account['prefix'], NoteEncryptedHash] + value: DecryptedNoteValue + }> +} { + const accounts: IDatabaseStore<{ key: string; value: AccountValue }> = db.addStore( + { + name: 'a', + keyEncoding: new StringEncoding(), + valueEncoding: new AccountValueEncoding(), + }, + false, + ) + + const decryptedNotes: IDatabaseStore<{ + key: [Account['prefix'], NoteEncryptedHash] + value: DecryptedNoteValue + }> = db.addStore({ + name: 'd', + keyEncoding: new PrefixEncoding(new BufferEncoding(), new BufferEncoding(), 4), + valueEncoding: new DecryptedNoteValueEncoding(), + }) + + return { accounts, decryptedNotes } +} diff --git a/ironfish/src/migrations/data/024-unspent-notes/stores.ts b/ironfish/src/migrations/data/024-unspent-notes/stores.ts new file mode 100644 index 0000000000..b046b7c66f --- /dev/null +++ b/ironfish/src/migrations/data/024-unspent-notes/stores.ts @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { IDatabase } from '../../../storage' +import { GetNewStores } from './new' +import { GetOldStores } from './old' + +export function GetStores(db: IDatabase): { + old: ReturnType + new: ReturnType +} { + const oldStores = GetOldStores(db) + const newStores = GetNewStores(db) + + return { old: oldStores, new: newStores } +} diff --git a/ironfish/src/migrations/data/index.ts b/ironfish/src/migrations/data/index.ts index 46eb94108b..1f374ded8d 100644 --- a/ironfish/src/migrations/data/index.ts +++ b/ironfish/src/migrations/data/index.ts @@ -12,6 +12,7 @@ import { Migration020 } from './020-backfill-null-asset-supplies' import { Migration021 } from './021-add-version-to-accounts' import { Migration022 } from './022-add-view-key-account' import { Migration023 } from './023-wallet-optional-spending-key' +import { Migration024 } from './024-unspent-notes' export const MIGRATIONS = [ Migration014, @@ -24,4 +25,5 @@ export const MIGRATIONS = [ Migration021, Migration022, Migration023, + Migration024, ] diff --git a/ironfish/src/rpc/routes/wallet/getBalance.ts b/ironfish/src/rpc/routes/wallet/getBalance.ts index d9029cd088..3935f0c19a 100644 --- a/ironfish/src/rpc/routes/wallet/getBalance.ts +++ b/ironfish/src/rpc/routes/wallet/getBalance.ts @@ -20,6 +20,7 @@ export type GetBalanceResponse = { unconfirmedCount: number pending: string pendingCount: number + available: string confirmations: number blockHash: string | null sequence: number | null @@ -42,6 +43,7 @@ export const GetBalanceResponseSchema: yup.ObjectSchema = yu pending: yup.string().defined(), pendingCount: yup.number().defined(), confirmed: yup.string().defined(), + available: yup.string().defined(), confirmations: yup.number().defined(), blockHash: yup.string().nullable(true).defined(), sequence: yup.number().nullable(true).defined(), @@ -72,6 +74,7 @@ router.register( unconfirmed: balance.unconfirmed.toString(), unconfirmedCount: balance.unconfirmedCount, pending: balance.pending.toString(), + available: balance.available.toString(), pendingCount: balance.pendingCount, confirmations: confirmations, blockHash: balance.blockHash?.toString('hex') ?? null, diff --git a/ironfish/src/rpc/routes/wallet/getBalances.test.ts b/ironfish/src/rpc/routes/wallet/getBalances.test.ts index 2b9137ee56..f829494ee3 100644 --- a/ironfish/src/rpc/routes/wallet/getBalances.test.ts +++ b/ironfish/src/rpc/routes/wallet/getBalances.test.ts @@ -35,6 +35,7 @@ describe('getBalances', () => { confirmed: BigInt(8), unconfirmed: BigInt(8), pending: BigInt(8), + available: BigInt(8), unconfirmedCount: 0, pendingCount: 0, blockHash: null, @@ -46,6 +47,7 @@ describe('getBalances', () => { confirmed: BigInt(2000000000), unconfirmed: BigInt(2000000000), pending: BigInt(2000000000), + available: BigInt(2000000000), unconfirmedCount: 0, pendingCount: 0, blockHash: null, @@ -88,6 +90,7 @@ describe('getBalances', () => { confirmed: mockBalance.confirmed.toString(), unconfirmed: mockBalance.unconfirmed.toString(), pending: mockBalance.pending.toString(), + available: mockBalance.available.toString(), })), ) }) diff --git a/ironfish/src/rpc/routes/wallet/getBalances.ts b/ironfish/src/rpc/routes/wallet/getBalances.ts index e60e56f773..c6a53befa0 100644 --- a/ironfish/src/rpc/routes/wallet/getBalances.ts +++ b/ironfish/src/rpc/routes/wallet/getBalances.ts @@ -21,6 +21,7 @@ export interface GetBalancesResponse { unconfirmedCount: number pending: string pendingCount: number + available: string blockHash: string | null sequence: number | null }[] @@ -49,6 +50,7 @@ export const GetBalancesResponseSchema: yup.ObjectSchema = pending: yup.string().defined(), pendingCount: yup.number().defined(), confirmed: yup.string().defined(), + available: yup.string().defined(), blockHash: yup.string().nullable(true).defined(), sequence: yup.number().nullable(true).defined(), }) @@ -82,6 +84,7 @@ router.register( unconfirmedCount: balance.unconfirmedCount, pending: CurrencyUtils.encode(balance.pending), pendingCount: balance.pendingCount, + available: CurrencyUtils.encode(balance.available), }) } diff --git a/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts b/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts index 19514791f5..6ea444744d 100644 --- a/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts +++ b/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts @@ -106,6 +106,7 @@ describe('Transactions sendTransaction', () => { unconfirmed: BigInt(11), confirmed: BigInt(0), pending: BigInt(11), + available: BigInt(0), unconfirmedCount: 0, pendingCount: 0, blockHash: null, @@ -126,6 +127,7 @@ describe('Transactions sendTransaction', () => { unconfirmed: BigInt(21), confirmed: BigInt(0), pending: BigInt(21), + available: BigInt(0), unconfirmedCount: 0, pendingCount: 0, blockHash: null, @@ -156,6 +158,7 @@ describe('Transactions sendTransaction', () => { unconfirmed: BigInt(11), confirmed: BigInt(11), pending: BigInt(11), + available: BigInt(11), unconfirmedCount: 0, pendingCount: 0, blockHash: null, @@ -182,6 +185,7 @@ describe('Transactions sendTransaction', () => { unconfirmed: BigInt(11), confirmed: BigInt(11), pending: BigInt(11), + available: BigInt(11), unconfirmedCount: 0, pendingCount: 0, blockHash: null, @@ -204,6 +208,7 @@ describe('Transactions sendTransaction', () => { unconfirmed: BigInt(21), confirmed: BigInt(21), pending: BigInt(21), + available: BigInt(21), unconfirmedCount: 0, pendingCount: 0, blockHash: null, @@ -225,6 +230,7 @@ describe('Transactions sendTransaction', () => { unconfirmed: BigInt(100000), pending: BigInt(100000), confirmed: BigInt(100000), + available: BigInt(100000), unconfirmedCount: 0, pendingCount: 0, blockHash: null, diff --git a/ironfish/src/storage/database/encoding.ts b/ironfish/src/storage/database/encoding.ts index c42af31346..d2de3ebdde 100644 --- a/ironfish/src/storage/database/encoding.ts +++ b/ironfish/src/storage/database/encoding.ts @@ -172,6 +172,19 @@ export class BigIntLEEncoding implements IDatabaseEncoding { } } +export class BigU64BEEncoding implements IDatabaseEncoding { + serialize(value: bigint): Buffer { + const buffer = bufio.write(8) + buffer.writeBigU64BE(value) + return buffer.render() + } + + deserialize(buffer: Buffer): bigint { + const reader = bufio.read(buffer, true) + return reader.readBigU64BE() + } +} + export class U64Encoding implements IDatabaseEncoding { serialize(value: number): Buffer { const buffer = bufio.write(8) diff --git a/ironfish/src/wallet/__fixtures__/account.test.ts.fixture b/ironfish/src/wallet/__fixtures__/account.test.ts.fixture index 70e347999e..9254a58b33 100644 --- a/ironfish/src/wallet/__fixtures__/account.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/account.test.ts.fixture @@ -2270,5 +2270,596 @@ "type": "Buffer", "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWcT8+R9PFxkcvsMSS1Oz+OZ/8ObxFapRuagQ7zO7ILSyLOATQOqQ+V9L1B1W+lPZXcPNG40pl4i95QqwU1ydK0Vgb813qu2ED/sp0i0c1y6vcDEondseDAyociw/OiwM0M3OIxAuQHkuMKwJteIRJBVR6ZG+4mA+BB2JXKvcxpsMhIlllrh6dUv24PcPcHR8CuLtAp9wVMIn1xQ03UEd9MzVehTpRTPEOPcGlvdQRaKVwN2dvJBt2tPsnQoJpQZsMSchtUffgjSDznb7c8X1DvuYPcsQBfFbhOjpjjuZUmYO/gqsXdOLUel9xvS5TfF6HlGN+DIcSlOaTx0+J0xOldlwfcLghjGpvKBQKLw/E9YQGY5rQwXy7hfaYinnAUtWBAAAAEpFA1ZPiThCEw80VIR4ryHBI+8L2l/MKBA+a7M3aTry7SNYasAg3dMAzFs1QR4uE9UZptQfrg61xFCini+kLVhclfBz+edISQ3EOoW2rR/SltEW4L3L/clqL8HixwYiAahGmVgmWd5Yhws17sTd9hU74aHzVEgSdJ9fjbacd9Z/xQRlGH4Le/4VyLciI1XS4akHZcmDVxp4ryaqmM40cXA9h9Jn3Y7AusK3M2wadzAYJG3G/kYPP6kdMdJJ4EnWZxkiwF6bqMara4YyqFpVPLS1qKmG0LKSZ+g7xqm6WRtFX2F3J3Ay+4TsT0RI0kQRVbSbkdQtbwzMHlu5YxyiXJeZI6XmEhmhLLvcBrPCUh9/i967pHG1xWEF6RugLQOrnNyCZNjy2ytNAzu5fYwPRauMsfAvnwtDKJU17HPkO7MueYWnR/kPrKjScAPEHEVafoHh3njJEXkcPWlhdekbWhQV/TEOgN2SrGmFPx7sY+WZgljABDtroY0kBmyR5tn3KOkdTaYeAuRQq1aImUpahaADKRWt/MpS7c3dNh0bziwZeoFLlWmFeS3O/wEIIBXDdCJWV4Q68DGabcqA8csLnupKjeWCyWM92hiqdCJkSXbshRGPZj2bOfO1yA0/CXlhKHfIUA4Pozd3LSJIyDUL//I94xiI7IGo4UUw8lzdjhZx3iBSN8KN17XxnULl0djpSl5cWzZkJoEjVXAr+QdXCEKvn5ductxo1SX4azz3Xafa4XMugoBulP8ehCLP64TZS+A8vORVm3S9nOjBfE0IktM0D2dXGBrm2l3khjIPD3FCjRsQGC60i5S3zKGpoSiWj1daQH3sOvPusmMs7XO4AUThmCfypvQAeFclDFoZg3dwAoOGhelDqcashMv7VzpnaqY/uu+7T7qxgiJ0N0ZwJ4uFJXLRKuvS7qgfMRRMixDiEka81dnfj6UTIwHhb4XTNkya7b/c5DERj304Kw51rQ/MJ2bkZVR2nawj2VohrWNcEnpVA3AZRLWno27PWSeElVKz0/BlKgLZFkEa7Vn54BUhP2OmAPnuYuh/DqNTNAnmHArh8mrk6/DkWK4Rew29zGN6yDIs05iFKBmM35++vdWA5sNiI+CAv0AcHucrG6xE4rm0eioil52BMN4FSN//lW0Wko9IkOJLUAO5v2HXTlSaXVKAGE1lGBrxuRGrJuAjBO/hpkeiaUx6JWVn2gQA/JHmBMwVH4zOFzY7A0oK/54xLa382Cry082fKB7GZX5Y4aVP7qoTB/IQ3N9vsOFg3Y9rN0jbnXb8tH8UGhwLnS/P9lXO7Xg1fSbHi6RELitxrGlL3hfCHKaQwf1N3o38ZmHJaWabCR4/fGXdxGsyTKGSQ4/89QMSlgChvzzCvc8QtKgNjkPLfH+uIznUErjwmvnKQfQr4zJ7irHNs4xyK+ccsdhEeNqQN/wRu5aKPbGrhIQZiqtChOCXcS/X65mnMEmNkfWlzm2XGyhcoS7XAfEaPhr+xeimK/j7BBoHX9YYQ7qqmbuQIG5bQgRdJakvRK4ZBE3niNnqpRbCBHS4QfK1GKnENoV9GeZUY9tHRImzjxm4pagJl+ZG0C+/h3cgAg==" } + ], + "Accounts connectTransaction should add received notes to unspentNoteHashes": [ + { + "version": 1, + "id": "89c07100-f8dc-4d9b-9d6d-580615cb99bf", + "name": "accountA", + "spendingKey": "823a5a676cb3b74f277f64bcf1a78bc9aa644204e9087ed4346e7768fc8c6a70", + "viewKey": "ed27084dc734d0eb9146afdc57f4f0a7ae53169cccecb4d8089b3fcf88a0490ef6bceea24ef291abd9e43a76f5937cc5364dafddda0531d75b04b183ed6f1e66", + "incomingViewKey": "4ca5b0a4618110a452518523b0a53547488972157ceba58dd6e7bda090167d05", + "outgoingViewKey": "96d5eccc0648dd7e20839b5738d4f858160b64261054436c954dcc77cf5cc18c", + "publicAddress": "5abcfd24f0178148c9a384789d023d2d65c5fa18bf10ed08c7f3f979cae6c8eb", + "default": false + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "D179D8B74987D6617267D46F4958554BA0DF02D7E5E6117DB02D6FF38FD0F6DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:e7YuXFZQWnJUohUuA5FzhJx0TYCpxkQWt35zss+7i0o=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:qT8l0JczOJhYXZDFWx5hRY2vdsav6dmH2AeMnupY5wU=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1677108695239, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAFlZAVP3DRX0CUIO/KOiIB+tFW4J0fjGr0JKVQl+k9VOI7M0cZvisxPmMg0l4M/EU0cd5saO+l3kUcIZAPIKILUhiHlQO4uWNaJ5ZF4Ur+xmTFJ2VI6leFaJHrR0IxGo8nZbAXSzFTI0kQfkWQfqcbWmHcEZtDbkrIU7ifDE5BnEYgAuLCOZfCSnuIpkDU4kCzHFk/vP14mJh28rTOVrRTQezKFNiX9/zkVJBkEZkCVCjw0CSfnJbQxeak7fgWgZoQYd+dXWdalp6xGmcUXlbUtXogzxYG42gODgbVLSbaQrOMjowb2d+NSSzwPfFLC/40P59fHUzcNp43JrzSNTdsG7z6C/2DW+qDsaptCBdaMmJLwo6l3jrYMT/7lmcsrEzry3kOtZNLoj09hy142M9YAfwfOF/6ttAFSa4ikP8i99jND31mNNS2eb1e+1CIxW1mb7Xi+kx7AMU5A2eznI+3AXmDh2+aeqeBM6V7qjoSbuXXYsV+uL6uSvZ3b2+bR2X7kNrk2P1Nwb1yiCh2CFFoyVgYd/tIazJtgIV5qklAITKtHZO4j/uLnuvEQMjmR0kxmzMCjS143X5bobp9Mr8UWaPoT3PVvjrOn1Q4QzUSEFuAwZnk+Cj3Elyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwrNkaIYKBcaQWS9u7dm63qhRvIpr0E6BkeegqoLdxuzWCSxD58aWSPTGwQY3WV46622JJoWgpceL4o4g2hrtoCQ==" + } + ] + } + ], + "Accounts connectTransaction should remove spent notes from unspentNoteHashes": [ + { + "version": 1, + "id": "af958165-8a56-49a9-9f74-7909a896c277", + "name": "accountA", + "spendingKey": "405f980a2400b8cd8b42f197b652c871ac35d174ddfe6b41099e3475c61efb15", + "viewKey": "be390db5a8f36cb5d32e47d2d795e46ac8da5ae822542fb64c53e71dc9686020719aa0ce14b7507e04047720f1243366effcf7f9987127aa218cfa77306cd265", + "incomingViewKey": "1bf4ba4c42a896febe00060e39f1a154d1d74dc69b1296e355c340a14c07dd04", + "outgoingViewKey": "f5f7fde3a1f0575d0ff8e6e515be9e6aaea5143988bf72ecba957ec08a6936fd", + "publicAddress": "23acc6789ea00a7dcf49a518a402a648ed527f63158c407f2b47916144160938", + "default": false + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "D179D8B74987D6617267D46F4958554BA0DF02D7E5E6117DB02D6FF38FD0F6DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:e3MImvRQAln7sd5UFl05PrvYbYop2pVGayKind/OLgc=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:B/wXnhKOM1C7Q9HJEu95RIh101+iYvVgtJe6LGlRCnk=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1677108695899, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAfnu53xSl6xH7E3OQ980m37Y+X8h7QZ10nR26P9YGKECQurlScNEP8FDkFtkdGq2JGtx2I59BVwzHFAV5LyzX1zhCUrL+iGNU0xBp2XyFmiSWOtcTrAxEAn0LUp6s+Z2T8n8gJonGf1hIlbwYX1ref/MaVsF5Mvm4hd6FZsWCygsQmgfNfp4lfZxsDBACy9E1f+VcvufLSwPlFMEsalBRLV8k/5jM7HF0PMh/bz0A+NapCjpB+yByVVZDP9XIQxUE7/lYFPrNRttfem+AZ3CTfveTxAwt9RyLHtaGTDxKoqWQdR07B46Ic/KTHrrly5bh9T3MHiVsXW83bYwHHKstyhj3kgCMJctJ1bTkVU2rnoScO8vvPffpoCdHQEpZUdgWyCReTr4+n0GKQ8Zp3zT9O3jIeM0wikyAlNhd/sTj+NXJSutqtyoK56pCRxGrkTMV/2UNIt06w8oT+iFbg+ieASJRDjNmWI/KQCwaOXhVTzuYIxkG+NJfZYmqzqjX2ccEr/AJCnl15462k47Fq0TBTnMsrFzRBoyzJae7et+xBweI3rah5kp4lxkX1g/1NAMzPMIzSoELNqtPqNi13tyjCzYCW+QQGrE3F8FpQOr08Boo+s9TspBWf0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwFtZQmK7s1JzxeoNrWOL2VTib/78tTU925siBOT+i+qi/h/Pd0UqpbB6HjH6jW508zz4XyC1S/tH9xapeHTWyBQ==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "EF6E3B332DBCABC8B1CD96F2E17CD5C9D0DE9A6548EA58C72104D5A6D138AE22", + "noteCommitment": { + "type": "Buffer", + "data": "base64:D5VTW1LOxbL6rYBEMBkaq/uc/kPOhjV/Pp4xve88KBc=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:wTIHxHNBdYR2uCbHdbk6yPO56Src755WzS7YSoiPrHA=" + }, + "target": "880842937844725196442695540779332307793253899902937591585455087694081134", + "randomness": "0", + "timestamp": 1677108698525, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 7, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAA6hvQb6nuSppUJB59yY6ie1OJsBpxMchc/5AUM2R7YbO3r4NQ6Mcf3JpxmFx9M2oYBmWBABPe3x6fBbJHqQnfZlVq+gOrM5hnDWIbDXIbDnupx0K95sArVeZpHtXk9DpM31yVfuCKweHXLrjy6OWda6CHI2/H/05C5rySzyDKQYgBLHsNzYnCzT9Ik3lU1SttyFrgYZB6MuiojiaijlYwL+zsd/2mnRF+zVqDnf9U4heORqBEJ9dpy7G04MUU6KF2uSk3t53UEcBG021SJCFH4jo9p5j2woUCJNdBqVpJp+HA52ztLvl8g/Fmgi6h8K454m8Zr7VtuX/bRwLIQ5aXkNgZAGYjryEQXvaC4zXmh+vhTrIdrJS5xn4utRSajkluPoZhT/U5PZzi6i6Pw1+Vhi0PJuuUo7L1Rn9Nhr4HSt/AoTHfqTubmBUJDu2NHbG5emQzBknBoCnOKQ/UVLwYU/S08QrvQ6me6bAZnbQQmCZhE4GAYDBQa+kMibO6GwqOblR48dgHX9FNxQxek0g3I+jgophJPgv7z1sKXR1haI9YvkV5XKcJIbQOiqjyAVsi3VDoFhjQ5tKzEcaLNOtdKVnF4Q9qp36X+uHn5qifRZb4zRVVp+WAxklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw16RbW0z2Njbm0XbXIZc3O2BBLxj97++vc24tnCdfa4S644RlzT/ArP+9jYHdg4rCIFHCHz5AfL6TnktvS1qyAQ==" + }, + { + "type": "Buffer", + "data": "base64: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" + } + ] + } + ], + "Accounts disconnectTransaction should remove disconnected output notes from unspentNoteHashes": [ + { + "version": 1, + "id": "b3d06fc1-7e6d-4d2a-910e-56bd8d8e02df", + "name": "accountA", + "spendingKey": "e99914c02120e8deef86e90cc1d44f1fee01345ceecc0e969ef87a1fb0487e70", + "viewKey": "3fcc76409b1f667221aec7ee2bb178d95844bdf6cc05196cb0154a70d2fdb1b55270c578c881ab30b3d92ed4f3eb9270c85d7f660585cea5b1ab1e7418a35a91", + "incomingViewKey": "e791f4a01d9728360ebc63dab520718fb932247637d947061f487eac4f3e7504", + "outgoingViewKey": "d46df5124e67064a148bd9237eb541ee6a934e8eb03a2af4d343a27950bdb9bc", + "publicAddress": "bd776a2986bbe5c692fdde2a34786fe05ff7b5d9e9cea53f1d4969515ffc3aad", + "default": false + }, + { + "version": 1, + "id": "710a061f-d293-40e5-85b5-36a058b1b273", + "name": "accountB", + "spendingKey": "dd51aca6a1876aadc1641c11a55605b225a3cf67663c7d06c213d041cbebf258", + "viewKey": "3e689ed49431584dffdde83cea598dffc6e9ea08caafea0bc1c868938fd70872698cd2788b44831cef58d326d39ecb817518ea916aa43e5ec55b3e75759f0413", + "incomingViewKey": "c92611a268ca0de26b5d6e6bb3b6c3a5f78d7092a566bb0471e6b9e4eb3b8d07", + "outgoingViewKey": "8763428d42f34dfb529432d5b533e374b3cda47c48c00dbb062ac8931e70155a", + "publicAddress": "88374eabd7a768e8d8944f7a45a8a94bc342a17141c56d50113db38ceea59e8e", + "default": false + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "D179D8B74987D6617267D46F4958554BA0DF02D7E5E6117DB02D6FF38FD0F6DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:HKiq8ZRyo4uOYaQd4heQX+OxweBwL7S76tnvl25rHTQ=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:WL984lQRjZz1FZWdNCn8bvhvSyb3U/pzYIP5If/N3e4=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1677108699109, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAADbE1/Z3JLyllv5JCWPhbljWxUeTZRRFxGkcSTydDJAOwyanlL9BCIYPHoMru2RYu+RSQc0EivbBKV5Y/jo2UsOhvnKIyxYe/bI7b8ZW9lASRugh/Wr3yKAdky+m028Y9n7GnmeOoOYT3E1fF51BJN2ToWq9EMeexqNUuPIVh7D8WWCAIZw1m5AB39Hn731ibF+LXh9vf8xAMs4vJEeVf2M/fkeArtR7x/MLNW0OcDn2AwiZpWrnQUQXodhODEwxg9yhRTJfc/4/BM933QiWWR7FSZPfKCunNsdo5hXrvH1EqmtR0njqjNseWrVC1YFbv7aYv2PQEC0GDz7HD762GHZWc9LqINXlW3uw4MtnXEudUI0FjbEHD1tpsMT0Uc1VjncHAfZPrcp4exzjplSWZ2UpHz38xxdFR6Q9vRGBf1otT0irUgb/O9oUjhWjjQ3EM4j4WwC2ieDwWty3pp6xHkwrCJ3UjB4mKkk2SLdGyLYZUSV3rBeO7WpwcSnl49kzWjB4Zgs3rdHxYRgMtlBtj8cSnd5mj6eG3w4mheuN2LpyviW5mClG+Qshr+ayYeaykraiw1isvNcArvuMTEwwjkzEotwLmgUt3lPwSJdbcPkYF6clBLQ6X80lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwhp+aITy94ZTMp0w42Id8KuG1Ff8v7gTBjYJGiu5POTNjpVuK54CQ64ywbfSSPIIeX78vLK/M4ZJWhqYjEfGcCA==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "4310165E1843A379B4271E2122AE74BEADA61E72202218DE08E020492ED94DA0", + "noteCommitment": { + "type": "Buffer", + "data": "base64:DpeiZp/ybt2yyOuPERnbp5ydL4wQosve7baXJ8N4Pmk=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:439+S7mCKjfnBFg1m1r3uFpQ+N3HRrN3mdxgNZLD+cg=" + }, + "target": "880842937844725196442695540779332307793253899902937591585455087694081134", + "randomness": "0", + "timestamp": 1677108701718, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 7, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAQi4bW+H8BobiMSwD6xE8J8XYpZ8GFqaMuk/9bNuHrAuzEwHIMa2AlboDAsKeqNk0M1qxTb5/bsvzJXwxIFY8xkcmw5BFO6x/Py8P4h6VVAezohr2vUfamDGz2EwF16mKXCs2NCu+jO8FEao+retZNw71QW0f3C0rDXKcVKydaTkZANWgDDvQbp39/wY8XAiDyOHUuCxCQKB5vPK7NR4AisCVEUnO5WTHabXv3x8h+FiHIFFMENI1RIP0iea/RiPlHMwcMjRBmGO45zZwL5Q5+gHFlLGeNBCyIwJqECbBCr4RASzBkDgaWzhTbIWwYvMt0PGFvvvej7RcgTaqwmjqu0Jd8NADj/FdcDw7bPlyMezLqx0Cb2jYBHifgBR0cvUpwc1qx8+j1YbZlwT9/AuFYGqhq3rqoWYthebqtYfyuznT3yhh6Vu4X+4GgcZjXoo2xUeueZoBZY48puqA/owI0vR+zpnwqhjAHZb6byZPimZeJgbOQ2YHbdPdnPshBB0dQJJmQ8LRTFwNjHdR6QjXer4lQ6dNA0XkuJpT3+CMvxcqKM3Cv+c0xa/Vq66aX8zvtz8n24G+Tj/kp648HqOI6LQTYEWHt1r1lYtDcXQPAvTjeHueVmTHt0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwEgl4NqDoX6sLrahDxg5PZpKaBoN29zqoVwOWpJz+NlmUvuJunR6Z1YFpXogbL5V2C/RLc0RDPCk8DN5CDVtECA==" + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8hPjeP8DoMXWooOV4Xuh66kEH2DUvxSTvt4OwF3dDtCCrmeSjEILA2N8sJdUaagGttlGPqb9L1UWGJf8M7c+Z5ED/h70VsR5tOUPHY7AJtSGdDoHFarnU8vrSNgyE/6Xi1C0WOmZBzKzcEaTNhwK0GnSmt7IL40gCMJE+R0o/JcLPx6tfyZfZtceR5QMRQ+SHMyhM232qAdVLS+rdf28F6Gcz2i+AgUIuxRHk86ZDkyImDZOnGiLHAHwJN4eJP+qxgArb2hxpobE6VD8WUcsmL2eOJXkqTYsDNj4xP76G+WsdrOoYu6xszoS9hLcmget2JYH1J+9BwqWRecED1B/MByoqvGUcqOLjmGkHeIXkF/jscHgcC+0u+rZ75duax00BAAAANQV0CPB6DQV4ucRxy+RFaLnX5wkhdivHQUpRXDhovSXFcH+IpeAntFX4JyAm6PRVrIF9TUaezYw59K14KOl6e2/2HNdWgGzxNQlkRqrq9Qh7a80vy20uOPWTVxYcq1zDJWV3S5e6I9xAzJE2JN28pmYesClRvUlTWv6LgRve0i4C4I5oRCLupZGVgFQaDYLD6YaIS55HGjrYjgPhbqNhBI4R3ANMbJG8g/mLc97Xq7zTjfbCZWZ60lMYp8cGN9ejhXEWF3aK6SvFiJb+5ih3E9wgj1xBKg8Qc+IcDX1tP3/GSUKft6dsHboBqkEx3Fwpq50EivVKKzQCgkZdPjkMDK3QN+Kt8J03Usxx3Y4Prz0EFYOcL35oo3dCaSaCgqzVRHm6Vaq13tz/EII2X46sZ71f98RK/S7choDoXC4viyTGQBU22gXtE0MnfrtZv4S2E19s31JynBLgtYHyZFKSTnvMU5jL4GUJFhrw8ukAc4F3iH9hqlbSDn1ilhWmrOCxHoaa9jMtZOnya+95Ekfo5h7ceyJ20W2zaTH778c3K99OcDyBb8mUOGqkr0LukRYFlKhBwLQEFe7oC4pG64EDqm3z+TIKFMmoK7fUTY93kKImlN0j1kIH0dDbm9q728UI5wv73earCxhpAC0C39v+pSwwtebZkFfbGllVA9eADDxFWoOiS2d6go44fI0lbdoHRQFtKqd7YK/vGNFr6b2YBAgOveK4GqdvsKYajbfbyiIDOsq3VSPTGSR1Efb9Ctxt/QIVnMrmYb09DyOedzlsjFwQcHg9e1F2d7OcS+nU2fFb9+dlWUGyk6vHO9F1MhLwZwkfwvNKRC1g8j9U3YEc55vJYN4934PE0jdGiJtahHZ4vpxgBSg5uqImUC8vRIJtCl95sxBO4ifP8nO5azcL6dg8pT08pwB5lgijhPK5cHX6WMXyOEXo0QFmu0OXsHpYwgtJV+q5FuPtVNe8dwWjV7yrABsIWt80QSg8+/slybDR5AWwCmYMCemGV6HfzXPw2gHxUWshnYq7J0DP97oP4FC6D83X3Ep1p46szG90IQzWdDn17upoUcQeyJvEuDTSbbrzxzZlpq5gTkmN9K32/dsRKkjinFec99Oz2g/0OshrF+nCcFkVU4PDFReg+S6jcC7h3l0/oE/j8rZQ0qbe/QeUtfMHdjTkwt5VxFlfK9KuCM5e6RVoQ1vBe1AbsR3Vri3fgV/G6TLoi6s2dOmYJBVlaTt00nPMPhJHPI2b7Nmfk6KIrpFFz7xGeIhfISDS32zVlapqHKygnKkx+BqoCAiO7OFjwN69+b7p6a1MCbKIMAfw41iDDdXmemj7hIBNmS8b1y9yPVxqlLwTlE1cKBXV9iUGc/1HQPlsg2IfcqAcD8ge7S2Koa30jnXtwa03heh391aC16bjAQLW3rzaiN6k4TuZOhpbOE31Dq5iljJFbYEWN+BguB8iGsqobBJNyK43gtZClk/NDJOxSyoqJMgDF5x3YbnOykNC4Tc/Q4n3PimQoXtpMebrlyOmW0WVBx0qf+QPVYIYCgFna4xB4kYUaEDnR+fkmm5HIT44h/fbn697ilLYrURKija9AyUCg==" + } + ] + } + ], + "Accounts addPendingTransaction should remove spent notes from unspentNoteHashes": [ + { + "version": 1, + "id": "cac48c0c-8dbd-4125-9732-f034bd3c2286", + "name": "accountA", + "spendingKey": "617e182022dbb5e2feeb91b8519f75708009aadb6c7ac1609c0be6ef1b20b89e", + "viewKey": "c6a460249900ce06f96180174153abaf4f45e13f99df4208f129b956879e4fddf77e6168682e6d1150e5f813b029f63921ce651c6630399c56017e1f968d4d08", + "incomingViewKey": "4a7287ed94ddef77ffc76df5ab5762dcc417dc8448be36d4c1f94d11fa46e106", + "outgoingViewKey": "713e79da6b0b5587b2455a9a75ff6af2edafb2e80ee07aad47698d9680cb79d8", + "publicAddress": "d8fc0f6643238e413b220984899314e392e9c381354cf1c64b281c6fbb09ddeb", + "default": false + }, + { + "version": 1, + "id": "6d2d6da5-8edd-4eb5-b333-16328b1bca9f", + "name": "accountB", + "spendingKey": "e0fb8cfac93391b898dec672523168b7beaf20d9d0480d924b5330e633036e76", + "viewKey": "7a38449edca26bcccd53205dd3d5385d56d4572ef3977b30944c6a3cf4ad44406c8b6d4ee80800af706ad0ae7cca99fcfd03c0eca46ef6c5a9c897bc3f38be39", + "incomingViewKey": "f06325b502cd69c89907e1371da6ed53a51f8997a8dcc2c223f8cbf13a593704", + "outgoingViewKey": "1ef5ebc51f7654e91410b76de9fddb6a8977c5052f832d50c323d7db09db0430", + "publicAddress": "b39d99c57f59fd7340f92413adda57a5f1bda526a66e3d5c673eeecb9e1f40ca", + "default": false + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "D179D8B74987D6617267D46F4958554BA0DF02D7E5E6117DB02D6FF38FD0F6DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:TffVGwa3BBBVP1bj6SIpcOFdzivU7D3CaCKZfFScbVk=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:tfzvKuRNjUNTlXXKz6+iqVN1e9UAkxu84cC0v+3X5Yc=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1677108689947, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAA239HkJwO1fxBcdUVRPIuR5vBFqjGd+nOXNlvBGo3Cm+HtACNYiCc4rX8Nx45CXC8PMONTP90VRYbKARe74HMuPzP8JGVYllazg0vM4GzkP60MdfazSBvrqC6pyP8altwMIm0cu6diwBQtd6p86sPE7hyfT+1D7QbDwemK2p++iwTUktOjXPtu5rq+NPq9JpNt/CKSyHq8V1+Q+g6JtoLBgkvNe47fT1NUclIq/dsic2MeXRAFSpSHmdaDd2Kp8pm9LaM05m2cpLosg3+NBSf5IMCERULXlWP/5ALKI3G5yLihUbSbWT4cMSkWN/v/+DPbsKm5/iKVTTVEzWnt5qtbRvAXuH4kgNySP2DoBzfiKbXrd6S/OSEGvSxZZfEJ3oq6KqT07AFZ+Vsvp8lAJ9+Ckn9Duv4hVIRULrq3M1pW9RdumTPz30arul+4IA9iG0tOeGHxW21xpITkZ7KgrxiGOUAwTSheuYm7AuCpTb5vJNR64jOJ+/9/pcsJoRLtTxTIoxAXIdH1jAI0Ap63TkC4eglmMhjOIFbGHwqtbGwRctRMH9Mhfqva+FCD/sBBxfKOBQUmHM+haKO5bentGeNFE2BTXZVHoQzgCYZIc5nhZRvee/wHwWmFklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwSI0qRABh5a2CC/+REtXfqeiibj5v9EVumczvBGxKG+sB8QxM9q7ja/gfvGmmrp1+QbZ26cSWOUDlKPmyArjtAw==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + } + ], + "Accounts addPendingTransaction should not add output notes to unspentNoteHashes": [ + { + "version": 1, + "id": "8b465784-44a5-477b-b237-4da662e39a7e", + "name": "accountA", + "spendingKey": "d2262a7a94fc13e658befb16ac7dc6af3cce48004159722cc93f5024616eff06", + "viewKey": "bfe1d53b3df7281848e95169401ee9446b798d9df3f1bc9821a9d188cfbb8f5176af27375412c3863c9f7fd5dd6fc2d0eedaa1f507e708408ea0df5dc0a3d212", + "incomingViewKey": "f9c66e50a3614222e9ed994d380f019689aab69e7c46decb6ae78ea99c563b05", + "outgoingViewKey": "541d6a8974c43a56b355ff2ab83520cc500399fc7b2d1cfc7aeb94168d130fc1", + "publicAddress": "17399bd5c493eb7e6eb71a4cc0b2d0fd5ebe9def4288f4fdaf3a1957cf087240", + "default": false + }, + { + "version": 1, + "id": "a3e0688d-16e0-45d5-9525-582d0b1e09f8", + "name": "accountB", + "spendingKey": "cc6c5e966fc41a911f75c1390535213a02a4aac1a6f6da7140ece20f667682cb", + "viewKey": "6ebedf84bd22b629bc6314fcf041c509aaaefa221c519e54c94741a902a7cd0260b46b520cf0109c241ce791751b937eb3b58ee1a6d3fc2f8a5d41eb819a48bc", + "incomingViewKey": "6da965ff36ebba78b98472fe8e4e79f07efa3d6e78cdb64b8a60d9489fd84603", + "outgoingViewKey": "c2f197edd8ab3345ac38d3a37f50974a37e5e6ff0f1bedf547470081d2e76ead", + "publicAddress": "dc784e1e48fa5d398dda1772c8a07ed9f6aabe9a128f8c89889963fd30304787", + "default": false + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "D179D8B74987D6617267D46F4958554BA0DF02D7E5E6117DB02D6FF38FD0F6DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:8eEsNJifUX3f4slvbHWagmVlEKxsrq5HOUMEWE7aIhE=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:qyzO7c5P0/UR0uQSZWsp94xA1Q6R90hqFYXHn7k+7cQ=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1677108692617, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAw+rNHOoxc09iQNlhvpMfeIUPK5LLAzwQG/UvhDYYQ7KYDgHN8faoqbuuXms/J3USFeTK0bd7c2t+BLgVyNW2Y9PwHyulHa1NaTIHWXSC3v+AGlwNzBTlYW4e9fV+VuOXenyRJL9Zuq0cfKC9Q8LUb6tzquQG05XCkM5DyZjbmHoQUFe1ZYIHvFP8FvF5F6fKXDqVBL+b0HSTfuA3KvWJrd/Ekg8g+VPtjNp8EMEBx/KqiP3HiAgzgAOccO2gaA8W+HxGXoTUSkB0G3BJ9n33lAalGfKmWdlP8b3CTOTGYTWdjWkFlbd3x9oBgjwvjofeUD8jK1VFZNgnHV63cFelKJ7VmPYauZ40InMWzGnR/WzeeMu3ymnBGcZ0hTDhI1lGLSAtt46VYhNInXcSNDZxRSbB4edec4brSZirhf7inuvRep4Zon+/2gdK+SgADXxV7XFPemDnFnfodv5Nw12VYvZXQTewTEcPo8d9az4XdyukrvQXs7/YfWB/ZSKsTiK/XKL2htJqBsNvh5/TSbLqDyEYYF9ygx1k887H9KiRd9e5wRRFPHC3waM8RB3NdZ39bUgFnsT7FpFLT2LFOSE12Xmxmx8HrdpqeTUdZfBR/i0H42mPO5ODHUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAPBfvTc2zS5K+VElDl1oW7f4CcrPIceoqOq7OAEEjKAXWcOsM3nXERFFZF06/WlRNHNUXrdQTiq90MVfS8xyBg==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + } + ], + "Accounts expireTransaction should add spent notes back into unspentNoteHashes": [ + { + "version": 1, + "id": "40cb82a4-a627-4568-88a7-c997ef89ec53", + "name": "accountA", + "spendingKey": "dc02a97bb497bbb050e22c8b5b75af505fc904e2736bb64c8c78b014b93c74c6", + "viewKey": "05827869e2117792df28df61bc63ae3a72825566da1a2f171c67d4e6259011a56060aca955ee36327b28da570c04a75f210a06d23073256ef59fe7895e34389f", + "incomingViewKey": "abfeec0bcaf349f8d1aaabd05225c326965c3e520355d71ffa09e09b71b77e05", + "outgoingViewKey": "eb5690caf9ca6efbd37b168a9b10ef0a37462529990d56a299bfdf3f2d069bf9", + "publicAddress": "ddc3f866e70f9f9dcc23bbfd309572c468daf8be2b287de3b62ab901d6e98467", + "default": false + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "D179D8B74987D6617267D46F4958554BA0DF02D7E5E6117DB02D6FF38FD0F6DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:D3Rik5/peLhaMjavvhE3Ur0Yufcr3JIs6/zjt8H8q2k=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:ELM0tjdFylIQXeXcjvqC9ZKzVcy+crFU2tVz9n9evdo=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1677108482522, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAA2Q4CA+b8HaCdJv48w2kQEXPd8FM/9cSxZDQBPdP0p2yM0XfJeLg9grB4O1+FJVHK2iJkganfnaowlurlycscsGaprrmSJvfvFd0OZfxP2tipSLwILvRZi+yAMFQq3jcFW/84AIUikCzxAtZHkcGGm5EJBrg+KOKRe/LZiTryWjwECOrJr4fEEiy8rE2pLVSd2hBYECVsqHyC5dOTvAaDRptk/mqmfyTJGMiqrvsGTSCYYXQ5PLQ9W2RBObUBTKOztgFo95t+xaPfHpfZQ17EyHgH+J6jFPkwg5bM9P0cRuu2lYURVHOJMnqYmUHEx4x9IxJ8kb7WXeQudEjw2H8TLK+qfxz5m/+qn+eESRQVDPldViKzhLoywvcDRaJKgCNepE3JUErZcMppxR0+39u6qnOJP1NqqaF50a6XeEm7P2OG5vJpaXX++3AH1+CRQzH2WliX8AhtY7fRZPlfdPV3+th5rcEsRMyZuUSq/hk/xLD+vsNACZT2yX2LBxFY4IdpzGNTGYG33TI29fNRp/WF01tsOFzbU2x9chbvqsJd0vP8gdATL9uIWmgUwGnGDYATBhEHLf6SCs1668Nuix4d4dthSwiaKLboMv2p1Ib5Smqm3Z9tYsjJS0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwTwLpd2B/oc7xfwNmSuGeivSwlm0k0SOE0Pw+FX0OOlfNtUU4zWlp/dTqqBf06EhR4x5qnXNlInGjQddPQKVVBw==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + } + ], + "Accounts getBalance should calculate available balance from pending transactions": [ + { + "version": 1, + "id": "ffae4120-bd58-410c-9557-b8e32a8049e7", + "name": "accountA", + "spendingKey": "d15fe847cbbe6b5cc64ad22811f6237a5c70dc640bde846d4e19e3baed748e50", + "viewKey": "fed0370de9c8865f22cc1dca7b314075fc08aa15d0eded4452af076a36bc29d96a6dfd461aa26f332a49a64dc813e7ffb2da51f7c173938ced708ad21d8bbe10", + "incomingViewKey": "68bb9604a1927ede371e571312e29bb3c96b2d17b02c521219457f7a7e4bc401", + "outgoingViewKey": "10dd67ee3cadeab8021086a42cd8699f185c0a24ed2aa8e9bef1e67e866befb6", + "publicAddress": "abd1af0a6d9951acfdefd515dc126d9af09c748e10020e20ca58ff4a5c7a50ef", + "default": false + }, + { + "version": 1, + "id": "cf7aff01-3ff0-4772-b2f1-bafd33555dbf", + "name": "accountB", + "spendingKey": "8cb1d36ad64d1c2b5dee338d3357228b966d0b0786044c6d6b71da92a5a1319d", + "viewKey": "7fc05863ea2e524c5f821f056130b3ac97394f1ae36a9de018d644b402b40c14f7d088778f83f1d62591d2e480286aa13f4f5876e62d705fe90c0bb56e2e99c5", + "incomingViewKey": "8f1d0b6549afc1aa4a05a4e103e763b7b29827d2d25b95ea8e9f5198bb597f00", + "outgoingViewKey": "9409f67e3456cb51b7f29d17ac59cbd73876ad36c3508945c93839f5287b73a7", + "publicAddress": "0d5b483a28b8c2e31f958abd76a5347dcc1dd04497c8ba4d14d0963d88582482", + "default": false + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "D179D8B74987D6617267D46F4958554BA0DF02D7E5E6117DB02D6FF38FD0F6DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:TtaxSambHaR3iYcntQ/Pd6DhWursWImXcbiveddCdBI=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:smDZGB7xS+PeJzFYW9EgRyIXui9W2Stm5L3X1DNrOV8=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1677108471188, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAkjf79+YSX0yJW6vfXRtAgGOrkyWN8OXoeNSw9kEUr5yTNVN4vGiZ6VbW1NoucEADB+LJrRFijQpu33Xf5K5n/JL2mQlipHrDxtDBTgkCOviIkHXzitd3kyI0mAe68mLH8EKfvuCufMNPwfOq+qpkE2iGsz75fBaWIhkp251dObEFAXUB5ewyb88Y9nes16wZwI+6/RhGfAef3RoKbh7SsAops3nN8s5zp/9/vZMEN7qw9iEpnYRA1fG+qSUL90RwBxZyagVgdePwJzJDu6cnH22wDjfuCVz9Q4xlHbC5h73B0iFA63XT2pMy4m3bwAxuQeeK+EYA7Ox+FJ+009VZNWyvtN2GTV9Ywd1L6QsGkDkfq606nL7vvhl7+dGY1zsN4CGIeekjwF5242Xv8cU5HCzJj+pkeYZ8kj7f/UBh21GLO+J1NGqO4ZOYZHoD2ZGRKQXcDjz2/EsSxYXAckbPqlBSbgBrYclzRrGr/rAOM+wrcVTYUuMpPN91O2mfvfpLoDlqpDBooeyYLg2qzqlok9ZBvqCzKfIoicw8L33OmtAj7PcYXU9e71huObRd5ME4OoL+V1oWGbDL0RN4lJOiWQUHrYOWj8cVqF+6ihLeHyUuEAVoo0OoxElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwtk7Ux1xbEo2zA0/cmB87T+qlc4aZlikxKPmdQb1vTlmeMbyRoHWgA8LMUGIzz9imI/699KLUofsXSgq4pexiAw==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + } + ], + "Accounts getBalance should calculate available balance from unconfirmed transactions": [ + { + "version": 1, + "id": "4ce2b89a-b80d-43d4-a402-83746b35e2f1", + "name": "accountA", + "spendingKey": "a1ca9e1c9141d7a95334926c6006a5c5c6bca6e8f93db0a5e9b99f6ecb50ad84", + "viewKey": "b1369a524fa859eb8b3cbebe73592e10203ed261f20b1c8de0ef5e0cf64443a4ca04b8c8670b7766f948f727ffa15bbd4d2e1bcd7c6604d2132baa90360d1cc5", + "incomingViewKey": "020c348a1da41bb986adfac01c6fa7b9fc362006819cca89672038ec43dd1301", + "outgoingViewKey": "1b4f38a7bcd4437c85c5fbdf9646132d2b1099134078518635b49259c9895ed9", + "publicAddress": "c50e9f38b53d26c0f85b23a70d10e804fb922184bfb259d4e5b4388e42edea11", + "default": false + }, + { + "version": 1, + "id": "e83151e5-3baf-4105-ba1b-55c4782ae90b", + "name": "accountB", + "spendingKey": "17d47e756a5e42b0b04d093eb3998197243270250920bc79bdc6bcc349fe3e1b", + "viewKey": "586f35881f0526bd9f543571d8c6a2bf821023317c023d6bb367f5e3f183b66a11787b877f6da02e1c27cd5f8923b39d63ca4913ec70998ebdb0319116099815", + "incomingViewKey": "ea4269a7bd76fa478083244f53b782b2f7bb8275af61d828ea09cd7a018c4b03", + "outgoingViewKey": "ae947c42fd21c66cd81bb5ed0a1295bd507c3b5722bcdc6aa4b102c2a17eb6e4", + "publicAddress": "4edb61d7e2c0f94d9469ecde81622ed0c0f9ee9143362de093d4ad48905132d3", + "default": false + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "D179D8B74987D6617267D46F4958554BA0DF02D7E5E6117DB02D6FF38FD0F6DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:uo7CTOENcXRoq8c6g0O3ND00naI+wBDOPThnYFZgPhU=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:iWzw2PO7rJbznFOHWeM/rhDTCLUY+UrftiF/yJ8qyvA=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1677108473832, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAy9r/8jFYNPlfowkaks+wv/SNxT735vFwo4EUWz71e+SsF/Sv+y3Z3KdGRcink7KkKgxoCzepnIbbaWXG47NMQeiDoTUQ5yxWMjMC/T5F+aGs/Vl8lE2F8ZfzDlVcimPhYk3W5crIMP4cUsq6c7WqOKGnbvvQ29mXcLIQt73vGOUZTKB414OrebcgF+ChX6zXDaEkPuRgc0jpdBbtqm3tfZzzpgrnXSgtYPg2At9+ig20smjW9NBhaX1v07u7jTGZNw+trdUZJruV04jHnZZON+OK1ZUE6z85+Ph+LgeD7aS42vFjQPOL491jHUrHlBa0/i6Cu8h0KhbtmJFPzZPY5fNO2adPe65OIDmUi23mJ2Pn2QGBYBLq22saDvXse0dddJQe9BzVsoU3ToLvtalYvfrA+JXevzZW0wa143U25PPnGGdVTKLN63DbilhoK+uVWXqQbEHDd4ViZssM1zDv/y5j51T1gpQOcsuFywXvGfPW2AwINBhpiIKu2cY6EBNfkGJE/MZyblgoXsGIdK9MxThSjd0pTA0K136tEe03ZRSadQ6iqLvrtMeqBCOurVPdHZu48/rGhShSoaRfpS5fdnyTfcKD4DEei9BNgMvmiojfnao5LQftR0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAweaGR+Ch+usDeBr3x+WCuOTtDjtxyeC/95M5WQgguBlfWFkbUcyZmHTkVpjsZfcSnbh/m5e6Rlp9JJFDlRmHAAg==" + } + ] + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "82764513EBFC9803D982FFB17884AB30A454B9C75A000BAEB7D6BEAA14DD3A84", + "noteCommitment": { + "type": "Buffer", + "data": "base64:Cy/O8pBmww7CHti9Nso/jtjsBHLV0DMRAAicTf9SL00=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:Jqfvd668nTO+bwHSRnloVynPH7A2gwj1UENJBPrvOO4=" + }, + "target": "880842937844725196442695540779332307793253899902937591585455087694081134", + "randomness": "0", + "timestamp": 1677108476315, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 7, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/2vKiP////8AAAAAwDI9SSZSB506b7N6VR4msQFLt6ZOwU31cTOktveiKD+KjGC30jo76cqrkwtJUpvnEEzCTryZ2wUdq0ZKt3JY3Tr+uf96LzVz4cCrRw+l9Hyv2Mt8IfEtCONVRL55dcmd9zr9X0I0foZHsHbZsMpY1PHkfd4OQ9QaZj0jsHaTQ0MEYWuRX+EXXJiOVrlREl1QkBEotjtU+SD+xjFvCJ32k5W5AYRpdwoz9YUU6zkvxuiPKotiPWEYXarOR1iRHaeu9qzphNOk6L0pPYV1+lImTuC643UiuDR2rKwudoBWfJ3wEFmmw2feXhSdn8x5DxXGVCJOaD95797O1q6Sw7UQywwEhwI8ZCqAPf/8LBFJOz33TPtULefG8UetJWrErZY3Bix9R1VICDIP5WKjozjBfLZFHTtJXFaBeCUOjFl5AANnAQVf0kw1FJXb/6tB20h6Q3gsbx2OPGHUH7rh/r6oqJA5p5nbyRk9Pa2yf9lTXj2tAGStA20B4HmkzPst88PGmnIV3SZN6jiKPTU97dPz4YnapQ5Vm9aKrA8k+eo6J42S7+0xtfww5NeA07SDUOIv/k8O3ySCoaSQkimXsFoWdK7QTP2Em+6tzTcni1aaQvdSfDCtX94YoElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwqOdyE9HRg16hliKutLKw3dNfIHNM41mzX1LvEFaLWquM1cm5ZGLsq3iEAghoqRk47miBjI1WVHP+U482sQduCw==" + }, + { + "type": "Buffer", + "data": "base64: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" + } + ] + } + ], + "Accounts getBalance should calculate available balance from pending and unconfirmed transactions": [ + { + "version": 1, + "id": "3a74697f-c602-402f-88bd-7e8e68e21ef0", + "name": "accountA", + "spendingKey": "93c6427a951d5f55ce152c06d0b6375b534f979678700274ac687c6dc9cb5242", + "viewKey": "7f5e86de478521cd4b8811282e935f4d40d28afdc4021df9f3c566cbea7fdc856643953dcd403ed9d6cf3061ac2df2ae5281219fe825172c651727444e01506b", + "incomingViewKey": "4e2d69a47d8df0b07f1fb90af55ef2613c3b234fdcc77d0e60f891903ae1c405", + "outgoingViewKey": "6d83eed5a0805e1bd1083002d6e72d561391bc56b01d4d6280b9ea51ec40df3c", + "publicAddress": "ac50ea3ca013b9a876cda1a1b05e00e3a717ab9e51e66178cde4d91dbb06ee5f", + "default": false + }, + { + "version": 1, + "id": "846194da-e26c-4dfd-bc6b-90a15cf7c990", + "name": "accountB", + "spendingKey": "efe1c1b9bd17c5b20fc2b8ddd4046e494c37b608212439adcc5b903cf2bc0f45", + "viewKey": "ef9fb91fd4d23f927252cd3fd5db99094329fd314734818a9ab5433214b5c5ae744c0f3b61378a81c04445f0e70b97cabae0f97710d94d7104cb047faa85730a", + "incomingViewKey": "ad1acc739cf694ffc0176bd6b185e15cc4e6d9939445507ba1ad208d7f6fa707", + "outgoingViewKey": "e7325490427ef9ce224257c9ea52623445ac15d6aeb0e8f698b404d525bbc20f", + "publicAddress": "adcb4879c9191c90d0711c036a2f28e91436137c15c7a8bc7f64c11b9eae3273", + "default": false + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "D179D8B74987D6617267D46F4958554BA0DF02D7E5E6117DB02D6FF38FD0F6DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:tlDFxcrFzUKAs48fglb5Q6kGBGKz9JugJe0CAZZKugw=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:YNdtV2duGIe5Pxl3vVe4ByRXS6dlXJXcY0UJVe0ob1Y=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1677108476851, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAA1hJZIdDBJ7OZwrq0DxMQlEX4SMV49NPadCx/0uU33rCVIj6M/0G1X1Jcfr4i6VCqkSWsB9iyPOq/dtcw2QkdWpH0+CBX7RX0r+2fhAs0QIyhk9WzBnWRksIPdOHHXOasQEoVvTJkDilEtbfx3TF67bVnwszjVkiv1ppxwrFsk5UNsb/Q90k8jHuBBUc9PbqqwqZaPXdQOKgZxj03gfTTsGkn9CfhKoayyqHffCgNQrW0e67TQ2ShpTJeqDRYUljh5Ve/MhUacrS8IOFeujPVfjmF3c70YLe2lYyJBuFvdogYbL9oUIb6tpjyBzLQX97WBT+WuhyEaMlst+7HFmVXH+Nlt0/aU9rgg2S8Gj8WtdExYy7eXHNc4JkAWJMqH3sDwByecspD+zawRQySYExCy3fSe7UV+x5NAQHWscER/l8Ksq3e4voVLpI3RpkPX/JwjkDp1ixvBNduByv9TJmPqLS2H/ZY1ejKqfCT/G9K+NAPFFY36kn2O9SyDq+wAVu4RyO8C6zYXmRSFOrK4YYncKMHkOgOmXXvxoAURHgndqQvicKvt2usi6DMt0RVCPKUTpVA0hsovH/AYUgK1H5VyPD+kk7ngxJCRxzHj8wQb3QpyOr6O6gWQUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwyFiAISQxFfYiPPIPhwEQqYLA435zuj0py0n5Vmdr7DKRmXYtQBoB4SCib+gdNard4cLQXTFA9nLRHN63jm3CBQ==" + } + ] + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "5F19D8F2E15B46431838B3BAE41C7B21668B262595D95522EBDEB88C041703EE", + "noteCommitment": { + "type": "Buffer", + "data": "base64:ILYt7YHL7d86+QVI5L/sPJjaH9vwZyqFs+52h7kthBo=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:vNeG2TFgwA2T5OPgcy6Q+/Fl1+c/rtYGIQaSJOyLXl8=" + }, + "target": "880842937844725196442695540779332307793253899902937591585455087694081134", + "randomness": "0", + "timestamp": 1677108477324, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 5, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAb80IozPPj2xp9VSIys5mFW62ufYykOdVPhIuDKjiuKiUcqQ4UlP1P0Z3wbxkZZvl0hFHs2pgrnS4e9OdoagS2a9l5xG8/gySLBoANIzfno6zKg2Ewwwg3IRwh28VOxbbNAJX8MFFUSTG9GEcgWyd8BpLw3Kx7qDVaNZpJPZ1uiECRegqwxO5/Qz0dt2IRGyFJE2YLpMTrEkTGce+Ep4Ozg3L9cpcyLZR8Nn+NP/fik+JkpBo0cdhIHmxBDx1CPpvGJ821BIyLhaXaGgbJK6Mli6w5CXRc45/ZtmaXvVyzEGY7+HZb7hZmq8F3OlgDj3j2qFAhQJ/H8zVdbyOpzdGHddPhnNSv/sveN1n+dioCdeIim4TVtsJFthfh3wbfosWvBDBNhuw8aRsyfRXUBL6fs/idgyWIbdouD6eQXqOoF+l90aqb89/Dy540q/Euo1m117K7qa4/uE6Tf/9f4XbLhLKvMI77CGTMCRu7Y+5leWvXhb/CvUyJ/Wrhy27Ep6wEuZq5LPJBmSAK802UutJ/Tm2VP7z6HxNcWcQRh2motw6BigC56mNNo6+Rp7t6YFJA4Ec+I+Q9fNdXcadZi8MEJPzQrPzIyie3UCRLJx++Ts1NM5Tr4WetElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwr+zwyz4t7M4F/eljTmxVsxiR/BjdFMNM8JtovMBu9lr7d1ls5NTqP73U5MIl4zR5b30S4tr/lsdySJwcFV13DA==" + } + ] + }, + { + "header": { + "sequence": 4, + "previousBlockHash": "6587D8DCAAD473E390E1227B4C047D65C96358A4B5E99C9AEE2219EEFAEFD42E", + "noteCommitment": { + "type": "Buffer", + "data": "base64:n6bRlRMdf+Hx7aWoLnSyYa4MBQuOjwW64FF2oPO2IhE=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:ZW8HcQZD8M/ZUy8Fu8aZA1vrTFfQZtVD7KB0tn8pZFA=" + }, + "target": "878277375889837647326843029495509009809390053592540685978895509768758568", + "randomness": "0", + "timestamp": 1677108479867, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 8, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/2vKiP////8AAAAANoDrhl+xRHHaLdk04rZQODHWuklRncrMocfVMFsd4Jml0JvPp4srmXS1PMCG0roznIDThejeaVrRAgGB1TBAJ7CMNZl6bbnOT5OfDW/aQJqVC+4JBHrcE+m6T0M4smTtaFu8NorXlc6Ni4Wb0AfUZhZO9Syug8JBa4rq7QMrwMUMDqEgGFeJs1WgoP7Pr3f5K4aUdBorKONvI1htfVp2Nhh8GsooiDyB5uvWVvKMEpyveZ8VJqgCRBvYCAhMWrypxCs8SWJbLu+Y3rO3E2c8+43PZjsEyuFYYmOxrqO7qKzJX2MYOYdFxIe4ubpHTA5lfeEtqjS1+nwprJYzW9Di18cXgFQzOa8qfxaJVZGLyBQH1xYpDZPYrYej5i1BeWYEth8Ov99WbfHjlpqx8v6wj/ig2jLi+ZjHsZi1on/hUN3/VCX5vWn2PFbiKAQxjwAyDA95b4qVGUOmtkpWeAD8Qd/Kx10dFjc8CEL1ZV+d9oBxbxuOvmMLq0BLgHyCFaNoOpqYlEucv+e6qF2uyLCDAWwkCmmJaDj9A5EH30qPMDl7aIy82F2f9B3H1SNkQLF2yevcXsyaD/+JLepSa0+isLLSqCs5FJrEXQtxnt7Qu4K23Cb1nMA9Y0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw7v213X9WMdGL2/0bGWos3D1xUK+oSeZhk4aDtFPO8Ot/nFgphcvg9R6rJ8uDcWbmEbrRDNbcuSg78N7gh5F2CA==" + }, + { + "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/account.test.ts b/ironfish/src/wallet/account.test.ts index f2d9a63e95..fddc52761c 100644 --- a/ironfish/src/wallet/account.test.ts +++ b/ironfish/src/wallet/account.test.ts @@ -362,6 +362,50 @@ describe('Accounts', () => { expect(pendingHashEntry).toBeDefined() }) + + it('should remove spent notes from unspentNoteHashes', async () => { + const { node } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + const accountB = await useAccountFixture(node.wallet, 'accountB') + + const block2 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block2) + await node.wallet.updateHead() + + let unspentA = await AsyncUtils.materialize( + accountA['walletDb'].loadUnspentNoteHashes(accountA), + ) + + expect(unspentA).toHaveLength(1) + + // create a pending transaction + await useTxFixture(node.wallet, accountA, accountB) + + unspentA = await AsyncUtils.materialize( + accountA['walletDb'].loadUnspentNoteHashes(accountA), + ) + expect(unspentA).toHaveLength(0) + }) + + it('should not add output notes to unspentNoteHashes', async () => { + const { node } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + const accountB = await useAccountFixture(node.wallet, 'accountB') + + const block2 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block2) + await node.wallet.updateHead() + + // create a pending transaction + await useTxFixture(node.wallet, accountA, accountB) + + const unspentB = await AsyncUtils.materialize( + accountB['walletDb'].loadUnspentNoteHashes(accountB), + ) + expect(unspentB).toHaveLength(0) + }) }) describe('connectTransaction', () => { @@ -722,6 +766,74 @@ describe('Accounts', () => { supply: null, }) }) + + it('should add received notes to unspentNoteHashes', async () => { + const { node } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + + const block2 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block2) + await node.wallet.updateHead() + + const unspentNoteHashes = await AsyncUtils.materialize( + accountA['walletDb'].loadUnspentNoteHashes(accountA), + ) + + expect(unspentNoteHashes).toHaveLength(1) + + const decryptedNote = await accountA.getDecryptedNote(unspentNoteHashes[0]) + + expect(decryptedNote).toBeDefined() + }) + + it('should remove spent notes from unspentNoteHashes', async () => { + const { node: nodeA } = nodeTest + const { node: nodeB } = await nodeTest.createSetup() + + const accountAnodeA = await useAccountFixture(nodeA.wallet, 'accountA') + + // import account onto nodeB to simulate connecting transaction not seen as pending + const accountAnodeB = await nodeB.wallet.importAccount(accountAnodeA) + + const block2 = await useMinerBlockFixture( + nodeA.chain, + undefined, + accountAnodeA, + nodeA.wallet, + ) + await nodeA.chain.addBlock(block2) + await nodeA.wallet.updateHead() + await nodeB.chain.addBlock(block2) + await nodeB.wallet.updateHead() + + const unspentNoteHashesBefore = await AsyncUtils.materialize( + accountAnodeB['walletDb'].loadUnspentNoteHashes(accountAnodeB), + ) + expect(unspentNoteHashesBefore).toHaveLength(1) + + const transaction = await useTxFixture(nodeA.wallet, accountAnodeA, accountAnodeA) + + // transaction is pending, but nodeB hasn't seen it, so note is still unspent + const unspentNoteHashesPending = await AsyncUtils.materialize( + accountAnodeB['walletDb'].loadUnspentNoteHashes(accountAnodeB), + ) + expect(unspentNoteHashesPending).toEqual(unspentNoteHashesBefore) + + // mine the transaction on a block that nodeB adds + const block3 = await useMinerBlockFixture(nodeA.chain, 3, accountAnodeA, undefined, [ + transaction, + ]) + await nodeA.chain.addBlock(block3) + await nodeA.wallet.updateHead() + await nodeB.chain.addBlock(block3) + await nodeB.wallet.updateHead() + + const unspentNoteHashesAfter = await AsyncUtils.materialize( + accountAnodeB['walletDb'].loadUnspentNoteHashes(accountAnodeB), + ) + expect(unspentNoteHashesAfter).not.toEqual(unspentNoteHashesBefore) + }) }) describe('disconnectTransaction', () => { @@ -1072,6 +1184,39 @@ describe('Accounts', () => { supply: null, }) }) + + it('should remove disconnected output notes from unspentNoteHashes', async () => { + const { node } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + const accountB = await useAccountFixture(node.wallet, 'accountB') + + const block2 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block2) + await node.wallet.updateHead() + + const transaction = await useTxFixture(node.wallet, accountA, accountB) + const block3 = await useMinerBlockFixture(node.chain, 3, accountA, undefined, [ + transaction, + ]) + await node.chain.addBlock(block3) + await node.wallet.updateHead() + + let unspentNoteHashesB = await AsyncUtils.materialize( + accountB['walletDb'].loadUnspentNoteHashes(accountB), + ) + + expect(unspentNoteHashesB).toHaveLength(1) + + // disconnect transaction + await accountB.disconnectTransaction(block3.header, transaction) + + unspentNoteHashesB = await AsyncUtils.materialize( + accountB['walletDb'].loadUnspentNoteHashes(accountB), + ) + + expect(unspentNoteHashesB).toHaveLength(0) + }) }) describe('deleteTransaction', () => { @@ -1265,6 +1410,122 @@ describe('Accounts', () => { unconfirmed: 10n, }) }) + + it('should calculate available balance from pending transactions', async () => { + const { node } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + const accountB = await useAccountFixture(node.wallet, 'accountB') + + const block2 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block2) + await node.wallet.updateHead() + + const balanceA = await accountA.getBalance(Asset.nativeId(), 0) + + expect(balanceA).toMatchObject({ + confirmed: 2000000000n, + unconfirmed: 2000000000n, + available: 2000000000n, + }) + + await useTxFixture(node.wallet, accountA, accountB) + + await expect(accountA.getBalance(Asset.nativeId(), 0)).resolves.toMatchObject({ + pending: balanceA.unconfirmed - 1n, + pendingCount: 1, + available: 0n, + }) + }) + + it('should calculate available balance from unconfirmed transactions', async () => { + const { node } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + const accountB = await useAccountFixture(node.wallet, 'accountB') + + const block2 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block2) + await node.wallet.updateHead() + + await expect(accountA.getBalance(Asset.nativeId(), 0)).resolves.toMatchObject({ + confirmed: 2000000000n, + unconfirmed: 2000000000n, + available: 2000000000n, + }) + + const { block: block3 } = await useBlockWithTx(node, accountA, accountB, false) + await node.chain.addBlock(block3) + await node.wallet.updateHead() + + // with 0 confirmations, available balance includes the transaction + await expect(accountA.getBalance(Asset.nativeId(), 0)).resolves.toMatchObject({ + confirmed: 1999999998n, + unconfirmed: 1999999998n, + available: 1999999998n, + }) + + // with 1 confirmation, available balance should not include the spent note or change + await expect(accountA.getBalance(Asset.nativeId(), 1)).resolves.toMatchObject({ + confirmed: 2000000000n, + unconfirmed: 1999999998n, + available: 0n, + }) + }) + + it('should calculate available balance from pending and unconfirmed transactions', async () => { + const { node } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + const accountB = await useAccountFixture(node.wallet, 'accountB') + + const block2 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block2) + await node.wallet.updateHead() + const block3 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block3) + await node.wallet.updateHead() + + await expect(accountA.getBalance(Asset.nativeId(), 0)).resolves.toMatchObject({ + confirmed: 4000000000n, + unconfirmed: 4000000000n, + available: 4000000000n, + }) + + const { block: block4 } = await useBlockWithTx(node, accountA, accountB, false) + await node.chain.addBlock(block4) + await node.wallet.updateHead() + + // with 0 confirmations, available balance includes the transaction + await expect(accountA.getBalance(Asset.nativeId(), 0)).resolves.toMatchObject({ + confirmed: 3999999998n, + unconfirmed: 3999999998n, + available: 3999999998n, + }) + + // with 1 confirmation, available balance should not include the spent note or change + await expect(accountA.getBalance(Asset.nativeId(), 1)).resolves.toMatchObject({ + confirmed: 4000000000n, + unconfirmed: 3999999998n, + available: 2000000000n, + }) + + // set confirmations to 1 so that new transaction can only spend the last note + node.config.set('confirmations', 1) + + // create a pending transaction sending 1 $ORE from A to B + await useTxFixture(node.wallet, accountA, accountB) + + // with 1 confirmation, all available notes have been spent in unconfirmed or pending transactions + await expect(accountA.getBalance(Asset.nativeId(), 1)).resolves.toMatchObject({ + confirmed: 4000000000n, + unconfirmed: 3999999998n, + pending: 3999999997n, + available: 0n, + pendingCount: 1, + unconfirmedCount: 1, + }) + }) }) describe('calculatePendingBalance', () => { @@ -1308,4 +1569,37 @@ describe('Accounts', () => { }) }) }) + + describe('expireTransaction', () => { + it('should add spent notes back into unspentNoteHashes', async () => { + const { node } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + + const block2 = await useMinerBlockFixture(node.chain, 2, accountA) + await node.chain.addBlock(block2) + await node.wallet.updateHead() + + let unspentHashes = await AsyncUtils.materialize( + accountA['walletDb'].loadUnspentNoteHashes(accountA), + ) + expect(unspentHashes).toHaveLength(1) + const unspentHash = unspentHashes[0] + + const transaction = await useTxFixture(node.wallet, accountA, accountA) + + unspentHashes = await AsyncUtils.materialize( + accountA['walletDb'].loadUnspentNoteHashes(accountA), + ) + expect(unspentHashes).toHaveLength(0) + + await accountA.expireTransaction(transaction) + + unspentHashes = await AsyncUtils.materialize( + accountA['walletDb'].loadUnspentNoteHashes(accountA), + ) + expect(unspentHashes).toHaveLength(1) + expect(unspentHash).toEqualBuffer(unspentHashes[0]) + }) + }) }) diff --git a/ironfish/src/wallet/account.ts b/ironfish/src/wallet/account.ts index 9ec456196f..57f13e6ab4 100644 --- a/ironfish/src/wallet/account.ts +++ b/ironfish/src/wallet/account.ts @@ -155,10 +155,12 @@ export class Account { const pendingNote = await this.getDecryptedNote(decryptedNote.hash, tx) + const spent = pendingNote?.spent ?? false + const note = { accountId: this.id, note: new Note(decryptedNote.serializedNote), - spent: pendingNote?.spent ?? false, + spent, transactionHash: transaction.hash(), nullifier: decryptedNote.nullifier, index: decryptedNote.index, @@ -169,6 +171,10 @@ export class Account { assetBalanceDeltas.increment(note.note.assetId(), note.note.value()) await this.walletDb.saveDecryptedNote(this, decryptedNote.hash, note, tx) + + if (!spent) { + await this.walletDb.addUnspentNoteHash(this, decryptedNote.hash, note, tx) + } } for (const spend of transaction.spends) { @@ -185,6 +191,7 @@ export class Account { const spentNote = { ...note, spent: true } await this.walletDb.saveDecryptedNote(this, spentNoteHash, spentNote, tx) + await this.walletDb.deleteUnspentNoteHash(this, spentNoteHash, spentNote, tx) } transactionValue = { @@ -483,6 +490,7 @@ export class Account { const spentNote = { ...note, spent: true } await this.walletDb.saveDecryptedNote(this, spentNoteHash, spentNote, tx) + await this.walletDb.deleteUnspentNoteHash(this, spentNoteHash, spentNote, tx) } const transactionValue = { @@ -544,6 +552,7 @@ export class Account { }, tx, ) + await this.walletDb.deleteUnspentNoteHash(this, noteHash, decryptedNoteValue, tx) } for (const spend of transaction.spends) { @@ -714,6 +723,7 @@ export class Account { }, tx, ) + await this.walletDb.addUnspentNoteHash(this, noteHash, decryptedNote, tx) } } @@ -762,6 +772,7 @@ export class Account { confirmed: bigint pending: bigint pendingCount: number + available: bigint blockHash: Buffer | null sequence: number | null }> { @@ -787,6 +798,13 @@ export class Account { tx, ) + const available = await this.calculateAvailableBalance( + head.sequence, + assetId, + confirmations, + tx, + ) + yield { assetId, unconfirmed: balance.unconfirmed, @@ -794,6 +812,7 @@ export class Account { confirmed, pending, pendingCount, + available, blockHash: balance.blockHash, sequence: balance.sequence, } @@ -815,6 +834,7 @@ export class Account { confirmed: bigint pending: bigint pendingCount: number + available: bigint blockHash: Buffer | null sequence: number | null }> { @@ -824,6 +844,7 @@ export class Account { unconfirmed: 0n, confirmed: 0n, pending: 0n, + available: 0n, unconfirmedCount: 0, pendingCount: 0, blockHash: null, @@ -849,11 +870,19 @@ export class Account { tx, ) + const available = await this.calculateAvailableBalance( + head.sequence, + assetId, + confirmations, + tx, + ) + return { unconfirmed: balance.unconfirmed, unconfirmedCount, confirmed, pending, + available, pendingCount, blockHash: balance.blockHash, sequence: balance.sequence, @@ -924,6 +953,26 @@ export class Account { } } + async calculateAvailableBalance( + headSequence: number, + assetId: Buffer, + confirmations: number, + tx?: IDatabaseTransaction, + ): Promise { + let available = 0n + + for await (const value of this.walletDb.loadUnspentNoteValues( + this, + assetId, + headSequence - confirmations, + tx, + )) { + available += value + } + + return available + } + async getUnconfirmedBalances(tx?: IDatabaseTransaction): Promise> { const unconfirmedBalances = new BufferMap() for await (const { assetId, balance } of this.walletDb.getUnconfirmedBalances(this, tx)) { diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 2234de15fa..6458502a5a 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -617,6 +617,7 @@ export class Wallet { pending: bigint pendingCount: number confirmed: bigint + available: bigint blockHash: Buffer | null sequence: number | null }> { @@ -639,6 +640,7 @@ export class Wallet { confirmed: bigint pendingCount: number pending: bigint + available: bigint blockHash: Buffer | null sequence: number | null }> { diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 17aa573d34..cf3f32ff78 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -11,6 +11,7 @@ import { NoteEncryptedHash } from '../../primitives/noteEncrypted' import { Nullifier } from '../../primitives/nullifier' import { TransactionHash } from '../../primitives/transaction' import { + BigU64BEEncoding, BUFFER_ENCODING, BufferEncoding, IDatabase, @@ -22,7 +23,11 @@ import { U32_ENCODING_BE, U64_ENCODING, } from '../../storage' -import { StorageUtils } from '../../storage/database/utils' +import { + getPrefixesKeyRange, + getPrefixKeyRange, + StorageUtils, +} from '../../storage/database/utils' import { createDB } from '../../storage/utils' import { WorkerPool } from '../../workerPool' import { Account, calculateAccountPrefix } from '../account' @@ -34,7 +39,7 @@ import { HeadValue, NullableHeadValueEncoding } from './headValue' import { AccountsDBMeta, MetaValue, MetaValueEncoding } from './metaValue' import { TransactionValue, TransactionValueEncoding } from './transactionValue' -const VERSION_DATABASE_ACCOUNTS = 23 +const VERSION_DATABASE_ACCOUNTS = 24 const getAccountsDBMetaDefaults = (): AccountsDBMeta => ({ defaultAccountId: null, @@ -113,6 +118,11 @@ export class WalletDB { value: AssetValue }> + unspentNoteHashes: IDatabaseStore<{ + key: [Account['prefix'], [Buffer, [number, [bigint, Buffer]]]] + value: null + }> + constructor({ files, location, @@ -225,6 +235,28 @@ export class WalletDB { keyEncoding: new PrefixEncoding(new BufferEncoding(), new BufferEncoding(), 4), valueEncoding: new AssetValueEncoding(), }) + + this.unspentNoteHashes = this.db.addStore({ + name: 'un', + keyEncoding: new PrefixEncoding( + new BufferEncoding(), // account prefix + new PrefixEncoding( + new BufferEncoding(), // asset ID + new PrefixEncoding( + U32_ENCODING_BE, // sequence + new PrefixEncoding( + new BigU64BEEncoding(), // value + new BufferEncoding(), // note hash + 8, + ), + 4, + ), + 32, + ), + 4, + ), + valueEncoding: NULL_ENCODING, + }) } async open(): Promise { @@ -502,6 +534,87 @@ export class WalletDB { await this.sequenceToNoteHash.clear(tx, keyRange) } + async addUnspentNoteHash( + account: Account, + noteHash: Buffer, + decryptedNote: DecryptedNoteValue, + tx?: IDatabaseTransaction, + ): Promise { + const sequence = decryptedNote.sequence + + if (sequence === null) { + return + } + + const assetId = decryptedNote.note.assetId() + const value = decryptedNote.note.value() + + await this.unspentNoteHashes.put( + [account.prefix, [assetId, [sequence, [value, noteHash]]]], + null, + tx, + ) + } + + async deleteUnspentNoteHash( + account: Account, + noteHash: Buffer, + decryptedNote: DecryptedNoteValue, + tx?: IDatabaseTransaction, + ): Promise { + const assetId = decryptedNote.note.assetId() + const sequence = decryptedNote.sequence + const value = decryptedNote.note.value() + + Assert.isNotNull(sequence, 'Cannot spend a note that is not on the chain.') + + await this.unspentNoteHashes.del( + [account.prefix, [assetId, [sequence, [value, noteHash]]]], + tx, + ) + } + + async *loadUnspentNoteHashes( + account: Account, + tx?: IDatabaseTransaction, + ): AsyncGenerator { + const range = getPrefixKeyRange(account.prefix) + + for await (const [, [, [, [, noteHash]]]] of this.unspentNoteHashes.getAllKeysIter( + tx, + range, + )) { + yield noteHash + } + } + + async *loadUnspentNoteValues( + account: Account, + assetId: Buffer, + sequence?: number, + tx?: IDatabaseTransaction, + ): AsyncGenerator { + const encoding = new PrefixEncoding( + BUFFER_ENCODING, + new PrefixEncoding(BUFFER_ENCODING, U32_ENCODING_BE, 32), + 4, + ) + + const maxConfirmedSequence = sequence ?? 2 ** 32 - 1 + + const range = getPrefixesKeyRange( + encoding.serialize([account.prefix, [assetId, 1]]), + encoding.serialize([account.prefix, [assetId, maxConfirmedSequence]]), + ) + + for await (const [, [, [, [value, _]]]] of this.unspentNoteHashes.getAllKeysIter( + tx, + range, + )) { + yield value + } + } + async loadNoteHash( account: Account, nullifier: Buffer, From df97fc94a839028e8eb98cc6acd5c2290ababa79 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Fri, 24 Feb 2023 15:21:32 -0800 Subject: [PATCH 2/8] fixes remaining confirmed balance checks (#3548) checks available balance to see if an account has enough funds to send a transaction instead of confirmed balance. available balance is the sum of all unspent note values and more accurately accounts for whether the account can send the transaction. changes confirmed balance references in the following places: - poolShares - sendTransaction RPC - promptCurrency - faucet --- ironfish-cli/src/commands/service/faucet.ts | 2 +- ironfish-cli/src/utils/currency.ts | 2 +- ironfish/src/mining/poolShares.test.ts | 2 +- ironfish/src/mining/poolShares.ts | 8 ++++---- ironfish/src/rpc/routes/wallet/sendTransaction.test.ts | 2 +- ironfish/src/rpc/routes/wallet/sendTransaction.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ironfish-cli/src/commands/service/faucet.ts b/ironfish-cli/src/commands/service/faucet.ts index 5f0e8913bc..d3eb867ece 100644 --- a/ironfish-cli/src/commands/service/faucet.ts +++ b/ironfish-cli/src/commands/service/faucet.ts @@ -167,7 +167,7 @@ export default class Faucet extends IronfishCommand { this.warnedFund = false const maxPossibleRecipients = Math.min( - Number(BigInt(response.content.confirmed) / BigInt(FAUCET_AMOUNT + FAUCET_FEE)), + Number(BigInt(response.content.available) / BigInt(FAUCET_AMOUNT + FAUCET_FEE)), MAX_RECIPIENTS_PER_TRANSACTION, ) diff --git a/ironfish-cli/src/utils/currency.ts b/ironfish-cli/src/utils/currency.ts index ff36c572fb..3581f54f87 100644 --- a/ironfish-cli/src/utils/currency.ts +++ b/ironfish-cli/src/utils/currency.ts @@ -37,7 +37,7 @@ export async function promptCurrency(options: { confirmations: options.balance.confirmations, }) - text += ` (balance ${CurrencyUtils.renderIron(balance.content.confirmed)})` + text += ` (balance ${CurrencyUtils.renderIron(balance.content.available)})` } // eslint-disable-next-line no-constant-condition diff --git a/ironfish/src/mining/poolShares.test.ts b/ironfish/src/mining/poolShares.test.ts index e30ba7117a..6cf0ef81af 100644 --- a/ironfish/src/mining/poolShares.test.ts +++ b/ironfish/src/mining/poolShares.test.ts @@ -226,7 +226,7 @@ describe('poolShares', () => { await shares.submitShare(publicAddress1) await shares.submitShare(publicAddress2) - const hasBalanceSpy = jest.spyOn(shares, 'hasConfirmedBalance').mockResolvedValueOnce(true) + const hasBalanceSpy = jest.spyOn(shares, 'hasAvailableBalance').mockResolvedValueOnce(true) const sendTransactionSpy = jest .spyOn(shares, 'sendTransaction') .mockResolvedValueOnce('testTransactionHash') diff --git a/ironfish/src/mining/poolShares.ts b/ironfish/src/mining/poolShares.ts index 84dcf4388a..4b727fdae9 100644 --- a/ironfish/src/mining/poolShares.ts +++ b/ironfish/src/mining/poolShares.ts @@ -212,7 +212,7 @@ export class MiningPoolShares { 'Payout total must be less than or equal to the total reward amount', ) - const hasEnoughBalance = await this.hasConfirmedBalance(totalRequired) + const hasEnoughBalance = await this.hasAvailableBalance(totalRequired) if (!hasEnoughBalance) { this.logger.info('Insufficient funds for payout, skipping.') return @@ -262,11 +262,11 @@ export class MiningPoolShares { } } - async hasConfirmedBalance(amount: bigint): Promise { + async hasAvailableBalance(amount: bigint): Promise { const balance = await this.rpc.getAccountBalance({ account: this.accountName }) - const confirmedBalance = BigInt(balance.content.confirmed) + const availableBalance = BigInt(balance.content.available) - return confirmedBalance >= amount + return availableBalance >= amount } async sendTransaction( diff --git a/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts b/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts index 6ea444744d..3ea40450b3 100644 --- a/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts +++ b/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts @@ -98,7 +98,7 @@ describe('Transactions sendTransaction', () => { ) }) - it('throws if the confirmed balance is too low', async () => { + it('throws if the available balance is too low', async () => { routeTest.node.peerNetwork['_isReady'] = true routeTest.chain.synced = true diff --git a/ironfish/src/rpc/routes/wallet/sendTransaction.ts b/ironfish/src/rpc/routes/wallet/sendTransaction.ts index 60639d1e14..7a14c63f23 100644 --- a/ironfish/src/rpc/routes/wallet/sendTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/sendTransaction.ts @@ -99,7 +99,7 @@ router.register( for (const [assetId, sum] of totalByAssetId) { const balance = await node.wallet.getBalance(account, assetId) - if (balance.confirmed < sum) { + if (balance.available < sum) { throw new ValidationError( `Your balance is too low. Add funds to your account first`, undefined, From f0310a2db5fba56e7bfd1a9b3a877956c88279ca Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Sat, 25 Feb 2023 13:05:53 -0500 Subject: [PATCH 3/8] Add Features field to Identify message (#3551) --- ironfish/src/network/messageRegistry.test.ts | 3 +++ .../src/network/messages/identify.test.ts | 2 ++ ironfish/src/network/messages/identify.ts | 19 +++++++++++++++ ironfish/src/network/peerNetwork.ts | 23 +++++++++++++++++-- .../connections/webRtcConnection.test.ts | 3 +++ .../connections/webSocketConnection.test.ts | 2 ++ ironfish/src/network/peers/localPeer.ts | 7 ++++++ ironfish/src/network/peers/peer.ts | 6 +++++ ironfish/src/network/peers/peerFeatures.ts | 13 +++++++++++ .../src/network/peers/peerManager.test.ts | 11 +++++++++ ironfish/src/network/peers/peerManager.ts | 1 + ironfish/src/network/testUtilities/helpers.ts | 3 +++ .../network/testUtilities/mockLocalPeer.ts | 2 +- ironfish/src/network/version.ts | 2 +- ironfish/src/syncer.ts | 2 +- 15 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 ironfish/src/network/peers/peerFeatures.ts diff --git a/ironfish/src/network/messageRegistry.test.ts b/ironfish/src/network/messageRegistry.test.ts index d4852d0ef2..ed7b6b367a 100644 --- a/ironfish/src/network/messageRegistry.test.ts +++ b/ironfish/src/network/messageRegistry.test.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { parseNetworkMessage } from './messageRegistry' import { IdentifyMessage } from './messages/identify' +import { defaultFeatures } from './peers/peerFeatures' describe('messageRegistry', () => { describe('parseNetworkMessage', () => { @@ -24,6 +25,7 @@ describe('messageRegistry', () => { work: BigInt(0), networkId: 0, genesisBlockHash: Buffer.alloc(32, 0), + features: defaultFeatures(), }) jest.spyOn(message, 'serialize').mockImplementationOnce(() => Buffer.from('adsf')) @@ -43,6 +45,7 @@ describe('messageRegistry', () => { work: BigInt(0), networkId: 0, genesisBlockHash: Buffer.alloc(32, 0), + features: defaultFeatures(), }) expect(parseNetworkMessage(message.serializeWithMetadata())).toEqual(message) diff --git a/ironfish/src/network/messages/identify.test.ts b/ironfish/src/network/messages/identify.test.ts index 893f8d4c38..9c922626c0 100644 --- a/ironfish/src/network/messages/identify.test.ts +++ b/ironfish/src/network/messages/identify.test.ts @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { identityLength } from '../identity' +import { defaultFeatures } from '../peers/peerFeatures' import { IdentifyMessage } from './identify' describe('IdentifyMessage', () => { @@ -17,6 +18,7 @@ describe('IdentifyMessage', () => { work: BigInt('123'), networkId: 0, genesisBlockHash: Buffer.alloc(32, 0), + features: defaultFeatures(), }) const buffer = message.serialize() diff --git a/ironfish/src/network/messages/identify.ts b/ironfish/src/network/messages/identify.ts index 80cf9374ef..816ec2558c 100644 --- a/ironfish/src/network/messages/identify.ts +++ b/ironfish/src/network/messages/identify.ts @@ -4,6 +4,7 @@ import bufio from 'bufio' import { BigIntUtils } from '../../utils/bigint' import { Identity, identityLength } from '../identity' +import { defaultFeatures, Features, FEATURES_MIN_VERSION } from '../peers/peerFeatures' import { NetworkMessageType } from '../types' import { NetworkMessage } from './networkMessage' @@ -18,6 +19,7 @@ interface CreateIdentifyMessageOptions { work: bigint networkId: number genesisBlockHash: Buffer + features: Features } export class IdentifyMessage extends NetworkMessage { @@ -31,6 +33,7 @@ export class IdentifyMessage extends NetworkMessage { readonly work: bigint readonly networkId: number readonly genesisBlockHash: Buffer + readonly features: Features constructor({ agent, @@ -43,6 +46,7 @@ export class IdentifyMessage extends NetworkMessage { work, networkId, genesisBlockHash, + features, }: CreateIdentifyMessageOptions) { super(NetworkMessageType.Identify) this.agent = agent @@ -55,6 +59,7 @@ export class IdentifyMessage extends NetworkMessage { this.work = work this.networkId = networkId this.genesisBlockHash = genesisBlockHash + this.features = features } serialize(): Buffer { @@ -69,6 +74,11 @@ export class IdentifyMessage extends NetworkMessage { bw.writeVarBytes(BigIntUtils.toBytesLE(this.work)) bw.writeU16(this.networkId) bw.writeHash(this.genesisBlockHash) + + let flags = 0 + flags |= Number(this.features.syncing) << 0 + bw.writeU32(flags) + return bw.render() } @@ -84,6 +94,13 @@ export class IdentifyMessage extends NetworkMessage { const work = BigIntUtils.fromBytesLE(reader.readVarBytes()) const networkId = reader.readU16() const genesisBlockHash = reader.readHash() + + const features = defaultFeatures() + if (version >= FEATURES_MIN_VERSION) { + const flags = reader.readU32() + features.syncing = Boolean(flags & (1 << 0)) + } + return new IdentifyMessage({ agent, head, @@ -95,6 +112,7 @@ export class IdentifyMessage extends NetworkMessage { work, networkId, genesisBlockHash, + features, }) } @@ -110,6 +128,7 @@ export class IdentifyMessage extends NetworkMessage { size += bufio.sizeVarBytes(BigIntUtils.toBytesLE(this.work)) size += 2 // network ID size += 32 // genesis block hash + size += 4 // features return size } } diff --git a/ironfish/src/network/peerNetwork.ts b/ironfish/src/network/peerNetwork.ts index 5e3f252bfa..8028ae913e 100644 --- a/ironfish/src/network/peerNetwork.ts +++ b/ironfish/src/network/peerNetwork.ts @@ -61,6 +61,7 @@ import { import { LocalPeer } from './peers/localPeer' import { BAN_SCORE, KnownBlockHashesValue, Peer } from './peers/peer' import { PeerConnectionManager } from './peers/peerConnectionManager' +import { FEATURES_MIN_VERSION } from './peers/peerFeatures' import { PeerManager } from './peers/peerManager' import { TransactionFetcher } from './transactionFetcher' import { IsomorphicWebSocketConstructor } from './types' @@ -190,6 +191,7 @@ export class PeerNetwork { options.chain, options.webSocket, options.networkId, + this.enableSyncing, ) this.localPeer.port = options.port === undefined ? null : options.port @@ -457,7 +459,11 @@ export class PeerNetwork { private *connectedPeersWithoutTransaction(hash: TransactionHash): Generator { for (const p of this.peerManager.identifiedPeers.values()) { - if (p.state.type === 'CONNECTED' && !this.knowsTransaction(hash, p.state.identity)) { + if ( + p.state.type === 'CONNECTED' && + p.features?.syncing && + !this.knowsTransaction(hash, p.state.identity) + ) { yield p } } @@ -465,7 +471,11 @@ export class PeerNetwork { private *connectedPeersWithoutBlock(hash: BlockHash): Generator { for (const p of this.peerManager.identifiedPeers.values()) { - if (p.state.type === 'CONNECTED' && !p.knownBlockHashes.has(hash)) { + if ( + p.state.type === 'CONNECTED' && + p.features?.syncing && + !p.knownBlockHashes.has(hash) + ) { yield p } } @@ -631,6 +641,15 @@ export class PeerNetwork { ): Promise { const { message } = incomingMessage + if ( + !this.localPeer.enableSyncing && + peer.version !== null && + peer.version >= FEATURES_MIN_VERSION + ) { + peer.punish(BAN_SCORE.MAX) + return + } + if (message instanceof RpcNetworkMessage) { await this.handleRpcMessage(peer, message) } else if (message instanceof NewBlockHashesMessage) { diff --git a/ironfish/src/network/peers/connections/webRtcConnection.test.ts b/ironfish/src/network/peers/connections/webRtcConnection.test.ts index 8f140bf69e..5961b4ce41 100644 --- a/ironfish/src/network/peers/connections/webRtcConnection.test.ts +++ b/ironfish/src/network/peers/connections/webRtcConnection.test.ts @@ -4,6 +4,7 @@ import { Assert } from '../../../assert' import { createRootLogger } from '../../../logger' import { IdentifyMessage } from '../../messages/identify' +import { defaultFeatures } from '../peerFeatures' import { WebRtcConnection } from './webRtcConnection' describe('WebRtcConnection', () => { @@ -21,6 +22,7 @@ describe('WebRtcConnection', () => { work: BigInt(0), networkId: 0, genesisBlockHash: Buffer.alloc(32, 0), + features: defaultFeatures(), }) expect(connection.send(message)).toBe(false) connection.close() @@ -46,6 +48,7 @@ describe('WebRtcConnection', () => { work: BigInt(0), networkId: 0, genesisBlockHash: Buffer.alloc(32, 0), + features: defaultFeatures(), }) expect(connection.send(message)).toBe(true) diff --git a/ironfish/src/network/peers/connections/webSocketConnection.test.ts b/ironfish/src/network/peers/connections/webSocketConnection.test.ts index b0dbcf27ef..6f0b52dee0 100644 --- a/ironfish/src/network/peers/connections/webSocketConnection.test.ts +++ b/ironfish/src/network/peers/connections/webSocketConnection.test.ts @@ -4,6 +4,7 @@ import ws from 'ws' import { createRootLogger } from '../../../logger' import { IdentifyMessage } from '../../messages/identify' +import { defaultFeatures } from '../peerFeatures' import { ConnectionDirection } from './connection' import { WebSocketConnection } from './webSocketConnection' @@ -34,6 +35,7 @@ describe('WebSocketConnection', () => { work: BigInt(0), networkId: 0, genesisBlockHash: Buffer.alloc(32, 0), + features: defaultFeatures(), }) expect(connection.send(message)).toBe(true) diff --git a/ironfish/src/network/peers/localPeer.ts b/ironfish/src/network/peers/localPeer.ts index e4b73a3007..dd3d743edd 100644 --- a/ironfish/src/network/peers/localPeer.ts +++ b/ironfish/src/network/peers/localPeer.ts @@ -26,6 +26,8 @@ export class LocalPeer { readonly webSocket: IsomorphicWebSocketConstructor // the unique ID number of the network readonly networkId: number + // true if the peer supports syncing and gossip messages + readonly enableSyncing: boolean // optional port the local peer is listening on port: number | null @@ -41,6 +43,7 @@ export class LocalPeer { chain: Blockchain, webSocket: IsomorphicWebSocketConstructor, networkId: number, + enableSyncing: boolean, ) { this.privateIdentity = identity this.publicIdentity = privateIdentityToIdentity(identity) @@ -48,6 +51,7 @@ export class LocalPeer { this.agent = agent this.version = version this.networkId = networkId + this.enableSyncing = enableSyncing this.webSocket = webSocket this.port = null @@ -71,6 +75,9 @@ export class LocalPeer { work: this.chain.head.work, networkId: this.networkId, genesisBlockHash: this.chain.genesis.hash, + features: { + syncing: this.enableSyncing, + }, }) } diff --git a/ironfish/src/network/peers/peer.ts b/ironfish/src/network/peers/peer.ts index 8b32d2ea13..899d014236 100644 --- a/ironfish/src/network/peers/peer.ts +++ b/ironfish/src/network/peers/peer.ts @@ -14,6 +14,7 @@ import { displayNetworkMessageType, NetworkMessage } from '../messages/networkMe import { NetworkMessageType } from '../types' import { NetworkError, WebRtcConnection, WebSocketConnection } from './connections' import { Connection, ConnectionType } from './connections/connection' +import { Features } from './peerFeatures' export enum BAN_SCORE { NO = 0, @@ -136,6 +137,11 @@ export class Peer { */ genesisBlockHash: Buffer | null = null + /** + * Features supported by the peer + */ + features: Features | null = null + /** * The loggable name of the peer. For a more specific value, * try Peer.name or Peer.state.identity. diff --git a/ironfish/src/network/peers/peerFeatures.ts b/ironfish/src/network/peers/peerFeatures.ts new file mode 100644 index 0000000000..3543796737 --- /dev/null +++ b/ironfish/src/network/peers/peerFeatures.ts @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export const FEATURES_MIN_VERSION = 21 + +export interface Features { + syncing: boolean +} + +export const defaultFeatures = (): Features => ({ + syncing: true, +}) diff --git a/ironfish/src/network/peers/peerManager.test.ts b/ironfish/src/network/peers/peerManager.test.ts index 1563306a9d..c7c7597280 100644 --- a/ironfish/src/network/peers/peerManager.test.ts +++ b/ironfish/src/network/peers/peerManager.test.ts @@ -35,6 +35,7 @@ import { WebSocketConnection, } from './connections' import { BAN_SCORE } from './peer' +import { defaultFeatures } from './peerFeatures' import { PeerManager } from './peerManager' jest.useFakeTimers() @@ -74,6 +75,7 @@ describe('PeerManager', () => { work: BigInt(0), networkId: localPeer.networkId, genesisBlockHash: localPeer.chain.genesis.hash, + features: defaultFeatures(), }) // Identify peerOut @@ -760,6 +762,7 @@ describe('PeerManager', () => { work: BigInt(0), networkId: localPeer.networkId, genesisBlockHash: localPeer.chain.genesis.hash, + features: defaultFeatures(), }) peer.onMessage.emit(identify, connection) @@ -795,6 +798,7 @@ describe('PeerManager', () => { work: BigInt(0), networkId: localPeer.networkId, genesisBlockHash: localPeer.chain.genesis.hash, + features: defaultFeatures(), }) peer.onMessage.emit(identify, connection) @@ -821,6 +825,7 @@ describe('PeerManager', () => { work: BigInt(0), networkId: localPeer.networkId, genesisBlockHash: localPeer.chain.genesis.hash, + features: defaultFeatures(), }) peer.onMessage.emit(identify, connection) expect(closeSpy).toHaveBeenCalled() @@ -847,6 +852,7 @@ describe('PeerManager', () => { work: BigInt(0), networkId: localPeer.networkId, genesisBlockHash: localPeer.chain.genesis.hash, + features: defaultFeatures(), }) connection.onMessage.emit(identify) @@ -881,6 +887,7 @@ describe('PeerManager', () => { work: BigInt(0), networkId: localPeer.networkId, genesisBlockHash: localPeer.chain.genesis.hash, + features: defaultFeatures(), }) connection.onMessage.emit(identify) @@ -921,6 +928,7 @@ describe('PeerManager', () => { work: BigInt(0), networkId: localPeer.networkId, genesisBlockHash: localPeer.chain.genesis.hash, + features: defaultFeatures(), }) connection.onMessage.emit(identify) @@ -970,6 +978,7 @@ describe('PeerManager', () => { work: BigInt(0), networkId: localPeer.networkId, genesisBlockHash: localPeer.chain.genesis.hash, + features: defaultFeatures(), }) connection.onMessage.emit(id) @@ -1010,6 +1019,7 @@ describe('PeerManager', () => { work: BigInt(0), networkId: localPeer.networkId + 1, genesisBlockHash: localPeer.chain.genesis.hash, + features: defaultFeatures(), }) peer.onMessage.emit(identify, connection) @@ -1037,6 +1047,7 @@ describe('PeerManager', () => { work: BigInt(0), networkId: localPeer.networkId, genesisBlockHash: Buffer.alloc(32, 1), + features: defaultFeatures(), }) Assert.isFalse(identify.genesisBlockHash.equals(localPeer.chain.genesis.hash)) peer.onMessage.emit(identify, connection) diff --git a/ironfish/src/network/peers/peerManager.ts b/ironfish/src/network/peers/peerManager.ts index cb3cd5071d..4ff7441566 100644 --- a/ironfish/src/network/peers/peerManager.ts +++ b/ironfish/src/network/peers/peerManager.ts @@ -1231,6 +1231,7 @@ export class PeerManager { peer.work = message.work peer.networkId = message.networkId peer.genesisBlockHash = message.genesisBlockHash + peer.features = message.features // If we've told the peer to stay disconnected, repeat // the disconnection time before closing the connection diff --git a/ironfish/src/network/testUtilities/helpers.ts b/ironfish/src/network/testUtilities/helpers.ts index 16ac853f6e..68d771d1fd 100644 --- a/ironfish/src/network/testUtilities/helpers.ts +++ b/ironfish/src/network/testUtilities/helpers.ts @@ -15,6 +15,7 @@ import { WebSocketConnection, } from '../peers/connections' import { Peer } from '../peers/peer' +import { defaultFeatures } from '../peers/peerFeatures' import { PeerManager } from '../peers/peerManager' import { WebSocketClient } from '../webSocketClient' import { mockIdentity } from './mockIdentity' @@ -119,6 +120,8 @@ export function getConnectedPeer( connection.setState({ type: 'CONNECTED', identity }) + peer.features = defaultFeatures() + return { peer, connection: connection } } diff --git a/ironfish/src/network/testUtilities/mockLocalPeer.ts b/ironfish/src/network/testUtilities/mockLocalPeer.ts index 7ef710040e..59da2e01b1 100644 --- a/ironfish/src/network/testUtilities/mockLocalPeer.ts +++ b/ironfish/src/network/testUtilities/mockLocalPeer.ts @@ -24,5 +24,5 @@ export function mockLocalPeer({ version?: number chain?: Blockchain } = {}): LocalPeer { - return new LocalPeer(identity, agent, version, chain || mockChain(), WebSocketClient, 0) + return new LocalPeer(identity, agent, version, chain || mockChain(), WebSocketClient, 0, true) } diff --git a/ironfish/src/network/version.ts b/ironfish/src/network/version.ts index fda1fecb78..b6e8664d83 100644 --- a/ironfish/src/network/version.ts +++ b/ironfish/src/network/version.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -export const VERSION_PROTOCOL = 20 +export const VERSION_PROTOCOL = 21 export const VERSION_PROTOCOL_MIN = 19 export const MAX_REQUESTED_BLOCKS = 50 diff --git a/ironfish/src/syncer.ts b/ironfish/src/syncer.ts index 49a872d8af..6cde04b4b8 100644 --- a/ironfish/src/syncer.ts +++ b/ironfish/src/syncer.ts @@ -122,7 +122,7 @@ export class Syncer { // Find all allowed peers that have more work than we have const peers = this.peerNetwork.peerManager .getConnectedPeers() - .filter((peer) => peer.work && peer.work > head.work) + .filter((peer) => peer.features?.syncing && peer.work && peer.work > head.work) // Get a random peer with higher work. We do this to encourage // peer diversity so the highest work peer isn't overwhelmed From abfcd318e592e6acb1714a88eafeb6a5cc5eb99c Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Sat, 25 Feb 2023 16:18:19 -0500 Subject: [PATCH 4/8] Trim file contents when importing an account (#3560) Many editors put new lines at the end of file, and any file with a newline that contains bech32 won't be importable. --- ironfish-cli/src/commands/wallet/import.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 08f5c048ea..609ee9cb60 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -127,7 +127,7 @@ export class ImportCommand extends IronfishCommand { async importFile(path: string): Promise { const resolved = this.sdk.fileSystem.resolve(path) const data = await this.sdk.fileSystem.readFile(resolved) - return this.stringToAccountImport(data) + return this.stringToAccountImport(data.trim()) } async importPipe(): Promise { From 35e3a4d06ba12b0909fc53c7f9cc32a9c12305ae Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Sat, 25 Feb 2023 16:18:53 -0500 Subject: [PATCH 5/8] Fix bech32 import for accounts without view key (#3561) --- ironfish-cli/src/commands/wallet/import.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 609ee9cb60..41811ac319 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -84,7 +84,7 @@ export class ImportCommand extends IronfishCommand { } } CliUx.ux.error( - `Detected mnemonic input, but the import failed. + `Detected mnemonic input, but the import failed. Please verify the input text or use a different method to import wallet`, ) } @@ -101,7 +101,16 @@ export class ImportCommand extends IronfishCommand { // bech32 encoded json const [decoded, _] = Bech32m.decode(data) if (decoded) { - return JSONUtils.parse(decoded) + let data = JSONUtils.parse(decoded) + + if (data.spendingKey) { + data = { + ...data, + ...generateKeyFromPrivateKey(data.spendingKey), + } + } + + return data } // mnemonic or explicit spending key From a99c11059199079ccf7483afe36a2dc588c6c874 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Sat, 25 Feb 2023 16:19:00 -0500 Subject: [PATCH 6/8] Fix JSON import for accounts without a view key (#3562) --- ironfish-cli/src/commands/wallet/import.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 41811ac319..2e520ff2ff 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -127,7 +127,16 @@ export class ImportCommand extends IronfishCommand { // raw json try { - return JSONUtils.parse(data) + let json = JSONUtils.parse(data) + + if (json.spendingKey) { + json = { + ...json, + ...generateKeyFromPrivateKey(json.spendingKey), + } + } + + return json } catch (e) { CliUx.ux.error(`Import failed for the given input: ${data}`) } From 8c5b96ce501e8e5f4ae3a774316cbf30020b2616 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Sat, 25 Feb 2023 16:56:16 -0500 Subject: [PATCH 7/8] Change promptCurrency to accept iron not ore (#3565) * Change promptCurrency to accept iron not ore Users were typing iron in here, but this prompt accepted ore. Changes this to accept and decode iron now. * Fix lint --- ironfish-cli/src/utils/currency.ts | 10 ++-------- ironfish/src/utils/currency.ts | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/ironfish-cli/src/utils/currency.ts b/ironfish-cli/src/utils/currency.ts index 3581f54f87..5e86ba59ab 100644 --- a/ironfish-cli/src/utils/currency.ts +++ b/ironfish-cli/src/utils/currency.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Asset } from '@ironfish/rust-nodejs' -import { Assert, CurrencyUtils, RpcClient } from '@ironfish/sdk' +import { CurrencyUtils, RpcClient } from '@ironfish/sdk' import { CliUx } from '@oclif/core' export async function promptCurrency(options: { @@ -50,13 +50,7 @@ export async function promptCurrency(options: { return null } - const [amount, error] = CurrencyUtils.decodeTry(input) - - if (error) { - throw error - } - - Assert.isNotNull(amount) + const amount = CurrencyUtils.decodeIron(input) if (options.minimum != null && amount < options.minimum) { continue diff --git a/ironfish/src/utils/currency.ts b/ironfish/src/utils/currency.ts index 8ee71ffc44..200e4eea76 100644 --- a/ironfish/src/utils/currency.ts +++ b/ironfish/src/utils/currency.ts @@ -18,7 +18,7 @@ export class CurrencyUtils { } /** - * Parses iron as ore + * Parses iron into ore */ static decodeIron(amount: string | number): bigint { return parseFixed(amount.toString(), 8).toBigInt() From 815b64bca141f5065d02b8163469330f943ccafc Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Sat, 25 Feb 2023 16:56:33 -0500 Subject: [PATCH 8/8] Version bump to version 70 (#3564) --- ironfish-cli/package.json | 4 ++-- ironfish/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 8a3322352a..5402b09e89 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "0.1.69", + "version": "0.1.70", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -61,7 +61,7 @@ "@aws-sdk/client-secrets-manager": "3.276.0", "@aws-sdk/s3-request-presigner": "3.127.0", "@ironfish/rust-nodejs": "0.1.27", - "@ironfish/sdk": "0.0.46", + "@ironfish/sdk": "0.0.47", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", diff --git a/ironfish/package.json b/ironfish/package.json index 558e392b6d..481cc014b7 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "0.0.46", + "version": "0.0.47", "description": "SDK for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js",