From df82420b44baf69eaa91ec791f963dbf00308bd2 Mon Sep 17 00:00:00 2001 From: ygao76 <4500784+ygao76@users.noreply.github.com> Date: Wed, 8 Mar 2023 12:35:22 -0800 Subject: [PATCH 01/12] Get network info rpc (#3622) * Get network info RPC * Just return network id * Make network id as defined --- .../rpc/routes/chain/getNetworkInfo.test.ts | 18 +++++++++++ .../src/rpc/routes/chain/getNetworkInfo.ts | 30 +++++++++++++++++++ ironfish/src/rpc/routes/chain/index.ts | 1 + 3 files changed, 49 insertions(+) create mode 100644 ironfish/src/rpc/routes/chain/getNetworkInfo.test.ts create mode 100644 ironfish/src/rpc/routes/chain/getNetworkInfo.ts diff --git a/ironfish/src/rpc/routes/chain/getNetworkInfo.test.ts b/ironfish/src/rpc/routes/chain/getNetworkInfo.test.ts new file mode 100644 index 0000000000..fe100c8448 --- /dev/null +++ b/ironfish/src/rpc/routes/chain/getNetworkInfo.test.ts @@ -0,0 +1,18 @@ +/* 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 { createRouteTest } from '../../../testUtilities/routeTest' +import { GetNetworkInfoResponse } from './getNetworkInfo' + +describe('Route chain.getNetworkInfo', () => { + const routeTest = createRouteTest() + + it('returns the network id', async () => { + const response = await routeTest.client + .request('chain/getNetworkInfo') + .waitForEnd() + + expect(response.content.networkId).toEqual(routeTest.node.internal.config.networkId) + }) +}) diff --git a/ironfish/src/rpc/routes/chain/getNetworkInfo.ts b/ironfish/src/rpc/routes/chain/getNetworkInfo.ts new file mode 100644 index 0000000000..524d187c06 --- /dev/null +++ b/ironfish/src/rpc/routes/chain/getNetworkInfo.ts @@ -0,0 +1,30 @@ +/* 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 * as yup from 'yup' +import { ApiNamespace, router } from '../router' + +export type GetNetworkInfoRequest = undefined +export type GetNetworkInfoResponse = { + networkId: number +} + +export const GetNetworkInfoRequestSchema: yup.MixedSchema = yup + .mixed() + .oneOf([undefined] as const) + +export const GetNetworkInfoResponseSchema: yup.ObjectSchema = yup + .object({ + networkId: yup.number().defined(), + }) + .defined() + +router.register( + `${ApiNamespace.chain}/getNetworkInfo`, + GetNetworkInfoRequestSchema, + (request, node): void => { + request.end({ + networkId: node.internal.get('networkId'), + }) + }, +) diff --git a/ironfish/src/rpc/routes/chain/index.ts b/ironfish/src/rpc/routes/chain/index.ts index 2952ccccf6..eea3fa2826 100644 --- a/ironfish/src/rpc/routes/chain/index.ts +++ b/ironfish/src/rpc/routes/chain/index.ts @@ -15,3 +15,4 @@ export * from './getTransactionStream' export * from './showChain' export * from './getConsensusParameters' export * from './getAsset' +export * from './getNetworkInfo' From c1b12ea59f217497de1ca1794f8618a4fd2ea092 Mon Sep 17 00:00:00 2001 From: ygao76 <4500784+ygao76@users.noreply.github.com> Date: Thu, 9 Mar 2023 09:14:28 -0800 Subject: [PATCH 02/12] Make faucet only for testnet (#3618) * Make faucet only for testnet * Call get network info to get network id * Hardcode testnet network id * Lint * Format --- ironfish-cli/src/commands/faucet.ts | 12 +++++++++--- ironfish/src/rpc/clients/client.ts | 11 +++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/ironfish-cli/src/commands/faucet.ts b/ironfish-cli/src/commands/faucet.ts index 4468d23d04..d0aa3b477c 100644 --- a/ironfish-cli/src/commands/faucet.ts +++ b/ironfish-cli/src/commands/faucet.ts @@ -11,7 +11,7 @@ import { ONE_FISH_IMAGE, TWO_FISH_IMAGE } from '../images' const FAUCET_DISABLED = false export class FaucetCommand extends IronfishCommand { - static description = `Receive coins from the Iron Fish official Faucet` + static description = `Receive coins from the Iron Fish official testnet Faucet` static flags = { ...RemoteFlags, @@ -33,10 +33,16 @@ export class FaucetCommand extends IronfishCommand { this.exit(1) } - this.log(ONE_FISH_IMAGE) - const client = await this.sdk.connectRpc() + const networkInfoResponse = await client.getNetworkInfo() + + if (networkInfoResponse.content === null || networkInfoResponse.content.networkId !== 0) { + // not testnet + this.log(`The faucet is only available for testnet.`) + this.exit(1) + } + this.log(ONE_FISH_IMAGE) let email = flags.email if (!email) { diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index aab474ded4..92fc556e52 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -38,6 +38,8 @@ import { GetLogStreamResponse, GetNetworkHashPowerRequest, GetNetworkHashPowerResponse, + GetNetworkInfoRequest, + GetNetworkInfoResponse, GetNodeStatusRequest, GetNodeStatusResponse, GetPeersRequest, @@ -603,4 +605,13 @@ export abstract class RpcClient { params, ).waitForEnd() } + + async getNetworkInfo( + params?: GetNetworkInfoRequest, + ): Promise> { + return this.request( + `${ApiNamespace.chain}/getNetworkInfo`, + params, + ).waitForEnd() + } } From 1dd3ef9db7e4a3672a0218689a26f12fcccfe91e Mon Sep 17 00:00:00 2001 From: ygao76 <4500784+ygao76@users.noreply.github.com> Date: Thu, 9 Mar 2023 11:15:14 -0800 Subject: [PATCH 03/12] Add network id check for get fund rpc (#3627) * Add network id check for get fund rpc * Hardcode testnet network id * Lint * Update ironfish/src/rpc/routes/faucet/getFunds.test.ts Co-authored-by: Derek Guenther --------- Co-authored-by: Derek Guenther --- ironfish/src/rpc/routes/faucet/getFunds.test.ts | 11 +++++++++++ ironfish/src/rpc/routes/faucet/getFunds.ts | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/ironfish/src/rpc/routes/faucet/getFunds.test.ts b/ironfish/src/rpc/routes/faucet/getFunds.test.ts index aa3f979112..d81d6f790c 100644 --- a/ironfish/src/rpc/routes/faucet/getFunds.test.ts +++ b/ironfish/src/rpc/routes/faucet/getFunds.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 axios, { AxiosError } from 'axios' +import { DEFAULT_NETWORK_ID } from '../../../fileStores' import { createRouteTest } from '../../../testUtilities/routeTest' import { RpcRequestError } from '../../clients' @@ -18,6 +19,7 @@ describe('Route faucet.getFunds', () => { accountName = 'test' + Math.random().toString() const account = await routeTest.node.wallet.createAccount(accountName, true) publicAddress = account.publicAddress + routeTest.node.internal.set('networkId', DEFAULT_NETWORK_ID) }) describe('when the API request succeeds', () => { @@ -74,4 +76,13 @@ describe('Route faucet.getFunds', () => { ) }) }) + + describe('should fail when non testnet node', () => { + it('throws an error', async () => { + routeTest.node.internal.set('networkId', 2) + await expect(routeTest.client.getFunds({ account: accountName, email })).rejects.toThrow( + 'This endpoint is only available for testnet.', + ) + }) + }) }) diff --git a/ironfish/src/rpc/routes/faucet/getFunds.ts b/ironfish/src/rpc/routes/faucet/getFunds.ts index 29ce8ad986..264cafb925 100644 --- a/ironfish/src/rpc/routes/faucet/getFunds.ts +++ b/ironfish/src/rpc/routes/faucet/getFunds.ts @@ -29,6 +29,14 @@ router.register( `${ApiNamespace.faucet}/getFunds`, GetFundsRequestSchema, async (request, node): Promise => { + // check node network id + const networkId = node.internal.get('networkId') + + if (networkId !== 0) { + // not testnet + throw new ResponseError('This endpoint is only available for testnet.', ERROR_CODES.ERROR) + } + const account = getAccount(node, request.data.account) const api = new WebApi({ From 73b0abe789177d1d26cbc11b2508b0ed4ffd8af8 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Thu, 9 Mar 2023 18:26:59 -0500 Subject: [PATCH 04/12] Replace bigint casting with bigint literal (#3624) * Replace bigint casting with bigint literal * Update ironfish/src/strategy.test.slow.ts Co-authored-by: Hugh Cunningham <57735705+hughy@users.noreply.github.com> * Add tsconfig to ironfish-rust-nodejs --------- Co-authored-by: Derek Guenther Co-authored-by: Hugh Cunningham <57735705+hughy@users.noreply.github.com> --- ironfish-rust-nodejs/tests/demo.test.slow.ts | 16 +++++++------- ironfish-rust-nodejs/tsconfig.json | 4 ++++ ironfish/src/consensus/verifier.ts | 7 ++---- ironfish/src/primitives/target.ts | 21 ++++++++---------- ironfish/src/strategy.test.slow.ts | 22 +++++++++---------- ironfish/src/syncer.test.ts | 14 ++++++------ .../src/testUtilities/helpers/blockchain.ts | 2 +- ironfish/src/utils/bigint.ts | 2 +- ironfish/src/wallet/account.ts | 6 ++--- ironfish/src/wallet/wallet.ts | 8 +++---- ironfish/src/wallet/walletdb/walletdb.ts | 4 ++-- ironfish/src/workerPool/tasks/decryptNotes.ts | 4 ++-- 12 files changed, 53 insertions(+), 57 deletions(-) create mode 100644 ironfish-rust-nodejs/tsconfig.json diff --git a/ironfish-rust-nodejs/tests/demo.test.slow.ts b/ironfish-rust-nodejs/tests/demo.test.slow.ts index 691e041b69..96bdd94622 100644 --- a/ironfish-rust-nodejs/tests/demo.test.slow.ts +++ b/ironfish-rust-nodejs/tests/demo.test.slow.ts @@ -59,13 +59,13 @@ describe('Demonstrate the Sapling API', () => { const key = generateKey() const transaction = new Transaction(key.spendingKey) - const note = new Note(key.publicAddress, BigInt(20), 'test', Asset.nativeId(), key.publicAddress) + const note = new Note(key.publicAddress, 20n, 'test', Asset.nativeId(), key.publicAddress) transaction.output(note) const serializedPostedTransaction = transaction.post_miners_fee() const postedTransaction = new TransactionPosted(serializedPostedTransaction) - expect(postedTransaction.fee()).toEqual(BigInt(-20)) + expect(postedTransaction.fee()).toEqual(-20n) expect(postedTransaction.notesLength()).toBe(1) expect(postedTransaction.spendsLength()).toBe(0) expect(postedTransaction.hash().byteLength).toBe(32) @@ -87,8 +87,8 @@ describe('Demonstrate the Sapling API', () => { // Null characters are included in the memo string expect(decryptedNote.memo().replace(/\0/g, '')).toEqual('test') - expect(decryptedNote.value()).toEqual(BigInt(20)) - expect(decryptedNote.nullifier(key.viewKey, BigInt(0)).byteLength).toBeGreaterThan(BigInt(0)) + expect(decryptedNote.value()).toEqual(20n) + expect(decryptedNote.nullifier(key.viewKey, 0n).byteLength).toBeGreaterThan(0n) }) it(`Should create a standard transaction`, () => { @@ -96,7 +96,7 @@ describe('Demonstrate the Sapling API', () => { const recipientKey = generateKey() const minersFeeTransaction = new Transaction(key.spendingKey) - const minersFeeNote = new Note(key.publicAddress, BigInt(20), 'miner', Asset.nativeId(), key.publicAddress) + const minersFeeNote = new Note(key.publicAddress, 20n, 'miner', Asset.nativeId(), key.publicAddress) minersFeeTransaction.output(minersFeeNote) const postedMinersFeeTransaction = new TransactionPosted(minersFeeTransaction.post_miners_fee()) @@ -105,7 +105,7 @@ describe('Demonstrate the Sapling API', () => { transaction.setExpiration(10) const encryptedNote = new NoteEncrypted(postedMinersFeeTransaction.getNote(0)) const decryptedNote = Note.deserialize(encryptedNote.decryptNoteForOwner(key.incomingViewKey)!) - const newNote = new Note(recipientKey.publicAddress, BigInt(15), 'receive', Asset.nativeId(), minersFeeNote.owner()) + const newNote = new Note(recipientKey.publicAddress, 15n, 'receive', Asset.nativeId(), minersFeeNote.owner()) let currentHash = encryptedNote.hash() let authPath = Array.from({ length: 32 }, (_, depth) => { @@ -128,10 +128,10 @@ describe('Demonstrate the Sapling API', () => { transaction.spend(decryptedNote, witness) transaction.output(newNote) - const postedTransaction = new TransactionPosted(transaction.post(key.publicAddress, BigInt(5))) + const postedTransaction = new TransactionPosted(transaction.post(key.publicAddress, 5n)) expect(postedTransaction.expiration()).toEqual(10) - expect(postedTransaction.fee()).toEqual(BigInt(5)) + expect(postedTransaction.fee()).toEqual(5n) expect(postedTransaction.notesLength()).toEqual(1) expect(postedTransaction.spendsLength()).toEqual(1) expect(postedTransaction.hash().byteLength).toEqual(32) diff --git a/ironfish-rust-nodejs/tsconfig.json b/ironfish-rust-nodejs/tsconfig.json new file mode 100644 index 0000000000..88c34c08e7 --- /dev/null +++ b/ironfish-rust-nodejs/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../config/tsconfig.base.json" +} + \ No newline at end of file diff --git a/ironfish/src/consensus/verifier.ts b/ironfish/src/consensus/verifier.ts index b71c5d793f..94c752d3a8 100644 --- a/ironfish/src/consensus/verifier.ts +++ b/ironfish/src/consensus/verifier.ts @@ -113,7 +113,7 @@ export class Verifier { } // Sum the total transaction fees - let totalTransactionFees = BigInt(0) + let totalTransactionFees = 0n for (const transaction of otherTransactions) { const transactionFee = transaction.fee() if (transactionFee < 0) { @@ -133,10 +133,7 @@ export class Verifier { // minersFee should be (negative) miningReward + totalTransactionFees const miningReward = this.chain.strategy.miningReward(block.header.sequence) - if ( - minersFeeTransaction.fee() !== - BigInt(-1) * (BigInt(miningReward) + totalTransactionFees) - ) { + if (minersFeeTransaction.fee() !== -1n * (BigInt(miningReward) + totalTransactionFees)) { return { valid: false, reason: VerificationResultReason.INVALID_MINERS_FEE } } diff --git a/ironfish/src/primitives/target.ts b/ironfish/src/primitives/target.ts index 9bd325452f..c27be8d36a 100644 --- a/ironfish/src/primitives/target.ts +++ b/ironfish/src/primitives/target.ts @@ -7,28 +7,25 @@ import { BigIntUtils } from '../utils/bigint' /** * Minimum difficulty, which is equivalent to maximum target */ -const MIN_DIFFICULTY = BigInt(131072) +const MIN_DIFFICULTY = 131072n /** * Maximum target, which is equivalent of minimum difficulty of 131072 * target == 2**256 / difficulty */ -const MAX_TARGET = BigInt( - '883423532389192164791648750371459257913741948437809479060803100646309888', -) +const MAX_TARGET = 883423532389192164791648750371459257913741948437809479060803100646309888n /** * Maximum number to represent a 256 bit number, which is 2**256 - 1 */ -const MAX_256_BIT_NUM = BigInt( - '115792089237316195423570985008687907853269984665640564039457584007913129639935', -) +const MAX_256_BIT_NUM = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n export class Target { targetValue: bigint constructor(targetValue: bigint | Buffer | string | number | undefined = undefined) { if (targetValue === undefined) { - this.targetValue = BigInt(0) + this.targetValue = 0n } else { const candidate = targetValue instanceof Buffer @@ -128,7 +125,7 @@ export class Target { bucket = Math.min(bucket, 99) const difficulty = - previousBlockDifficulty - (previousBlockDifficulty / BigInt(2048)) * BigInt(bucket) + previousBlockDifficulty - (previousBlockDifficulty / 2048n) * BigInt(bucket) return BigIntUtils.max(difficulty, Target.minDifficulty()) } @@ -145,17 +142,17 @@ export class Target { * Converts difficulty to Target */ static fromDifficulty(difficulty: bigint): Target { - if (difficulty === BigInt(1)) { + if (difficulty === 1n) { return new Target(MAX_256_BIT_NUM) } - return new Target((BigInt(2) ** BigInt(256) / BigInt(difficulty)).valueOf()) + return new Target((2n ** 256n / difficulty).valueOf()) } /** * Return the difficulty representation as a big integer */ toDifficulty(): bigint { - return BigInt(2) ** BigInt(256) / this.targetValue + return 2n ** 256n / this.targetValue } /** diff --git a/ironfish/src/strategy.test.slow.ts b/ironfish/src/strategy.test.slow.ts index cd86dc672d..4d94822025 100644 --- a/ironfish/src/strategy.test.slow.ts +++ b/ironfish/src/strategy.test.slow.ts @@ -96,7 +96,7 @@ describe('Demonstrate the Sapling API', () => { it('Can create a miner reward', () => { const owner = generateKeyFromPrivateKey(spenderKey.spendingKey).publicAddress - minerNote = new NativeNote(owner, BigInt(42), '', Asset.nativeId(), owner) + minerNote = new NativeNote(owner, 42n, '', Asset.nativeId(), owner) const transaction = new NativeTransaction(spenderKey.spendingKey) transaction.output(minerNote) @@ -139,14 +139,14 @@ describe('Demonstrate the Sapling API', () => { receiverKey = generateKey() const outputNote = new NativeNote( receiverKey.publicAddress, - BigInt(40), + 40n, '', Asset.nativeId(), minerNote.owner(), ) transaction.output(outputNote) - publicTransaction = new NativeTransactionPosted(transaction.post(null, BigInt(0))) + publicTransaction = new NativeTransactionPosted(transaction.post(null, 0n)) expect(publicTransaction).toBeTruthy() }) @@ -176,7 +176,7 @@ describe('Demonstrate the Sapling API', () => { workerPool, consensus: new TestnetConsensus(consensusParameters), }) - const minersFee = await strategy.createMinersFee(BigInt(0), 0, generateKey().spendingKey) + const minersFee = await strategy.createMinersFee(0n, 0, generateKey().spendingKey) expect(minersFee['transactionPosted']).toBeNull() expect(await workerPool.verify(minersFee, { verifyFees: false })).toEqual({ valid: true }) @@ -190,7 +190,7 @@ describe('Demonstrate the Sapling API', () => { consensus: new TestnetConsensus(consensusParameters), }) - const minersFee = await strategy.createMinersFee(BigInt(0), 0, generateKey().spendingKey) + const minersFee = await strategy.createMinersFee(0n, 0, generateKey().spendingKey) await minersFee.withReference(async () => { expect(minersFee['transactionPosted']).not.toBeNull() @@ -213,7 +213,7 @@ describe('Demonstrate the Sapling API', () => { workerPool: new WorkerPool(), consensus: new TestnetConsensus(consensusParameters), }) - const minersFee = await strategy.createMinersFee(BigInt(0), 0, key.spendingKey) + const minersFee = await strategy.createMinersFee(0n, 0, key.spendingKey) expect(minersFee['transactionPosted']).toBeNull() @@ -233,7 +233,7 @@ describe('Demonstrate the Sapling API', () => { } expect(decryptedNote['note']).toBeNull() - expect(decryptedNote.value()).toBe(BigInt(2000000000)) + expect(decryptedNote.value()).toBe(2000000000n) expect(decryptedNote['note']).toBeNull() }) }) @@ -284,14 +284,14 @@ describe('Demonstrate the Sapling API', () => { const receiverAddress = receiverKey.publicAddress const noteForSpender = new NativeNote( spenderKey.publicAddress, - BigInt(10), + 10n, '', Asset.nativeId(), receiverAddress, ) const receiverNoteToSelf = new NativeNote( receiverAddress, - BigInt(29), + 29n, '', Asset.nativeId(), receiverAddress, @@ -300,9 +300,7 @@ describe('Demonstrate the Sapling API', () => { transaction.output(noteForSpender) transaction.output(receiverNoteToSelf) - const postedTransaction = new NativeTransactionPosted( - transaction.post(undefined, BigInt(1)), - ) + const postedTransaction = new NativeTransactionPosted(transaction.post(undefined, 1n)) expect(postedTransaction).toBeTruthy() expect(postedTransaction.verify()).toBeTruthy() }) diff --git a/ironfish/src/syncer.test.ts b/ironfish/src/syncer.test.ts index 5c8487845b..d1efa5676e 100644 --- a/ironfish/src/syncer.test.ts +++ b/ironfish/src/syncer.test.ts @@ -33,13 +33,13 @@ describe('Syncer', () => { expect(startSyncSpy).not.toHaveBeenCalled() const { peer } = getConnectedPeer(peerNetwork.peerManager) - peer.work = BigInt(0) + peer.work = 0n // Peer does not have more work syncer.findPeer() expect(startSyncSpy).not.toHaveBeenCalled() - peer.work = chain.head.work + BigInt(1) + peer.work = chain.head.work + 1n // Peer should have more work than us now syncer.findPeer() @@ -50,7 +50,7 @@ describe('Syncer', () => { const { peerNetwork, syncer } = nodeTest const { peer } = getConnectedPeer(peerNetwork.peerManager) - peer.work = BigInt(1) + peer.work = 1n peer.sequence = 1 peer.head = Buffer.from('') @@ -76,7 +76,7 @@ describe('Syncer', () => { const { peerNetwork, syncer } = nodeTest const { peer } = getConnectedPeer(peerNetwork.peerManager) - peer.work = BigInt(1) + peer.work = 1n peer.sequence = 1 peer.head = Buffer.from('') @@ -106,7 +106,7 @@ describe('Syncer', () => { const { peerNetwork, syncer } = nodeTest const { peer } = getConnectedPeer(peerNetwork.peerManager) - peer.work = BigInt(1) + peer.work = 1n peer.sequence = 1 peer.head = Buffer.from('') @@ -156,7 +156,7 @@ describe('Syncer', () => { const { peer } = getConnectedPeer(peerNetwork.peerManager) peer.sequence = blockA4.header.sequence peer.head = blockA4.header.hash - peer.work = BigInt(10) + peer.work = 10n const getBlocksSpy = jest .spyOn(peerNetwork, 'getBlocks') @@ -203,7 +203,7 @@ describe('Syncer', () => { const { peer } = getConnectedPeer(peerNetwork.peerManager) peer.sequence = 2 peer.head = Buffer.alloc(32, 1) - peer.work = BigInt(10) + peer.work = 10n const getBlocksSpy = jest .spyOn(peerNetwork, 'getBlocks') diff --git a/ironfish/src/testUtilities/helpers/blockchain.ts b/ironfish/src/testUtilities/helpers/blockchain.ts index 7c4aeaf070..d100e557a4 100644 --- a/ironfish/src/testUtilities/helpers/blockchain.ts +++ b/ironfish/src/testUtilities/helpers/blockchain.ts @@ -6,5 +6,5 @@ import '../matchers/blockchain' import { Target } from '../../primitives/target' export function acceptsAllTarget(): Target { - return new Target(BigInt(2) ** BigInt(256) - BigInt(1)) + return new Target(2n ** 256n - 1n) } diff --git a/ironfish/src/utils/bigint.ts b/ironfish/src/utils/bigint.ts index 390c77bdaf..227259c263 100644 --- a/ironfish/src/utils/bigint.ts +++ b/ironfish/src/utils/bigint.ts @@ -44,7 +44,7 @@ function min(a: bigint, b: bigint): bigint { */ function fromBytesBE(bytes: Buffer): bigint { if (bytes.length === 0) { - return BigInt(0) + return 0n } const hex: string[] = [] diff --git a/ironfish/src/wallet/account.ts b/ironfish/src/wallet/account.ts index 94d514235f..2877374c1c 100644 --- a/ironfish/src/wallet/account.ts +++ b/ironfish/src/wallet/account.ts @@ -281,7 +281,7 @@ export class Account { const existingAsset = await this.walletDb.getAsset(this, asset.id(), tx) let createdTransactionHash = transaction.hash() - let supply = BigInt(0) + let supply = 0n // Adjust supply if this transaction is connected on a block. if (blockHash && sequence) { @@ -336,7 +336,7 @@ export class Account { Assert.isNotNull(existingAsset.supply, 'Supply should be non-null for asset') const supply = existingAsset.supply - value - Assert.isTrue(supply >= BigInt(0), 'Invalid burn value') + Assert.isTrue(supply >= 0n, 'Invalid burn value') await this.walletDb.putAsset( this, @@ -412,7 +412,7 @@ export class Account { const existingSupply = existingAsset.supply const supply = existingSupply - value - Assert.isTrue(supply >= BigInt(0)) + Assert.isTrue(supply >= 0n) let blockHash = existingAsset.blockHash let sequence = existingAsset.sequence diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 0d83e2d91b..b905f22b13 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -928,12 +928,12 @@ export class Wallet { amountsNeeded.set(Asset.nativeId(), options.fee) for (const output of raw.outputs) { - const currentAmount = amountsNeeded.get(output.note.assetId()) ?? BigInt(0) + const currentAmount = amountsNeeded.get(output.note.assetId()) ?? 0n amountsNeeded.set(output.note.assetId(), currentAmount + output.note.value()) } for (const burn of raw.burns) { - const currentAmount = amountsNeeded.get(burn.assetId) ?? BigInt(0) + const currentAmount = amountsNeeded.get(burn.assetId) ?? 0n amountsNeeded.set(burn.assetId, currentAmount + burn.value) } @@ -971,7 +971,7 @@ export class Wallet { amountNeeded: bigint, confirmations: number, ): Promise<{ amount: bigint; notes: Array<{ note: Note; witness: NoteWitness }> }> { - let amount = BigInt(0) + let amount = 0n const notes: Array<{ note: Note; witness: NoteWitness }> = [] const head = await sender.getHead() @@ -980,7 +980,7 @@ export class Wallet { } for await (const unspentNote of this.getUnspentNotes(sender, assetId)) { - if (unspentNote.note.value() <= BigInt(0)) { + if (unspentNote.note.value() <= 0n) { continue } diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 0edaadb120..524613a21b 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -289,7 +289,7 @@ export class WalletDB { account, Asset.nativeId(), { - unconfirmed: BigInt(0), + unconfirmed: 0n, blockHash: null, sequence: null, }, @@ -879,7 +879,7 @@ export class WalletDB { return ( unconfirmedBalance ?? { - unconfirmed: BigInt(0), + unconfirmed: 0n, blockHash: null, sequence: null, } diff --git a/ironfish/src/workerPool/tasks/decryptNotes.ts b/ironfish/src/workerPool/tasks/decryptNotes.ts index a0ad1e3a9e..8846e427eb 100644 --- a/ironfish/src/workerPool/tasks/decryptNotes.ts +++ b/ironfish/src/workerPool/tasks/decryptNotes.ts @@ -228,7 +228,7 @@ export class DecryptNotesTask extends WorkerTask { // Try decrypting the note as the owner const receivedNote = note.decryptNoteForOwner(incomingViewKey) - if (receivedNote && receivedNote.value() !== BigInt(0)) { + if (receivedNote && receivedNote.value() !== 0n) { decryptedNotes.push({ index: currentNoteIndex, forSpender: false, @@ -245,7 +245,7 @@ export class DecryptNotesTask extends WorkerTask { if (decryptForSpender) { // Try decrypting the note as the spender const spentNote = note.decryptNoteForSpender(outgoingViewKey) - if (spentNote && spentNote.value() !== BigInt(0)) { + if (spentNote && spentNote.value() !== 0n) { decryptedNotes.push({ index: currentNoteIndex, forSpender: true, From 610088b86adcecc888b7a7f191bf23abc5265754 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 9 Mar 2023 16:28:49 -0800 Subject: [PATCH 05/12] fixes getUnspentNotes when confirmations >= chain length (#3632) if the length of the chain is shorter than or equal to the confirmation range then we calculate a non-positive number for the maximum confirmed sequence. this results in an error when attempting to construct a key range for loading unspent notes from the unspentNoteHashes index. the genesis block is always confirmed, so the maximum confirmed sequence can never be less than 1. fixes getUnspentNotes by setting maxConfirmedSequence to be at least 1. --- .../__fixtures__/account.test.ts.fixture | 39 +++++++++++++++++++ ironfish/src/wallet/account.test.ts | 24 ++++++++++++ ironfish/src/wallet/account.ts | 2 +- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/ironfish/src/wallet/__fixtures__/account.test.ts.fixture b/ironfish/src/wallet/__fixtures__/account.test.ts.fixture index e707e77ab4..2772ac1249 100644 --- a/ironfish/src/wallet/__fixtures__/account.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/account.test.ts.fixture @@ -3521,5 +3521,44 @@ } ] } + ], + "Accounts getUnspentNotes should load no unspent notes with no confirmed blocks": [ + { + "version": 1, + "id": "c4177e2b-ee73-43ca-8712-e893e5e89f86", + "name": "accountA", + "spendingKey": "14f0551148e8dec0f4f2ff83c9475c2ab88cc8c07b2340854a0ed32213e7b408", + "viewKey": "730c6953808ce3ee390daeac4d1ca7b015b7dae558375d5fb7d821c1b215c0b08f1d8747f390c81d69c19bfde7b52ea513befd771a97640611edd8ec9cdcac46", + "incomingViewKey": "29c5626c2bf4b1566eafdb6e63f8204ef0929a4a7fb8f941357fb7e6aba08302", + "outgoingViewKey": "84b5e7f1cce602942dbed46583604597346cf98dfda451652d7d382d81d35c19", + "publicAddress": "f5317749223c4dc4d09e5105a69336c6292d088d470874f6dfc5088fc9e7a960", + "createdAt": "2023-03-09T22:56:21.777Z" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "93ACCF91C6793C3CCB9E380A2E186ABD06EA0FC6F6C71E35D6FC7D0A98693DFD", + "noteCommitment": { + "type": "Buffer", + "data": "base64:0Ffv1BNu2yop6YWgt4AcQshpko7KD6OX8VzKIsdga0w=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:J+slL2SRlhnfQilFEcPkrN+HwWthOvaaV0AH0K5HKxs=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1678402582773, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAA7hu0q568+0RSRdFIuAp3TltNIF7xYLA4hxkLlGYvPaiwVotBAQuK8rGb8Gzbp+gzLoAoeAVf2ombgUGRvT7smTwX6CWRNXmTUwRQxJ3uC6K0XTF+BQr7ftZ7ufcpKHPkKIc6MWsK2MD67/3nvt89FyyT9CwRD00ZhUh0J4gmAoQO4ojIV4FVIULG7RlU3GZ/2YVZaqWPv4Aur4IwvlnBsHD+AIIio6/ztto8cQDKHESI6gGwqIlPnFljIk3FP2Kwh3WOzVJXHVJ8U2ZpC9ItdJg49URHux1N2gpKt/qevoAWysyy6L24O2c1C4nxrhZdYx2GkUxUtAId/rlUdlxToIblvsfcqzl6Z4URSET+lmzTO6UgeitlEw+iourPUvxbyInTWSA74x7gk4m9yqVtW+TrqEDCUECepTJPqVe4DbBoBoxKUV4byLcvPUPCx7GHRyjEM/Ps9hMhbCAE2IN3f0WSX6SefGq/R7KpTid+wXfcPU5nArjEb5U13T2AWRS2RkUxPUAPr9lRuPgAn30m21KqO1Wb8Yexyqdv+LbaBKtelSUVhDlrDAl+pxrsrkNbRE1wbS1VZE/Noku6UkWOFft0Uk2Eq4d+lOnlV2VgohTGH7YRFy476Elyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwKLG3XrltfDDT5kW3+sreZAAVMO+mwecGcpBWN6J14L0rTa5c5QoYal1XRe9oNuqs0pElq3LmWe8AkYLl9tfvBw==" + } + ] + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/account.test.ts b/ironfish/src/wallet/account.test.ts index 8ff05f1cf9..d2bf541b7f 100644 --- a/ironfish/src/wallet/account.test.ts +++ b/ironfish/src/wallet/account.test.ts @@ -1981,5 +1981,29 @@ describe('Accounts', () => { expect(unspentNotes).toHaveLength(1) }) + + it('should load no unspent notes with no confirmed blocks', 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 unspentNotes = await AsyncUtils.materialize( + accountA.getUnspentNotes(Asset.nativeId(), { confirmations: 0 }), + ) + + expect(unspentNotes).toHaveLength(1) + + unspentNotes = await AsyncUtils.materialize( + accountA.getUnspentNotes(Asset.nativeId(), { + confirmations: node.chain.head.sequence + 1, + }), + ) + + expect(unspentNotes).toHaveLength(0) + }) }) }) diff --git a/ironfish/src/wallet/account.ts b/ironfish/src/wallet/account.ts index 2877374c1c..b4ec0d0c6a 100644 --- a/ironfish/src/wallet/account.ts +++ b/ironfish/src/wallet/account.ts @@ -103,7 +103,7 @@ export class Account { const confirmations = options?.confirmations ?? 0 - const maxConfirmedSequence = head.sequence - confirmations + const maxConfirmedSequence = Math.max(head.sequence - confirmations, GENESIS_BLOCK_SEQUENCE) for await (const decryptedNote of this.walletDb.loadUnspentNotes( this, From 2d80160cf953c72358ede1695896bef80a2dc99a Mon Sep 17 00:00:00 2001 From: ygao76 <4500784+ygao76@users.noreply.github.com> Date: Thu, 9 Mar 2023 17:50:09 -0800 Subject: [PATCH 06/12] Remove is confirmed check (#3633) * Remove is confirmed check * Pass confirmations --- ironfish/src/wallet/wallet.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index b905f22b13..f54f1bb213 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -979,7 +979,7 @@ export class Wallet { return { amount, notes } } - for await (const unspentNote of this.getUnspentNotes(sender, assetId)) { + for await (const unspentNote of this.getUnspentNotes(sender, assetId, { confirmations })) { if (unspentNote.note.value() <= 0n) { continue } @@ -988,11 +988,6 @@ export class Wallet { Assert.isNotNull(unspentNote.nullifier) Assert.isNotNull(unspentNote.sequence) - const isConfirmed = head.sequence - unspentNote.sequence >= confirmations - if (!isConfirmed) { - continue - } - if (await this.checkNoteOnChainAndRepair(sender, unspentNote)) { continue } From aacffc501bd7ea112265f958c2b9b94c6b6ac271 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 9 Mar 2023 18:25:10 -0800 Subject: [PATCH 07/12] fixes genesis block transaction status (#3634) all transactions on the genesis block are confirmed. 'wallet:balance --all' shows a 'confirmed' balance of 0 for the genesis account when the chain contains only the genesis block. fixes calculation of confirmed balance by ensuring that the range of unconfirmed sequences starts at at least 2 'wallet:transactions' shows a single 'unconfirmed' transaction for the genesis account when the chain contains only the genesis block. fixes display of transaction status by checking if transaction has sequence equal to genesis block sequence --- ironfish/src/wallet/account.ts | 6 ++---- ironfish/src/wallet/wallet.ts | 5 ++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ironfish/src/wallet/account.ts b/ironfish/src/wallet/account.ts index b4ec0d0c6a..0b0386c4d7 100644 --- a/ironfish/src/wallet/account.ts +++ b/ironfish/src/wallet/account.ts @@ -959,10 +959,8 @@ export class Account { if (confirmations > 0) { const unconfirmedSequenceEnd = headSequence - const unconfirmedSequenceStart = Math.max( - unconfirmedSequenceEnd - confirmations + 1, - GENESIS_BLOCK_SEQUENCE, - ) + const unconfirmedSequenceStart = + Math.max(unconfirmedSequenceEnd - confirmations, GENESIS_BLOCK_SEQUENCE) + 1 for await (const transaction of this.walletDb.loadTransactionsInSequenceRange( this, diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index f54f1bb213..22f05f6db2 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -16,6 +16,7 @@ import { getFee } from '../memPool/feeEstimator' import { NoteHasher } from '../merkletree/hasher' import { NoteWitness, Witness } from '../merkletree/witness' import { Mutex } from '../mutex' +import { GENESIS_BLOCK_SEQUENCE } from '../primitives' import { BlockHeader } from '../primitives/blockheader' import { BurnDescription } from '../primitives/burnDescription' import { Note } from '../primitives/note' @@ -1197,7 +1198,9 @@ export class Wallet { } if (transaction.sequence) { - const isConfirmed = headSequence - transaction.sequence >= confirmations + const isConfirmed = + transaction.sequence === GENESIS_BLOCK_SEQUENCE || + headSequence - transaction.sequence >= confirmations return isConfirmed ? TransactionStatus.CONFIRMED : TransactionStatus.UNCONFIRMED } else { From bed07f6f8328889b17882cdf9dcc464f09750c32 Mon Sep 17 00:00:00 2001 From: ygao76 <4500784+ygao76@users.noreply.github.com> Date: Thu, 9 Mar 2023 18:33:03 -0800 Subject: [PATCH 08/12] Allow no for broadcast in wallet:post (#3635) --- ironfish-cli/src/commands/wallet/post.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ironfish-cli/src/commands/wallet/post.ts b/ironfish-cli/src/commands/wallet/post.ts index 92c4d77c1b..80dcf086b0 100644 --- a/ironfish-cli/src/commands/wallet/post.ts +++ b/ironfish-cli/src/commands/wallet/post.ts @@ -34,6 +34,7 @@ export class PostCommand extends IronfishCommand { }), broadcast: Flags.boolean({ default: true, + allowNo: true, description: 'Broadcast the transaction after posting', }), } From 56d9dfd4c618dc97e898a1134ab0eeea5cf4a35e Mon Sep 17 00:00:00 2001 From: Evan Richard <5766842+EvanJRichard@users.noreply.github.com> Date: Fri, 10 Mar 2023 11:46:10 -0500 Subject: [PATCH 09/12] Feature: RPC HTTP Adapter (#3630) Review: https://github.com/iron-fish/ironfish/pull/3630 --- ironfish/src/fileStores/config.ts | 11 ++ ironfish/src/rpc/adapters/httpAdapter.ts | 219 +++++++++++++++++++++++ ironfish/src/rpc/adapters/index.ts | 1 + ironfish/src/sdk.ts | 15 +- 4 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 ironfish/src/rpc/adapters/httpAdapter.ts diff --git a/ironfish/src/fileStores/config.ts b/ironfish/src/fileStores/config.ts index 3b4ce2a35d..10989a78f3 100644 --- a/ironfish/src/fileStores/config.ts +++ b/ironfish/src/fileStores/config.ts @@ -13,6 +13,8 @@ export const DEFAULT_DISCORD_INVITE = 'https://discord.ironfish.network' export const DEFAULT_USE_RPC_IPC = true export const DEFAULT_USE_RPC_TCP = false export const DEFAULT_USE_RPC_TLS = true +// TODO(daniel): Setting this to false until we can get HTTPS + basic auth +export const DEFAULT_USE_RPC_HTTP = false export const DEFAULT_POOL_HOST = '::' export const DEFAULT_POOL_PORT = 9034 export const DEFAULT_NETWORK_ID = 0 @@ -33,6 +35,7 @@ export type ConfigOptions = { enableRpcIpc: boolean enableRpcTcp: boolean enableRpcTls: boolean + enableRpcHttp: boolean enableSyncing: boolean enableTelemetry: boolean enableMetrics: boolean @@ -93,6 +96,8 @@ export type ConfigOptions = { rpcTcpPort: number tlsKeyPath: string tlsCertPath: string + rpcHttpHost: string + rpcHttpPort: number /** * The maximum number of peers we can be connected to at a time. Past this number, * new connections will be rejected. @@ -276,6 +281,7 @@ export const ConfigOptionsSchema: yup.ObjectSchema> = yup enableRpcIpc: yup.boolean(), enableRpcTcp: yup.boolean(), enableRpcTls: yup.boolean(), + enableRpcHttp: yup.boolean(), enableSyncing: yup.boolean(), enableTelemetry: yup.boolean(), enableMetrics: yup.boolean(), @@ -298,6 +304,8 @@ export const ConfigOptionsSchema: yup.ObjectSchema> = yup rpcTcpPort: YupUtils.isPort, tlsKeyPath: yup.string().trim(), tlsCertPath: yup.string().trim(), + rpcHttpHost: yup.string().trim(), + rpcHttpPort: YupUtils.isPort, maxPeers: YupUtils.isPositiveInteger, minPeers: YupUtils.isPositiveInteger, targetPeers: yup.number().integer().min(1), @@ -369,6 +377,7 @@ export class Config extends KeyStore { enableRpcIpc: DEFAULT_USE_RPC_IPC, enableRpcTcp: DEFAULT_USE_RPC_TCP, enableRpcTls: DEFAULT_USE_RPC_TLS, + enableRpcHttp: DEFAULT_USE_RPC_HTTP, enableSyncing: true, enableTelemetry: false, enableMetrics: true, @@ -388,6 +397,8 @@ export class Config extends KeyStore { rpcTcpPort: 8020, tlsKeyPath: files.resolve(files.join(dataDir, 'certs', 'node-key.pem')), tlsCertPath: files.resolve(files.join(dataDir, 'certs', 'node-cert.pem')), + rpcHttpHost: 'localhost', + rpcHttpPort: 8021, maxPeers: 50, confirmations: 2, minPeers: 1, diff --git a/ironfish/src/rpc/adapters/httpAdapter.ts b/ironfish/src/rpc/adapters/httpAdapter.ts new file mode 100644 index 0000000000..172ef49e54 --- /dev/null +++ b/ironfish/src/rpc/adapters/httpAdapter.ts @@ -0,0 +1,219 @@ +/* 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 http from 'http' +import { v4 as uuid } from 'uuid' +import { Assert } from '../../assert' +import { createRootLogger, Logger } from '../../logger' +import { ErrorUtils } from '../../utils' +import { RpcRequest } from '../request' +import { ApiNamespace, Router } from '../routes' +import { RpcServer } from '../server' +import { IRpcAdapter } from './adapter' +import { ERROR_CODES, ResponseError } from './errors' + +const MEGABYTES = 1000 * 1000 +const MAX_REQUEST_SIZE = 5 * MEGABYTES + +export type HttpRpcError = { + status: number + code: string + message: string + stack?: string +} + +export class RpcHttpAdapter implements IRpcAdapter { + server: http.Server | null = null + router: Router | null = null + + readonly host: string + readonly port: number + readonly logger: Logger + readonly namespaces: ApiNamespace[] + private requests: Map< + string, + { + rpcRequest?: RpcRequest + req: http.IncomingMessage + waitForClose: Promise + } + > + + constructor( + host: string, + port: number, + logger: Logger = createRootLogger(), + namespaces: ApiNamespace[], + ) { + this.host = host + this.port = port + this.logger = logger + this.namespaces = namespaces + this.requests = new Map() + } + + attach(server: RpcServer): void | Promise { + this.router = server.getRouter(this.namespaces) + } + + start(): Promise { + this.logger.debug(`Serving RPC on HTTP ${this.host}:${this.port}`) + + const server = http.createServer() + this.server = server + + return new Promise((resolve, reject) => { + const onError = (err: unknown) => { + server.off('error', onError) + server.off('listening', onListening) + reject(err) + } + + const onListening = () => { + server.off('error', onError) + server.off('listening', onListening) + + server.on('request', (req, res) => { + const requestId = uuid() + + const waitForClose = new Promise((resolve) => { + req.on('close', () => { + this.cleanUpRequest(requestId) + resolve() + }) + }) + + this.requests.set(requestId, { req, waitForClose }) + + void this.handleRequest(req, res, requestId).catch((e) => { + const error = ErrorUtils.renderError(e) + this.logger.debug(`Error in HTTP adapter: ${error}`) + let errorResponse: HttpRpcError = { + code: ERROR_CODES.ERROR, + status: 500, + message: error, + } + + if (e instanceof ResponseError) { + errorResponse = { + code: e.code, + status: e.status, + message: e.message, + stack: e.stack, + } + } + + res.writeHead(errorResponse.status) + res.end(JSON.stringify(errorResponse)) + + this.cleanUpRequest(requestId) + }) + }) + + resolve() + } + + server.on('error', onError) + server.on('listening', onListening) + server.listen(this.port, this.host) + }) + } + + async stop(): Promise { + for (const { req, rpcRequest } of this.requests.values()) { + req.destroy() + rpcRequest?.close() + } + + await new Promise((resolve) => { + this.server?.close(() => resolve()) || resolve() + }) + + await Promise.all( + Array.from(this.requests.values()).map(({ waitForClose }) => waitForClose), + ) + } + + cleanUpRequest(requestId: string): void { + const request = this.requests.get(requestId) + + // TODO: request.req was is already closed at this point + // but do we need to clean that up here at all + request?.rpcRequest?.close() + this.requests.delete(requestId) + } + + async handleRequest( + request: http.IncomingMessage, + response: http.ServerResponse, + requestId: string, + ): Promise { + if (this.router === null || this.router.server === null) { + throw new ResponseError('Tried to connect to unmounted adapter') + } + + const router = this.router + + if (request.url === undefined) { + throw new ResponseError('No request url provided') + } + + this.logger.debug( + `Call HTTP RPC: ${request.method || 'undefined'} ${request.url || 'undefined'}`, + ) + + // TODO(daniel): better way to parse method from request here + const url = new URL(request.url, `http://${request.headers.host || 'localhost'}`) + const route = url.pathname.substring(1) + + if (request.method !== 'POST') { + throw new ResponseError( + `Route does not exist, Did you mean to use POST?`, + ERROR_CODES.ROUTE_NOT_FOUND, + 404, + ) + } + + // TODO(daniel): clean up reading body code here a bit of possible + let size = 0 + const data: Buffer[] = [] + + for await (const chunk of request) { + Assert.isInstanceOf(chunk, Buffer) + size += chunk.byteLength + data.push(chunk) + + if (size >= MAX_REQUEST_SIZE) { + throw new ResponseError('Max request size exceeded') + } + } + + const combined = Buffer.concat(data) + // TODO(daniel): some routes assume that no data will be passed as undefined + // so keeping that convention here. Could think of a better way to handle? + const body = combined.length ? combined.toString('utf8') : undefined + + const rpcRequest = new RpcRequest( + body === undefined ? undefined : JSON.parse(body), + route, + (status: number, data?: unknown) => { + response.writeHead(status, { + 'Content-Type': 'application/json', + }) + response.end(JSON.stringify({ status, data })) + this.cleanUpRequest(requestId) + }, + (data: unknown) => { + // TODO: see if this is correct way to implement HTTP streaming. + // do more headers need to be set, etc.?? + const bufferData = Buffer.from(JSON.stringify(data)) + response.write(bufferData) + }, + ) + + const currRequest = this.requests.get(requestId) + currRequest && this.requests.set(requestId, { ...currRequest, rpcRequest }) + + await router.route(route, rpcRequest) + } +} diff --git a/ironfish/src/rpc/adapters/index.ts b/ironfish/src/rpc/adapters/index.ts index 9d0cf800ed..94324252a0 100644 --- a/ironfish/src/rpc/adapters/index.ts +++ b/ironfish/src/rpc/adapters/index.ts @@ -4,6 +4,7 @@ export * from './adapter' export * from './errors' +export * from './httpAdapter' export * from './ipcAdapter' export * from './socketAdapter' export * from './tcpAdapter' diff --git a/ironfish/src/sdk.ts b/ironfish/src/sdk.ts index b773542138..02b903363b 100644 --- a/ironfish/src/sdk.ts +++ b/ironfish/src/sdk.ts @@ -25,7 +25,7 @@ import { WebSocketClient } from './network/webSocketClient' import { IronfishNode } from './node' import { IronfishPKG, Package } from './package' import { Platform } from './platform' -import { RpcSocketClient, RpcTlsAdapter } from './rpc' +import { RpcHttpAdapter, RpcSocketClient, RpcTlsAdapter } from './rpc' import { RpcIpcAdapter } from './rpc/adapters/ipcAdapter' import { RpcTcpAdapter } from './rpc/adapters/tcpAdapter' import { RpcClient } from './rpc/clients/client' @@ -200,10 +200,19 @@ export class IronfishSdk { }) if (this.config.get('enableRpcIpc')) { - const namespaces = ALL_API_NAMESPACES + await node.rpc.mount( + new RpcIpcAdapter(this.config.get('ipcPath'), this.logger, ALL_API_NAMESPACES), + ) + } + if (this.config.get('enableRpcHttp')) { await node.rpc.mount( - new RpcIpcAdapter(this.config.get('ipcPath'), this.logger, namespaces), + new RpcHttpAdapter( + this.config.get('rpcHttpHost'), + this.config.get('rpcHttpPort'), + this.logger, + ALL_API_NAMESPACES, + ), ) } From 696141786f8618440a96d78f5e2ae36bc5c0771b Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Fri, 10 Mar 2023 09:43:04 -0800 Subject: [PATCH 10/12] Use confirmations when checking balance in sendTransaction (#3637) --- ironfish/src/rpc/routes/wallet/sendTransaction.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ironfish/src/rpc/routes/wallet/sendTransaction.ts b/ironfish/src/rpc/routes/wallet/sendTransaction.ts index 7a14c63f23..bff62ab1d4 100644 --- a/ironfish/src/rpc/routes/wallet/sendTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/sendTransaction.ts @@ -97,7 +97,9 @@ router.register( // Check that the node has enough balance for (const [assetId, sum] of totalByAssetId) { - const balance = await node.wallet.getBalance(account, assetId) + const balance = await node.wallet.getBalance(account, assetId, { + confirmations: request.data.confirmations ?? undefined, + }) if (balance.available < sum) { throw new ValidationError( From 78139ab1a763468ffad318517b8dd26f94f1fb9b Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Fri, 10 Mar 2023 12:51:42 -0800 Subject: [PATCH 11/12] changes timestampToTransactionHash to one-to-many (#3625) * changes timestampToTransactionHash to one-to-many it's possible for multiple transactions to have the same timestamp in a wallet. for example, if an account receives two transactions that are on the same block, but did not sync those transactions until they were on the chain, then both transactions will have the block header timestamp. this example might occur in practice if a node was offline when the transactions were first created and/or did not receive the transactions in gossip messages before they were added to the chain. this might also happen for an account that is imported to a wallet after the transactions were added to the chain. when rescanning the chain for the imported account all transactions will use the timestamp of the block header of the block they are found on. we use the 'timestampToTransactionHash' index to load transactions in order for the 'getAccountTransactions' RPC. the impact of the one-to-one datastore is that the RPC would miss transactions that the account has. * adds more expectations to test ensure that the right transactions are returned from getTransactionsByTime in the expected order * fixes order of transactions in test do not expect on ordering for accountA transactions - fixture generation sets the timestamps for pending transactions to be equal to the time of the test run, so we cannot maintain ordering on these timestamps right now --- .../data/026-timestamp-to-transactions.ts | 98 +++++++++++++++ .../new/index.ts | 33 +++++ .../old/accountValue.ts | 84 +++++++++++++ .../old/index.ts | 46 +++++++ .../old/transactionValue.ts | 112 +++++++++++++++++ .../026-timestamp-to-transactions/stores.ts | 16 +++ ironfish/src/migrations/data/index.ts | 2 + .../__fixtures__/account.test.ts.fixture | 118 ++++++++++++++++++ ironfish/src/wallet/account.test.ts | 57 +++++++++ ironfish/src/wallet/walletdb/walletdb.ts | 29 +++-- 10 files changed, 584 insertions(+), 11 deletions(-) create mode 100644 ironfish/src/migrations/data/026-timestamp-to-transactions.ts create mode 100644 ironfish/src/migrations/data/026-timestamp-to-transactions/new/index.ts create mode 100644 ironfish/src/migrations/data/026-timestamp-to-transactions/old/accountValue.ts create mode 100644 ironfish/src/migrations/data/026-timestamp-to-transactions/old/index.ts create mode 100644 ironfish/src/migrations/data/026-timestamp-to-transactions/old/transactionValue.ts create mode 100644 ironfish/src/migrations/data/026-timestamp-to-transactions/stores.ts diff --git a/ironfish/src/migrations/data/026-timestamp-to-transactions.ts b/ironfish/src/migrations/data/026-timestamp-to-transactions.ts new file mode 100644 index 0000000000..706daea9fc --- /dev/null +++ b/ironfish/src/migrations/data/026-timestamp-to-transactions.ts @@ -0,0 +1,98 @@ +/* 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 './026-timestamp-to-transactions/stores' + +export class Migration026 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 accounts = [] + const stores = GetStores(db) + + for await (const account of stores.old.accounts.getAllValuesIter()) { + accounts.push( + new Account({ + ...account, + createdAt: null, + walletDb: node.wallet.walletDb, + }), + ) + } + + const accountsString = + accounts.length === 1 ? `${accounts.length} account` : `${accounts.length} accounts` + logger.info(`Indexing transaction timestamps for ${accountsString}`) + + for (const account of accounts) { + logger.info('') + logger.info(` Indexing transaction timestamps for account ${account.name}`) + + let transactionCount = 0 + for await (const { timestamp, transaction } of stores.old.transactions.getAllValuesIter( + undefined, + account.prefixRange, + )) { + await stores.new.timestampToTransactionHash.put( + [account.prefix, [timestamp.getTime(), transaction.hash()]], + null, + ) + + transactionCount++ + } + + const transactionsString = + transactionCount === 1 + ? `${transactionCount} transaction` + : `${transactionCount} transactions` + logger.info(` Completed indexing ${transactionsString} for account ${account.name}`) + } + + await stores.old.timestampToTransactionHash.clear() + logger.info('') + } + + async backward(node: IronfishNode, db: IDatabase): Promise { + const accounts = [] + const stores = GetStores(db) + + for await (const account of stores.old.accounts.getAllValuesIter()) { + accounts.push( + new Account({ + ...account, + createdAt: null, + walletDb: node.wallet.walletDb, + }), + ) + } + + for (const account of accounts) { + for await (const { timestamp, transaction } of stores.old.transactions.getAllValuesIter( + undefined, + account.prefixRange, + )) { + await stores.old.timestampToTransactionHash.put( + [account.prefix, timestamp.getTime()], + transaction.hash(), + ) + } + + await stores.new.timestampToTransactionHash.clear() + } + } +} diff --git a/ironfish/src/migrations/data/026-timestamp-to-transactions/new/index.ts b/ironfish/src/migrations/data/026-timestamp-to-transactions/new/index.ts new file mode 100644 index 0000000000..ba95bdb2c2 --- /dev/null +++ b/ironfish/src/migrations/data/026-timestamp-to-transactions/new/index.ts @@ -0,0 +1,33 @@ +/* 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 { + BufferEncoding, + IDatabase, + IDatabaseStore, + NULL_ENCODING, + PrefixEncoding, + U64_ENCODING, +} from '../../../../storage' + +export function GetNewStores(db: IDatabase): { + timestampToTransactionHash: IDatabaseStore<{ + key: [Buffer, [number, Buffer]] + value: null + }> +} { + const timestampToTransactionHash: IDatabaseStore<{ + key: [Buffer, [number, Buffer]] + value: null + }> = db.addStore({ + name: 'TT', + keyEncoding: new PrefixEncoding( + new BufferEncoding(), + new PrefixEncoding(U64_ENCODING, new BufferEncoding(), 8), + 4, + ), + valueEncoding: NULL_ENCODING, + }) + + return { timestampToTransactionHash } +} diff --git a/ironfish/src/migrations/data/026-timestamp-to-transactions/old/accountValue.ts b/ironfish/src/migrations/data/026-timestamp-to-transactions/old/accountValue.ts new file mode 100644 index 0000000000..dedebec152 --- /dev/null +++ b/ironfish/src/migrations/data/026-timestamp-to-transactions/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 +export 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/026-timestamp-to-transactions/old/index.ts b/ironfish/src/migrations/data/026-timestamp-to-transactions/old/index.ts new file mode 100644 index 0000000000..f0c6e87d4d --- /dev/null +++ b/ironfish/src/migrations/data/026-timestamp-to-transactions/old/index.ts @@ -0,0 +1,46 @@ +/* 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 { + BufferEncoding, + IDatabase, + IDatabaseStore, + PrefixEncoding, + StringEncoding, + U64_ENCODING, +} from '../../../../storage' +import { AccountValue, AccountValueEncoding } from './accountValue' +import { TransactionValue, TransactionValueEncoding } from './transactionValue' + +export function GetOldStores(db: IDatabase): { + accounts: IDatabaseStore<{ key: string; value: AccountValue }> + transactions: IDatabaseStore<{ key: [Buffer, Buffer]; value: TransactionValue }> + timestampToTransactionHash: IDatabaseStore<{ + key: [Buffer, number] + value: Buffer + }> +} { + const accounts: IDatabaseStore<{ key: string; value: AccountValue }> = db.addStore({ + name: 'a', + keyEncoding: new StringEncoding(), + valueEncoding: new AccountValueEncoding(), + }) + + const transactions: IDatabaseStore<{ + key: [Buffer, Buffer] + value: TransactionValue + }> = db.addStore({ + name: 't', + keyEncoding: new PrefixEncoding(new BufferEncoding(), new BufferEncoding(), 4), + valueEncoding: new TransactionValueEncoding(), + }) + + const timestampToTransactionHash: IDatabaseStore<{ key: [Buffer, number]; value: Buffer }> = + db.addStore({ + name: 'T', + keyEncoding: new PrefixEncoding(new BufferEncoding(), U64_ENCODING, 4), + valueEncoding: new BufferEncoding(), + }) + + return { accounts, transactions, timestampToTransactionHash } +} diff --git a/ironfish/src/migrations/data/026-timestamp-to-transactions/old/transactionValue.ts b/ironfish/src/migrations/data/026-timestamp-to-transactions/old/transactionValue.ts new file mode 100644 index 0000000000..083ba413e8 --- /dev/null +++ b/ironfish/src/migrations/data/026-timestamp-to-transactions/old/transactionValue.ts @@ -0,0 +1,112 @@ +/* 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 type { IDatabaseEncoding } from '../../../../storage/database/types' +import { BufferMap } from 'buffer-map' +import bufio from 'bufio' +import { Transaction } from '../../../../primitives' + +const ASSET_ID_LENGTH = 32 + +export interface TransactionValue { + transaction: Transaction + timestamp: Date + // These fields are populated once the transaction is on the main chain + blockHash: Buffer | null + sequence: number | null + // This is populated when we create a transaction to track when we should + // rebroadcast. This can be null if we created it on another node, or the + // transaction was created for us by another person. + submittedSequence: number + assetBalanceDeltas: BufferMap +} + +export class TransactionValueEncoding implements IDatabaseEncoding { + serialize(value: TransactionValue): Buffer { + const { transaction, blockHash, sequence, submittedSequence, timestamp } = value + + const bw = bufio.write(this.getSize(value)) + bw.writeVarBytes(transaction.serialize()) + bw.writeU64(timestamp.getTime()) + + let flags = 0 + flags |= Number(!!blockHash) << 0 + flags |= Number(!!sequence) << 1 + bw.writeU8(flags) + + if (blockHash) { + bw.writeHash(blockHash) + } + if (sequence) { + bw.writeU32(sequence) + } + + bw.writeU32(submittedSequence) + + const assetCount = value.assetBalanceDeltas.size + bw.writeU32(assetCount) + + for (const [assetId, balanceDelta] of value.assetBalanceDeltas) { + bw.writeHash(assetId) + bw.writeBigI64(balanceDelta) + } + + return bw.render() + } + + deserialize(buffer: Buffer): TransactionValue { + const reader = bufio.read(buffer, true) + const transaction = new Transaction(reader.readVarBytes()) + const timestamp = new Date(reader.readU64()) + + const flags = reader.readU8() + const hasBlockHash = flags & (1 << 0) + const hasSequence = flags & (1 << 1) + + let blockHash = null + if (hasBlockHash) { + blockHash = reader.readHash() + } + + let sequence = null + if (hasSequence) { + sequence = reader.readU32() + } + + const submittedSequence = reader.readU32() + + const assetBalanceDeltas = new BufferMap() + const assetCount = reader.readU32() + + for (let i = 0; i < assetCount; i++) { + const assetId = reader.readHash() + const balanceDelta = reader.readBigI64() + assetBalanceDeltas.set(assetId, balanceDelta) + } + + return { + transaction, + blockHash, + submittedSequence, + sequence, + timestamp, + assetBalanceDeltas, + } + } + + getSize(value: TransactionValue): number { + let size = bufio.sizeVarBytes(value.transaction.serialize()) + size += 8 + size += 1 + if (value.blockHash) { + size += 32 + } + if (value.sequence) { + size += 4 + } + size += 4 + size += 4 + size += value.assetBalanceDeltas.size * (ASSET_ID_LENGTH + 8) + return size + } +} diff --git a/ironfish/src/migrations/data/026-timestamp-to-transactions/stores.ts b/ironfish/src/migrations/data/026-timestamp-to-transactions/stores.ts new file mode 100644 index 0000000000..b046b7c66f --- /dev/null +++ b/ironfish/src/migrations/data/026-timestamp-to-transactions/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 5688d0c9b5..d06dccf1aa 100644 --- a/ironfish/src/migrations/data/index.ts +++ b/ironfish/src/migrations/data/index.ts @@ -14,6 +14,7 @@ import { Migration022 } from './022-add-view-key-account' import { Migration023 } from './023-wallet-optional-spending-key' import { Migration024 } from './024-unspent-notes' import { Migration025 } from './025-backfill-wallet-nullifier-to-transaction-hash' +import { Migration026 } from './026-timestamp-to-transactions' export const MIGRATIONS = [ Migration014, @@ -28,4 +29,5 @@ export const MIGRATIONS = [ Migration023, Migration024, Migration025, + Migration026, ] diff --git a/ironfish/src/wallet/__fixtures__/account.test.ts.fixture b/ironfish/src/wallet/__fixtures__/account.test.ts.fixture index 2772ac1249..cd1f977fc6 100644 --- a/ironfish/src/wallet/__fixtures__/account.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/account.test.ts.fixture @@ -3560,5 +3560,123 @@ } ] } + ], + "Accounts getTransactionsByTime loads multiple transactions on a block for an account": [ + { + "version": 1, + "id": "7c56f476-0e61-4dba-960b-8e3240b7a941", + "name": "accountA", + "spendingKey": "0e3458b5982783ac3a026b8d196c9cf822ebef30bdc5fc6598b7f2b20aae3c0a", + "viewKey": "dc45cc0955fc970fad2c73195b6f973fe828371a09959178b449cb958d757ddc605b307cdacb7f6f1348ce29a018156ed1d448b4e53e1d95405317c7d53b7e39", + "incomingViewKey": "381eb338cf7c72cae5fab4bab9440a84a704dc9017e9a0a784536079a7979406", + "outgoingViewKey": "3dbc52dcb9c81a57fbcee95089b443ac5f801016431ffc9420039df71c44cc83", + "publicAddress": "7fcba29f49dae1e2bc76616b4275329fbfd2bedbb5f9f5ad18010455d0ca79cc", + "createdAt": "2023-03-10T19:22:11.580Z" + }, + { + "version": 1, + "id": "3c2c7985-eb8f-4a05-b231-0707b0411399", + "name": "accountB", + "spendingKey": "b3c952ba50818c7083ea8184809cd8740ce08fa4990734df736671ba3781a882", + "viewKey": "9189355a0d331968848929e6f0d143fb3f881efffc8c0f6dd6eb6b267cf7128ead49dafc0f747a34590526787bf9943845daa4bdb17ff3d77cf01961a4d9660e", + "incomingViewKey": "94087665118c9f430db9da464f5e35e2f80206590d69609ce08a910e6dbdf104", + "outgoingViewKey": "d8a5ab71d1c0d5511946495001905a97e262bace56477f89f4823732f7863f96", + "publicAddress": "30d6a7bf0ae65e169545e8a26f647e55794ec716ebeae2ff5bd058995be2fdb6", + "createdAt": "2023-03-10T19:22:11.592Z" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "93ACCF91C6793C3CCB9E380A2E186ABD06EA0FC6F6C71E35D6FC7D0A98693DFD", + "noteCommitment": { + "type": "Buffer", + "data": "base64:gdHvQYpJK6owfw4q6cHPxATR6xIdetq1EQO0Es9UExM=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:CFVXp7/ECxuIfp81nIOhSfedCcrCh5UyTTX9rHVFOzA=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1678476132367, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAhZ+ADIWGcSJ3zo/LBZBLotHxrADxo0B7QKYnpotPo0KCw5izOCRUVn8IkZsrSK/CfGACJuYriE3CTYvzOVUYitcMWaL6OC3VJG0aoInGRouiZ0EKFXkXWBE8179CM6VEakvEdjyP701QyOf4o6UEdh6xIv7OzchjNKdvowAmztsNDcWDeI+uS0KEwg3rJzud/X9s1BFP0dBem4cLGFNZGq9h3EWI7mRm1ZWRf0foNqeFFvJSCRWMR0j89ktvmxbwFsve+T7rIlRrneoC7Cf3oOfnY/M+VpMopymsZ8GA0ALyxYXfgSgv+a0VPNAtF2HEBNtiDVcYT3Z7brHm3/dBCbFrRaj1utPyIJHiwmnwuHq1LqxX8CyzPCz5ydRhIF9fxiyRZRuXxn1/l488tzO3cDxrD14hoDkTxly9h27q26i09+I1gBpAstO6fW0J3e8aQ4gnwh4CMM915812eCs/tafWXuPgtVeOIZqmi/2JoBr3k4bsKfMIwtLUe41cI3XSUJ/b4KKFPiturjIzaH0lKkcpZTEDpYaep7ajVmyj2Ouqml8O+7he5TYgO/+PQMjaNfkmyWm728zgSyw1SHL9Qh8dF4xQB017KrFiBtmjGktJVt81WCWWOElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwVkLtsdQOR9VaAtpKid/LEnNdUawT8mhk3agGdYjfbmm/f1o6TMEpu38kOfTKofURQ1g9rryUint12IWrh/l+Bg==" + } + ] + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "756946402DD50EDB56F7A4DF2F47815CE2859959786D798BD0FFCB309BA27CDC", + "noteCommitment": { + "type": "Buffer", + "data": "base64:OdaulLjoHgTbAhe42btS5ELZkvVvRQP3CrUTG4EuKQc=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:tggL+jRDMjS+pgpeAop6/lM4clRKLQykBzGBT4wWhkU=" + }, + "target": "880842937844725196442695540779332307793253899902937591585455087694081134", + "randomness": "0", + "timestamp": 1678476132906, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 5, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAA8Pz7j3WBTt/nJTq+xhC6ZyRm/UD4/whYQ9yxbCZ7meKyGT97HREVQVcJRgrTw1uwYOTSyevTHCEmmfhyUXg/gVqJzP0D2VuBioUJNH0ZDmSkcmUU0wpKAH6RgYN0Hhucl4DaOKGa7JPmmFsN+2jGK1p4ciPPP+y5YkABpro9bWwJnjGm2vtsRDhJFgxOVr3sz5C98nLPyIWvDI/HedUqlr7MS8EABmdwLBtz1b82KnSSqZgP8MRIx8/p95TYHIvb2vrf0ffjo1skFCulWExX6/MK4bTk/hti8suj//5t4ZP3ubRuR082XLQ9oUb8nby7nWfiu2vUzvztf+Srtw6NLm0xH8d5N8cB1Cl7ZsyW3PWjiKIhg+4+CMZo6q7npbEZEGq6jPUiWJo6aLQp3//rD/pEOIpTOv5pHqB+Q4XzNbqRipfBE9FENR8e9UyXB1TqV5msV7GY1r28P7t8oRNy6keCN+QuGConKlDCP06l7tZyspqwwbEfKMlhHelEWQUXlYQpvrsgG2MrMhmJt96EDjLHt2w0ZPim3B8J+tUM27e8dBZDGOD2ET1ZgbmYWWWbSg7SNHzCgCK3j/4r+3pOsKJ6jputtkRQRTWAcz71kmOYpZ1APx4F9Ulyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwEd/3XFFlJuYvhMyjz82A7A7gLGHZTz+3B/hJzv87NkOO/AaEW648bnsUYleqiYWedcey6glmhSnbtDW/PidICA==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + }, + { + "type": "Buffer", + "data": "base64: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" + }, + { + "header": { + "sequence": 4, + "previousBlockHash": "0EB2CC670A5DA3EBFADD8509EFFE4EE59F462AC45B34E1CA9D995DE64D9EC498", + "noteCommitment": { + "type": "Buffer", + "data": "base64:PqFD4C1ed5BTvBplirr+HEOO2vLNZX7BlvrXQ1e6zC8=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:NXBQbX4dIKTu4xeDi5Ck1dGBnLqD+Onf/WAu2Go4cMU=" + }, + "target": "878703931196243590817531151413670986016194031277626912635514691657912894", + "randomness": "0", + "timestamp": 1678476137949, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 10, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAS3aSHpW1XjEM0ApjHjrQsDJ4k8KWdfmEMOPLg/68R0Kio3p3xBxj4FQFFEBrTP+Zv14NsaPefekg5uecjOxVoVGKj4vbrFpLbGYW5AGdGluifKCilx9i+sLOUDeaA8/wENHa5sWXQaB5LeUk4eJjUl6L6dX9WHSLLakv5zdgPTsNl/FhnuVVsCBsesE1ob1WYpcF4ifpVTp2kX+lhekiTOmaza2W4g0DCiYyGD2bXyGvpnS9XENnN3HGR4dWA89x/e/HYaHDnGpRr8mlDGTvafwfG7IF1qte9gJj7tQ+Gv0zhMTvxwFhbj+jfqK9Jp2aPk26U3z9A+dk/XiBeXCvjwJjZ682c6f5tA+vCHvR1MbBDDLNvF/WZIni1UsVK/NQwirnYy1oPicBK4p/NhL5flIxPMPftZFTSrnuVHj6XoT/dAhZtIz2DYc3OC04ptzRaHtAxFc72Imw1zdDQDBMyDRwtjgpn9jsAkZHLqsOGKdOkEBOGcKsQ5qvIrGSZS/iTa4DgI9j9isHeRr0n3TqPMO+JkOGzdEdgEXLjE7bgbHzCUtHLN9t2j2CfU4S8mPD4lh4zEiNVK0EqZ8B9h0IntN+lZSmIGX8U0kKiTwvfmKRjX4x7S43DUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw3NUS/+ovvuofhbG+zkEu0wYmFwh7LjkLny0GG6Cam5VsU7NjCw7m0yZ5uNqaYnLpiObj/YAGGN92h5BucGmYCQ==" + }, + { + "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 d2bf541b7f..3368fa2cab 100644 --- a/ironfish/src/wallet/account.test.ts +++ b/ironfish/src/wallet/account.test.ts @@ -2006,4 +2006,61 @@ describe('Accounts', () => { expect(unspentNotes).toHaveLength(0) }) }) + + describe('getTransactionsByTime', () => { + it('loads multiple transactions on a block for an account', async () => { + const { node: nodeA } = await nodeTest.createSetup() + const { node: nodeB } = await nodeTest.createSetup() + + // create accounts on separate nodes so that accountB doesn't see pending transactions + const accountA = await useAccountFixture(nodeA.wallet, 'accountA') + const accountB = await useAccountFixture(nodeB.wallet, 'accountB') + + // mine two blocks to give accountA notes for two transactions + const block2 = await useMinerBlockFixture(nodeA.chain, 2, accountA) + await nodeA.chain.addBlock(block2) + await nodeB.chain.addBlock(block2) + + const block3 = await useMinerBlockFixture(nodeA.chain, 3, accountA) + await nodeA.chain.addBlock(block3) + await nodeB.chain.addBlock(block3) + await nodeA.wallet.updateHead() + await nodeB.wallet.updateHead() + + // create two transactions from A to B + const tx1 = await useTxFixture(nodeA.wallet, accountA, accountB) + const tx2 = await useTxFixture(nodeA.wallet, accountA, accountB) + + // mine a block that includes both transactions + const block4 = await useMinerBlockFixture(nodeA.chain, 4, accountA, undefined, [tx1, tx2]) + await nodeA.chain.addBlock(block4) + await nodeB.chain.addBlock(block4) + await nodeA.wallet.updateHead() + await nodeB.wallet.updateHead() + + // getTransactionsByTime returns transactions in reverse order by time, hash + const accountATx = await AsyncUtils.materialize(accountA.getTransactionsByTime()) + const accountBTx = await AsyncUtils.materialize(accountB.getTransactionsByTime()) + + // 3 block rewards plus 2 outgoing transactions + expect(accountATx).toHaveLength(5) + + const accountATxHashes = accountATx.map((tx) => tx.transaction.hash().toString('hex')) + + expect(accountATxHashes).toContain(tx2.hash().toString('hex')) + expect(accountATxHashes).toContain(tx1.hash().toString('hex')) + expect(accountATxHashes).toContain(block4.transactions[0].hash().toString('hex')) + expect(accountATxHashes).toContain(block3.transactions[0].hash().toString('hex')) + expect(accountATxHashes).toContain(block2.transactions[0].hash().toString('hex')) + + // 2 transactions from block4 + expect(accountBTx).toHaveLength(2) + + // tx1 and tx2 will have the same timestamp for accountB, so ordering should be reverse by hash + const sortedHashes = [tx1.hash(), tx2.hash()].sort().reverse() + + expect(accountBTx[0].transaction.hash()).toEqualHash(sortedHashes[0]) + expect(accountBTx[1].transaction.hash()).toEqualHash(sortedHashes[1]) + }) + }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 524613a21b..9e5f2cd516 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -35,7 +35,7 @@ import { HeadValue, NullableHeadValueEncoding } from './headValue' import { AccountsDBMeta, MetaValue, MetaValueEncoding } from './metaValue' import { TransactionValue, TransactionValueEncoding } from './transactionValue' -const VERSION_DATABASE_ACCOUNTS = 25 +const VERSION_DATABASE_ACCOUNTS = 26 const getAccountsDBMetaDefaults = (): AccountsDBMeta => ({ defaultAccountId: null, @@ -105,8 +105,8 @@ export class WalletDB { }> timestampToTransactionHash: IDatabaseStore<{ - key: [Account['prefix'], number] - value: TransactionHash + key: [Account['prefix'], [number, TransactionHash]] + value: null }> assets: IDatabaseStore<{ @@ -226,9 +226,13 @@ export class WalletDB { }) this.timestampToTransactionHash = this.db.addStore({ - name: 'T', - keyEncoding: new PrefixEncoding(new BufferEncoding(), U64_ENCODING, 4), - valueEncoding: new BufferEncoding(), + name: 'TT', + keyEncoding: new PrefixEncoding( + new BufferEncoding(), + new PrefixEncoding(U64_ENCODING, new BufferEncoding(), 8), + 4, + ), + valueEncoding: NULL_ENCODING, }) this.assets = this.db.addStore({ @@ -390,8 +394,8 @@ export class WalletDB { await this.transactions.put([account.prefix, transactionHash], transactionValue, tx) await this.timestampToTransactionHash.put( - [account.prefix, transactionValue.timestamp.getTime()], - transactionHash, + [account.prefix, [transactionValue.timestamp.getTime(), transactionHash]], + null, tx, ) }) @@ -406,7 +410,7 @@ export class WalletDB { Assert.isNotUndefined(transaction) await this.timestampToTransactionHash.del( - [account.prefix, transaction.timestamp.getTime()], + [account.prefix, [transaction.timestamp.getTime(), transactionHash]], tx, ) await this.transactions.del([account.prefix, transactionHash], tx) @@ -1088,10 +1092,13 @@ export class WalletDB { account: Account, tx?: IDatabaseTransaction, ): AsyncGenerator { - for await (const transactionHash of this.timestampToTransactionHash.getAllValuesIter( + for await (const [, [, transactionHash]] of this.timestampToTransactionHash.getAllKeysIter( tx, account.prefixRange, - { ordered: true, reverse: true }, + { + ordered: true, + reverse: true, + }, )) { const transaction = await this.loadTransaction(account, transactionHash, tx) Assert.isNotUndefined(transaction) From 429afb522f665f456e2a9f663f26196db6beefe1 Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Fri, 10 Mar 2023 14:55:23 -0800 Subject: [PATCH 12/12] bump to version 0.1.72 (#3639) --- ironfish-cli/package.json | 6 +++--- ironfish-rust-nodejs/npm/darwin-arm64/package.json | 2 +- ironfish-rust-nodejs/npm/darwin-x64/package.json | 2 +- ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json | 2 +- ironfish-rust-nodejs/npm/linux-arm64-musl/package.json | 2 +- ironfish-rust-nodejs/npm/linux-x64-gnu/package.json | 2 +- ironfish-rust-nodejs/npm/linux-x64-musl/package.json | 2 +- ironfish-rust-nodejs/npm/win32-x64-msvc/package.json | 2 +- ironfish-rust-nodejs/package.json | 2 +- ironfish/package.json | 4 ++-- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 3a5acdec7c..825643bb62 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "0.1.71", + "version": "0.1.72", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -59,8 +59,8 @@ "@aws-sdk/client-s3": "3.127.0", "@aws-sdk/client-secrets-manager": "3.276.0", "@aws-sdk/s3-request-presigner": "3.127.0", - "@ironfish/rust-nodejs": "0.1.28", - "@ironfish/sdk": "0.0.48", + "@ironfish/rust-nodejs": "0.1.29", + "@ironfish/sdk": "0.0.49", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", diff --git a/ironfish-rust-nodejs/npm/darwin-arm64/package.json b/ironfish-rust-nodejs/npm/darwin-arm64/package.json index b7030234ec..1c1af51eec 100644 --- a/ironfish-rust-nodejs/npm/darwin-arm64/package.json +++ b/ironfish-rust-nodejs/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-darwin-arm64", - "version": "0.1.28", + "version": "0.1.29", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/darwin-x64/package.json b/ironfish-rust-nodejs/npm/darwin-x64/package.json index 38072d054e..a0ac3ff5b9 100644 --- a/ironfish-rust-nodejs/npm/darwin-x64/package.json +++ b/ironfish-rust-nodejs/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-darwin-x64", - "version": "0.1.28", + "version": "0.1.29", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json b/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json index 3ca0e222d7..7a4ceb2c75 100644 --- a/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json +++ b/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-arm64-gnu", - "version": "0.1.28", + "version": "0.1.29", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json b/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json index b11a7cedc5..ea6b35a9f3 100644 --- a/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json +++ b/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-arm64-musl", - "version": "0.1.28", + "version": "0.1.29", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json b/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json index 0c5df104c1..bb988ba4e5 100644 --- a/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json +++ b/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-x64-gnu", - "version": "0.1.28", + "version": "0.1.29", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-x64-musl/package.json b/ironfish-rust-nodejs/npm/linux-x64-musl/package.json index d6928b7b73..3336ba5e56 100644 --- a/ironfish-rust-nodejs/npm/linux-x64-musl/package.json +++ b/ironfish-rust-nodejs/npm/linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-x64-musl", - "version": "0.1.28", + "version": "0.1.29", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json b/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json index 7550f32210..f523f770a3 100644 --- a/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json +++ b/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-win32-x64-msvc", - "version": "0.1.28", + "version": "0.1.29", "os": [ "win32" ], diff --git a/ironfish-rust-nodejs/package.json b/ironfish-rust-nodejs/package.json index aaca294e4f..95c6e92571 100644 --- a/ironfish-rust-nodejs/package.json +++ b/ironfish-rust-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs", - "version": "0.1.28", + "version": "0.1.29", "description": "Node.js bindings for Rust code required by the Iron Fish SDK", "main": "index.js", "types": "index.d.ts", diff --git a/ironfish/package.json b/ironfish/package.json index 25618c5a9d..b4b1bc7674 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "0.0.48", + "version": "0.0.49", "description": "SDK for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -18,7 +18,7 @@ ], "dependencies": { "@ethersproject/bignumber": "5.7.0", - "@ironfish/rust-nodejs": "0.1.28", + "@ironfish/rust-nodejs": "0.1.29", "@napi-rs/blake-hash": "1.3.3", "axios": "0.21.4", "bech32": "2.0.0",