diff --git a/.github/workflows/build-ironfish-rust-nodejs.yml b/.github/workflows/build-ironfish-rust-nodejs.yml index c7fe2409d5..46f9e33d24 100644 --- a/.github/workflows/build-ironfish-rust-nodejs.yml +++ b/.github/workflows/build-ironfish-rust-nodejs.yml @@ -46,10 +46,10 @@ jobs: ${{ contains(matrix.settings.host, 'windows-') && 'Get-WmiObject -Class Win32_Processor -ComputerName.' || '' }} - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: yarn @@ -112,14 +112,14 @@ jobs: runs-on: ${{ matrix.settings.host }} steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU for Docker if: ${{ matrix.settings.docker }} run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/ci-regenerate-fixtures.yml b/.github/workflows/ci-regenerate-fixtures.yml index 2b089b86a0..5ce5444ca6 100644 --- a/.github/workflows/ci-regenerate-fixtures.yml +++ b/.github/workflows/ci-regenerate-fixtures.yml @@ -11,12 +11,12 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: staging - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 062ab904a7..b742a8a6be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,10 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' @@ -45,10 +45,10 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: # Tests will only run on Node v20 due to https://github.com/nodejs/node/issues/35889 node-version: 20 @@ -85,7 +85,7 @@ jobs: uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: # Tests will only run on Node v20 due to https://github.com/nodejs/node/issues/35889 node-version: 20 diff --git a/.github/workflows/deploy-brew.yml b/.github/workflows/deploy-brew.yml index 3c6b1797c7..9c77db05f5 100644 --- a/.github/workflows/deploy-brew.yml +++ b/.github/workflows/deploy-brew.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ inputs.refToBuild }} @@ -22,7 +22,7 @@ jobs: shared-key: nodejs - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' diff --git a/.github/workflows/deploy-node-docker-image.yml b/.github/workflows/deploy-node-docker-image.yml index 66a238bde0..673638a571 100644 --- a/.github/workflows/deploy-node-docker-image.yml +++ b/.github/workflows/deploy-node-docker-image.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Login to GitHub Registry run: echo ${GITHUB_TOKEN} | docker login -u ${GITHUB_USER} --password-stdin ghcr.io diff --git a/.github/workflows/deploy-npm-ironfish-cli.yml b/.github/workflows/deploy-npm-ironfish-cli.yml index acf8c18696..9de31cb06c 100644 --- a/.github/workflows/deploy-npm-ironfish-cli.yml +++ b/.github/workflows/deploy-npm-ironfish-cli.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/deploy-npm-ironfish-rust-nodejs.yml b/.github/workflows/deploy-npm-ironfish-rust-nodejs.yml index f8138ce2fd..652fe6a8ac 100644 --- a/.github/workflows/deploy-npm-ironfish-rust-nodejs.yml +++ b/.github/workflows/deploy-npm-ironfish-rust-nodejs.yml @@ -21,10 +21,10 @@ jobs: working-directory: ./ironfish-rust-nodejs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/deploy-npm-ironfish.yml b/.github/workflows/deploy-npm-ironfish.yml index 204a0746f0..d3bfa98fd4 100644 --- a/.github/workflows/deploy-npm-ironfish.yml +++ b/.github/workflows/deploy-npm-ironfish.yml @@ -8,10 +8,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/perf_test.yml b/.github/workflows/perf_test.yml index c3fcbf6325..bf937dc1f3 100644 --- a/.github/workflows/perf_test.yml +++ b/.github/workflows/perf_test.yml @@ -17,12 +17,12 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' diff --git a/.github/workflows/publish-binaries.yml b/.github/workflows/publish-binaries.yml index e0afe6dca5..fdb9e5e052 100644 --- a/.github/workflows/publish-binaries.yml +++ b/.github/workflows/publish-binaries.yml @@ -47,7 +47,7 @@ jobs: find . -name . -o -prune -exec rm -rf -- {} + - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/push-version-to-api.yml b/.github/workflows/push-version-to-api.yml index 6103823326..1c01249c35 100644 --- a/.github/workflows/push-version-to-api.yml +++ b/.github/workflows/push-version-to-api.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Push version string to mainnet API if: ${{ inputs.push_mainnet }} diff --git a/.github/workflows/rust_ci.yml b/.github/workflows/rust_ci.yml index be00a825f5..12a4057edd 100644 --- a/.github/workflows/rust_ci.yml +++ b/.github/workflows/rust_ci.yml @@ -33,7 +33,7 @@ jobs: name: Lint Rust runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache Rust uses: Swatinem/rust-cache@v2 @@ -68,7 +68,7 @@ jobs: name: Test ironfish-rust runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache Rust uses: Swatinem/rust-cache@v2 @@ -94,7 +94,7 @@ jobs: name: Test ironfish-zkp runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache Rust uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/rust_ci_cache.yml b/.github/workflows/rust_ci_cache.yml index 3a0ba33f8f..792183f62a 100644 --- a/.github/workflows/rust_ci_cache.yml +++ b/.github/workflows/rust_ci_cache.yml @@ -21,7 +21,7 @@ jobs: name: Build and cache rust code runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache Rust uses: Swatinem/rust-cache@v2 diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 5a5e6e0f2a..e23c4a4e32 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "1.13.0", + "version": "1.14.0", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -62,8 +62,8 @@ "@aws-sdk/client-s3": "3", "@aws-sdk/client-secrets-manager": "3", "@aws-sdk/s3-request-presigner": "3", - "@ironfish/rust-nodejs": "1.11.0", - "@ironfish/sdk": "1.13.0", + "@ironfish/rust-nodejs": "1.12.0", + "@ironfish/sdk": "1.14.0", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", diff --git a/ironfish-cli/src/commands/peers/add.ts b/ironfish-cli/src/commands/peers/add.ts index f0c18e2936..7636b5fef6 100644 --- a/ironfish-cli/src/commands/peers/add.ts +++ b/ironfish-cli/src/commands/peers/add.ts @@ -53,6 +53,11 @@ export class AddCommand extends IronfishCommand { if (response.content.added) { this.log(`Successfully added peer ${request.host}:${request.port}`) + } else if (response.content.error !== undefined) { + this.log( + `Failed to add peer ${request.host}:${request.port} because: ${response.content.error}`, + ) + this.exit(0) } else { this.log(`Could not add peer ${request.host}:${request.port}`) this.exit(0) diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index dac7e0b178..4d42f92ac1 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -40,6 +40,13 @@ export class ImportCommand extends IronfishCommand { const client = await this.sdk.connectRpc() let account: string + + if (blob && blob.length !== 0 && flags.path && flags.path.length !== 0) { + this.error( + `Your command includes an unexpected argument. Please pass either --path or the output of wallet:export.`, + ) + } + if (blob) { account = blob } else if (flags.path) { diff --git a/ironfish-cli/src/commands/wallet/notes.ts b/ironfish-cli/src/commands/wallet/notes/index.ts similarity index 93% rename from ironfish-cli/src/commands/wallet/notes.ts rename to ironfish-cli/src/commands/wallet/notes/index.ts index 67b435cac2..1962dac0af 100644 --- a/ironfish-cli/src/commands/wallet/notes.ts +++ b/ironfish-cli/src/commands/wallet/notes/index.ts @@ -3,9 +3,9 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { CurrencyUtils } from '@ironfish/sdk' import { CliUx } from '@oclif/core' -import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' -import { TableCols } from '../../utils/table' +import { IronfishCommand } from '../../../command' +import { RemoteFlags } from '../../../flags' +import { TableCols } from '../../../utils/table' const { sort: _, ...tableFlags } = CliUx.ux.table.flags() export class NotesCommand extends IronfishCommand { diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index 8877381fce..4c9754646b 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -97,6 +97,33 @@ export class Send extends IronfishCommand { }), } + renderTransactionSummary( + transaction: RawTransaction, + assetId: string, + amount: bigint, + from: string, + to: string, + memo: string, + ): void { + const amountString = CurrencyUtils.renderIron(amount, true, assetId) + const feeString = CurrencyUtils.renderIron(transaction.fee, true) + + const summary = `\ +\nTRANSACTION DETAILS: +From ${from} +To ${to} +Amount ${amountString} +Fee ${feeString} +Memo ${memo} +Outputs ${transaction.outputs.length} +Spends ${transaction.spends.length} +Expiration ${transaction.expiration ? transaction.expiration.toString() : ''} +Version ${transaction.version} +` + + this.log(summary) + } + async start(): Promise { const { flags } = await this.parse(Send) let amount = flags.amount @@ -219,8 +246,13 @@ export class Send extends IronfishCommand { this.exit(0) } - if (!flags.confirm && !(await this.confirm(assetId, amount, raw.fee, from, to, memo))) { - this.error('Transaction aborted.') + this.renderTransactionSummary(raw, assetId, amount, from, to, memo) + + if (!flags.confirm) { + const confirmed = await CliUx.ux.confirm('Do you confirm (Y/N)?') + if (!confirmed) { + this.error('Transaction aborted.') + } } CliUx.ux.action.start('Sending the transaction') @@ -266,26 +298,4 @@ export class Send extends IronfishCommand { }) } } - - async confirm( - assetId: string, - amount: bigint, - fee: bigint, - from: string, - to: string, - memo: string, - ): Promise { - this.log( - `You are about to send a transaction: ${CurrencyUtils.renderIron( - amount, - true, - assetId, - )} plus a transaction fee of ${CurrencyUtils.renderIron( - fee, - true, - )} to ${to} from the account "${from}" with the memo "${memo}"`, - ) - - return await CliUx.ux.confirm('Do you confirm (Y/N)?') - } } diff --git a/ironfish-cli/src/commands/wallet/status.ts b/ironfish-cli/src/commands/wallet/status.ts index cfae466c11..586c57924f 100644 --- a/ironfish-cli/src/commands/wallet/status.ts +++ b/ironfish-cli/src/commands/wallet/status.ts @@ -6,7 +6,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' export class StatusCommand extends IronfishCommand { - static description = `Get status of an account` + static description = `Get status of all accounts` static flags = { ...RemoteFlags, @@ -14,14 +14,11 @@ export class StatusCommand extends IronfishCommand { } async start(): Promise { - const { args, flags } = await this.parse(StatusCommand) - const account = args.account as string | undefined + const { flags } = await this.parse(StatusCommand) const client = await this.sdk.connectRpc() - const response = await client.wallet.getAccountsStatus({ - account: account, - }) + const response = await client.wallet.getAccountsStatus() CliUx.ux.table( response.content.accounts, @@ -33,13 +30,19 @@ export class StatusCommand extends IronfishCommand { id: { header: 'Account ID', }, + viewOnly: { + header: 'View Only', + }, headHash: { + get: (row) => row.head?.hash ?? 'NULL', header: 'Head Hash', }, headInChain: { + get: (row) => row.head?.inChain ?? 'NULL', header: 'Head In Chain', }, sequence: { + get: (row) => row.head?.sequence ?? 'NULL', header: 'Head Sequence', }, }, diff --git a/ironfish-cli/src/snapshot.ts b/ironfish-cli/src/snapshot.ts index b71fd2d2f5..561503ef31 100644 --- a/ironfish-cli/src/snapshot.ts +++ b/ironfish-cli/src/snapshot.ts @@ -123,11 +123,11 @@ export class SnapshotDownloader { } const idleTimeout = 30000 - let idleLastChunk = Date.now() + let idleLastChunk = performance.now() const idleCancelSource = axios.CancelToken.source() const idleInterval = setInterval(() => { - const timeSinceLastChunk = Date.now() - idleLastChunk + const timeSinceLastChunk = performance.now() - idleLastChunk if (timeSinceLastChunk > idleTimeout) { clearInterval(idleInterval) @@ -186,7 +186,7 @@ export class SnapshotDownloader { onDownloadProgress(downloaded, downloaded + chunk.length) downloaded += chunk.length - idleLastChunk = Date.now() + idleLastChunk = performance.now() }) }) .catch((error) => { diff --git a/ironfish-rust-nodejs/npm/darwin-arm64/package.json b/ironfish-rust-nodejs/npm/darwin-arm64/package.json index 14e66760d7..c0ca2ee349 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": "1.11.0", + "version": "1.12.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/darwin-x64/package.json b/ironfish-rust-nodejs/npm/darwin-x64/package.json index da58f558ae..f5cdfaf504 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": "1.11.0", + "version": "1.12.0", "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 e71c3c13d9..7e6f311cca 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": "1.11.0", + "version": "1.12.0", "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 c4d9ca989e..0e15bd9e83 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": "1.11.0", + "version": "1.12.0", "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 2d6a6d3a20..b8e6939e76 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": "1.11.0", + "version": "1.12.0", "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 fd40bb84d6..f11f006da1 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": "1.11.0", + "version": "1.12.0", "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 6ab1b0ca93..d5a5a4d76f 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": "1.11.0", + "version": "1.12.0", "os": [ "win32" ], diff --git a/ironfish-rust-nodejs/package.json b/ironfish-rust-nodejs/package.json index b3c2a867fe..e7a0e011b1 100644 --- a/ironfish-rust-nodejs/package.json +++ b/ironfish-rust-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs", - "version": "1.11.0", + "version": "1.12.0", "description": "Node.js bindings for Rust code required by the Iron Fish SDK", "main": "index.js", "types": "index.d.ts", diff --git a/ironfish-rust/src/keys/public_address.rs b/ironfish-rust/src/keys/public_address.rs index 2b28fcfdd4..d7af609462 100644 --- a/ironfish-rust/src/keys/public_address.rs +++ b/ironfish-rust/src/keys/public_address.rs @@ -10,7 +10,7 @@ use group::GroupEncoding; use ironfish_zkp::constants::PUBLIC_KEY_GENERATOR; use jubjub::SubgroupPoint; -use std::{convert::TryInto, io}; +use std::io; use super::{IncomingViewKey, SaplingKey}; pub const PUBLIC_ADDRESS_SIZE: usize = 32; @@ -19,19 +19,21 @@ pub const PUBLIC_ADDRESS_SIZE: usize = 32; /// transmission key. Using the incoming_viewing_key allows /// the creation of a unique public addresses without revealing the viewing key. #[derive(Clone, Copy)] -pub struct PublicAddress { - /// The transmission key is the result of combining the diversifier with the - /// incoming viewing key (a non-reversible operation). Together, the two - /// form a public address to which payments can be sent. - pub(crate) transmission_key: SubgroupPoint, -} +pub struct PublicAddress(pub(crate) SubgroupPoint); impl PublicAddress { /// Initialize a public address from its 32 byte representation. - pub fn new(address_bytes: &[u8; PUBLIC_ADDRESS_SIZE]) -> Result { - let transmission_key = PublicAddress::load_transmission_key(&address_bytes[0..])?; - - Ok(PublicAddress { transmission_key }) + pub fn new( + public_address_bytes: &[u8; PUBLIC_ADDRESS_SIZE], + ) -> Result { + assert!(public_address_bytes.len() == 32); + let public_address_non_prime = SubgroupPoint::from_bytes(public_address_bytes); + + if public_address_non_prime.is_some().into() { + Ok(PublicAddress(public_address_non_prime.unwrap())) + } else { + Err(IronfishError::new(IronfishErrorKind::InvalidPaymentAddress)) + } } /// Load a public address from a Read implementation (e.g: socket, file) @@ -48,9 +50,7 @@ impl PublicAddress { } pub fn from_view_key(view_key: &IncomingViewKey) -> PublicAddress { - PublicAddress { - transmission_key: *PUBLIC_KEY_GENERATOR * view_key.view_key, - } + PublicAddress(*PUBLIC_KEY_GENERATOR * view_key.view_key) } /// Convert a String of hex values to a PublicAddress. The String must @@ -65,7 +65,7 @@ impl PublicAddress { /// Retrieve the public address in byte form. pub fn public_address(&self) -> [u8; PUBLIC_ADDRESS_SIZE] { - self.transmission_key.to_bytes() + self.0.to_bytes() } /// Retrieve the public address in hex form. @@ -79,20 +79,6 @@ impl PublicAddress { Ok(()) } - - pub(crate) fn load_transmission_key( - transmission_key_bytes: &[u8], - ) -> Result { - assert!(transmission_key_bytes.len() == 32); - let transmission_key_non_prime = - SubgroupPoint::from_bytes(transmission_key_bytes.try_into().unwrap()); - - if transmission_key_non_prime.is_some().into() { - Ok(transmission_key_non_prime.unwrap()) - } else { - Err(IronfishError::new(IronfishErrorKind::InvalidPaymentAddress)) - } - } } impl std::fmt::Debug for PublicAddress { @@ -109,7 +95,10 @@ impl std::cmp::PartialEq for PublicAddress { #[cfg(test)] mod test { - use crate::{keys::PUBLIC_ADDRESS_SIZE, PublicAddress, SaplingKey}; + use crate::{ + keys::{PublicAddress, PUBLIC_ADDRESS_SIZE}, + SaplingKey, + }; #[test] fn public_address_validation() { diff --git a/ironfish-rust/src/keys/test.rs b/ironfish-rust/src/keys/test.rs index f49b4211ad..997785307a 100644 --- a/ironfish-rust/src/keys/test.rs +++ b/ironfish-rust/src/keys/test.rs @@ -27,7 +27,7 @@ fn test_diffie_hellman_shared_key() { let secret_key = key_pair.secret(); let public_key = key_pair.public(); - let shared_secret1 = shared_secret(secret_key, &address1.transmission_key, public_key); + let shared_secret1 = shared_secret(secret_key, &address1.0, public_key); let shared_secret2 = shared_secret(&key1.incoming_viewing_key.view_key, public_key, public_key); assert_eq!(shared_secret1, shared_secret2); } @@ -44,15 +44,11 @@ fn test_diffie_hellman_shared_key_with_other_key() { let secret_key = key_pair.secret(); let public_key = key_pair.public(); - let shared_secret1 = shared_secret(secret_key, &address.transmission_key, public_key); + let shared_secret1 = shared_secret(secret_key, &address.0, public_key); let shared_secret2 = shared_secret(&key.incoming_viewing_key.view_key, public_key, public_key); assert_eq!(shared_secret1, shared_secret2); - let shared_secret_third_party1 = shared_secret( - secret_key, - &third_party_address.transmission_key, - public_key, - ); + let shared_secret_third_party1 = shared_secret(secret_key, &third_party_address.0, public_key); assert_ne!(shared_secret1, shared_secret_third_party1); assert_ne!(shared_secret2, shared_secret_third_party1); @@ -90,8 +86,8 @@ fn test_serialization() { .expect("Should be able to construct address from valid bytes"); assert_eq!( - ExtendedPoint::from(read_back_address.transmission_key).to_affine(), - ExtendedPoint::from(public_address.transmission_key).to_affine() + ExtendedPoint::from(read_back_address.0).to_affine(), + ExtendedPoint::from(public_address.0).to_affine() ) } diff --git a/ironfish-rust/src/merkle_note.rs b/ironfish-rust/src/merkle_note.rs index c28a0cdd88..9b874d76a6 100644 --- a/ironfish-rust/src/merkle_note.rs +++ b/ironfish-rust/src/merkle_note.rs @@ -82,7 +82,7 @@ impl MerkleNote { let public_key = diffie_hellman_keys.public(); let mut key_bytes = [0; 64]; - key_bytes[..32].copy_from_slice(¬e.owner.transmission_key.to_bytes()); + key_bytes[..32].copy_from_slice(¬e.owner.0.to_bytes()); key_bytes[32..].clone_from_slice(secret_key.to_repr().as_ref()); let encryption_key = calculate_key_for_encryption_keys( @@ -131,11 +131,7 @@ impl MerkleNote { let secret_key = diffie_hellman_keys.secret(); let public_key = diffie_hellman_keys.public(); - let encrypted_note = note.encrypt(&shared_secret( - secret_key, - ¬e.owner.transmission_key, - public_key, - )); + let encrypted_note = note.encrypt(&shared_secret(secret_key, ¬e.owner.0, public_key)); MerkleNote { value_commitment: value_commitment.commitment().into(), @@ -204,11 +200,11 @@ impl MerkleNote { let note_encryption_keys: [u8; ENCRYPTED_SHARED_KEY_SIZE] = aead::decrypt(&encryption_key, &self.note_encryption_keys)?; - let transmission_key = PublicAddress::load_transmission_key(¬e_encryption_keys[..32])?; + let public_address = PublicAddress::new(¬e_encryption_keys[..32].try_into().unwrap())?; let secret_key = read_scalar(¬e_encryption_keys[32..])?; - let shared_key = shared_secret(&secret_key, &transmission_key, &self.ephemeral_public_key); + let shared_key = shared_secret(&secret_key, &public_address.0, &self.ephemeral_public_key); let note = - Note::from_spender_encrypted(transmission_key, &shared_key, &self.encrypted_note)?; + Note::from_spender_encrypted(public_address.0, &shared_key, &self.encrypted_note)?; note.verify_commitment(self.note_commitment)?; Ok(note) } diff --git a/ironfish-rust/src/note.rs b/ironfish-rust/src/note.rs index 4468fd4402..a9e0f7cb11 100644 --- a/ironfish-rust/src/note.rs +++ b/ironfish-rust/src/note.rs @@ -201,14 +201,14 @@ impl<'a> Note { /// This function allows the owner to decrypt the note using the derived /// shared secret and their own view key. pub(crate) fn from_spender_encrypted( - transmission_key: SubgroupPoint, + public_address: SubgroupPoint, shared_secret: &[u8; 32], encrypted_bytes: &[u8; ENCRYPTED_NOTE_SIZE + aead::MAC_SIZE], ) -> Result { let (randomness, asset_id, value, memo, sender) = Note::decrypt_note_parts(shared_secret, encrypted_bytes)?; - let owner = PublicAddress { transmission_key }; + let owner = PublicAddress(public_address); Ok(Note { owner, @@ -278,9 +278,9 @@ impl<'a> Note { commitment_full_point( self.asset_generator(), self.value, - self.owner.transmission_key, + self.owner.0, self.randomness, - self.sender.transmission_key, + self.sender.0, ) } @@ -409,8 +409,7 @@ mod test { let dh_secret = diffie_hellman_keys.secret(); let dh_public = diffie_hellman_keys.public(); - let public_shared_secret = - shared_secret(dh_secret, &public_address.transmission_key, dh_public); + let public_shared_secret = shared_secret(dh_secret, &public_address.0, dh_public); let note = Note::new(public_address, 42, "", NATIVE_ASSET, sender_address); let encryption_result = note.encrypt(&public_shared_secret); @@ -434,12 +433,9 @@ mod test { note.sender.public_address() ); - let spender_decrypted = Note::from_spender_encrypted( - note.owner.transmission_key, - &public_shared_secret, - &encryption_result, - ) - .expect("Should be able to load from transmission key"); + let spender_decrypted = + Note::from_spender_encrypted(note.owner.0, &public_shared_secret, &encryption_result) + .expect("Should be able to load from transmission key"); assert!( spender_decrypted.owner.public_address().as_ref() == note.owner.public_address().as_ref() diff --git a/ironfish-rust/src/transaction/mints.rs b/ironfish-rust/src/transaction/mints.rs index a2c3bc5eac..7dc715ea18 100644 --- a/ironfish-rust/src/transaction/mints.rs +++ b/ironfish-rust/src/transaction/mints.rs @@ -127,6 +127,11 @@ impl UnsignedMintDescription { return Err(IronfishError::new(IronfishErrorKind::InvalidSigningKey)); } + // NOTE: The initial versions of the RedDSA specification and the redjubjub crate (that + // we're using here) require the public key bytes to be prefixed to the message. The latest + // version of the spec and the crate add the public key bytes automatically. Therefore, if + // in the future we upgrade to a newer version of redjubjub, `data_to_be_signed` will have + // to equal `signature_hash` let mut data_to_be_signed = [0; 64]; data_to_be_signed[..32].copy_from_slice(&randomized_public_key.0.to_bytes()); data_to_be_signed[32..].copy_from_slice(&signature_hash[..]); @@ -179,6 +184,12 @@ impl MintDescription { if randomized_public_key.0.is_small_order().into() { return Err(IronfishError::new(IronfishErrorKind::IsSmallOrder)); } + + // NOTE: The initial versions of the RedDSA specification and the redjubjub crate (that + // we're using here) require the public key bytes to be prefixed to the message. The latest + // version of the spec and the crate add the public key bytes automatically. Therefore, if + // in the future we upgrade to a newer version of redjubjub, `data_to_be_signed` will have + // to equal `signature_hash_value` let mut data_to_be_signed = [0; 64]; data_to_be_signed[..32].copy_from_slice(&randomized_public_key.0.to_bytes()); data_to_be_signed[32..].copy_from_slice(&signature_hash_value[..]); @@ -201,7 +212,7 @@ impl MintDescription { public_inputs[0] = randomized_public_key_point.get_u(); public_inputs[1] = randomized_public_key_point.get_v(); - let public_address_point = ExtendedPoint::from(self.owner.transmission_key).to_affine(); + let public_address_point = ExtendedPoint::from(self.owner.0).to_affine(); public_inputs[2] = public_address_point.get_u(); public_inputs[3] = public_address_point.get_v(); diff --git a/ironfish-rust/src/transaction/mod.rs b/ironfish-rust/src/transaction/mod.rs index 96aaf4638e..3cab77f43f 100644 --- a/ironfish-rust/src/transaction/mod.rs +++ b/ironfish-rust/src/transaction/mod.rs @@ -426,6 +426,11 @@ impl ProposedTransaction { public_key: &PublicKey, transaction_signature_hash: &[u8; 32], ) -> Result { + // NOTE: The initial versions of the RedDSA specification and the redjubjub crate (that + // we're using here) require the public key bytes to be prefixed to the message. The latest + // version of the spec and the crate add the public key bytes automatically. Therefore, if + // in the future we upgrade to a newer version of redjubjub, `data_to_be_signed` will have + // to equal `transaction_signature_hash` let mut data_to_be_signed = [0u8; TRANSACTION_SIGNATURE_SIZE]; data_to_be_signed[..TRANSACTION_PUBLIC_KEY_SIZE].copy_from_slice(&public_key.0.to_bytes()); data_to_be_signed[TRANSACTION_PUBLIC_KEY_SIZE..] diff --git a/ironfish-rust/src/transaction/outputs.rs b/ironfish-rust/src/transaction/outputs.rs index 7d7f6ebd55..b5c5fa5709 100644 --- a/ironfish-rust/src/transaction/outputs.rs +++ b/ironfish-rust/src/transaction/outputs.rs @@ -84,7 +84,7 @@ impl OutputBuilder { let circuit = Output { value_commitment: Some(self.value_commitment.clone()), - payment_address: Some(self.note.owner.transmission_key), + payment_address: Some(self.note.owner.0), commitment_randomness: Some(self.note.randomness), esk: Some(*diffie_hellman_keys.secret()), asset_id: *self.note.asset_id().as_bytes(), diff --git a/ironfish-rust/src/transaction/spends.rs b/ironfish-rust/src/transaction/spends.rs index 8fb281ee6c..3e646851c0 100644 --- a/ironfish-rust/src/transaction/spends.rs +++ b/ironfish-rust/src/transaction/spends.rs @@ -99,12 +99,12 @@ impl SpendBuilder { let circuit = Spend { value_commitment: Some(self.value_commitment.clone()), proof_generation_key: Some(spender_key.sapling_proof_generation_key()), - payment_address: Some(self.note.owner.transmission_key), + payment_address: Some(self.note.owner.0), auth_path: self.auth_path.clone(), commitment_randomness: Some(self.note.randomness), anchor: Some(self.root_hash), ar: Some(*public_key_randomness), - sender_address: Some(self.note.sender.transmission_key), + sender_address: Some(self.note.sender.0), }; // Proof that the spend was valid and successful for the provided owner @@ -174,6 +174,11 @@ impl UnsignedSpendDescription { return Err(IronfishError::new(IronfishErrorKind::InvalidSigningKey)); } + // NOTE: The initial versions of the RedDSA specification and the redjubjub crate (that + // we're using here) require the public key bytes to be prefixed to the message. The latest + // version of the spec and the crate add the public key bytes automatically. Therefore, if + // in the future we upgrade to a newer version of redjubjub, `data_to_be_signed` will have + // to equal `signature_hash` let mut data_to_be_signed = [0; 64]; data_to_be_signed[..TRANSACTION_PUBLIC_KEY_SIZE] .copy_from_slice(&transaction_randomized_public_key.0.to_bytes()); @@ -277,6 +282,12 @@ impl SpendDescription { if randomized_public_key.0.is_small_order().into() { return Err(IronfishError::new(IronfishErrorKind::IsSmallOrder)); } + + // NOTE: The initial versions of the RedDSA specification and the redjubjub crate (that + // we're using here) require the public key bytes to be prefixed to the message. The latest + // version of the spec and the crate add the public key bytes automatically. Therefore, if + // in the future we upgrade to a newer version of redjubjub, `data_to_be_signed` will have + // to equal `signature_hash_value` let mut data_to_be_signed = [0; 64]; data_to_be_signed[..32].copy_from_slice(&randomized_public_key.0.to_bytes()); data_to_be_signed[32..].copy_from_slice(&signature_hash_value[..]); diff --git a/ironfish/package.json b/ironfish/package.json index 1acaedb9f9..2554d4c537 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "1.13.0", + "version": "1.14.0", "description": "SDK for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -22,7 +22,7 @@ "dependencies": { "@ethersproject/bignumber": "5.7.0", "@fast-csv/format": "4.3.5", - "@ironfish/rust-nodejs": "1.11.0", + "@ironfish/rust-nodejs": "1.12.0", "@napi-rs/blake-hash": "1.3.3", "axios": "0.21.4", "bech32": "2.0.0", diff --git a/ironfish/src/fileStores/config.ts b/ironfish/src/fileStores/config.ts index 7177d082a9..2373d00f96 100644 --- a/ironfish/src/fileStores/config.ts +++ b/ironfish/src/fileStores/config.ts @@ -214,7 +214,7 @@ export type ConfigOptions = { /** * The discord webhook URL to post pool critical pool information to */ - poolDiscordWebhook: '' + poolDiscordWebhook: string /** * The maximum number of concurrent open connections per remote address. @@ -225,7 +225,7 @@ export type ConfigOptions = { /** * The lark webhook URL to post pool critical pool information to */ - poolLarkWebhook: '' + poolLarkWebhook: string /** * Whether we want the logs to the console to be in JSON format or not. This can be used to log to diff --git a/ironfish/src/network/peerNetwork.test.ts b/ironfish/src/network/peerNetwork.test.ts index 444db3f4c6..eb7324a6c9 100644 --- a/ironfish/src/network/peerNetwork.test.ts +++ b/ironfish/src/network/peerNetwork.test.ts @@ -685,10 +685,10 @@ describe('PeerNetwork', () => { await peerNetwork.peerManager.onMessage.emitAsync(peer, message) + getSizeSpy.mockRestore() + expect(sendSpy.mock.calls[0][0]).toBeInstanceOf(GetBlocksResponse) expectGetBlocksResponseToMatch(sendSpy.mock.calls[0][0] as GetBlocksResponse, response) - - getSizeSpy.mockRestore() }) it('should respect the lookup limit', async () => { diff --git a/ironfish/src/network/testUtilities/helpers.ts b/ironfish/src/network/testUtilities/helpers.ts index 95697764fd..2c2ca2df42 100644 --- a/ironfish/src/network/testUtilities/helpers.ts +++ b/ironfish/src/network/testUtilities/helpers.ts @@ -252,12 +252,5 @@ export function expectGetBlocksResponseToMatch( a: GetBlocksResponse, b: GetBlocksResponse, ): void { - expect(a.blocks.length).toEqual(b.blocks.length) - a.blocks.forEach((blockA, blockIndexA) => { - const blockB = b.blocks[blockIndexA] - - expect(blockA.equals(blockB)).toBe(true) - }) - - expect({ ...a, blocks: undefined }).toMatchObject({ ...b, blocks: undefined }) + expect(a.serialize().equals(b.serialize())).toBe(true) } diff --git a/ironfish/src/primitives/blockheader.test.ts b/ironfish/src/primitives/blockheader.test.ts index 7a8adcebf7..583a4bd0a0 100644 --- a/ironfish/src/primitives/blockheader.test.ts +++ b/ironfish/src/primitives/blockheader.test.ts @@ -129,12 +129,6 @@ describe('BlockHeader', () => { header2.noteCommitment = header1.noteCommitment expect(header1.equals(header2)).toBe(true) - // note size - header2.noteSize = 7 - expect(header1.equals(header2)).toBe(false) - header2.noteSize = header1.noteSize - expect(header1.equals(header2)).toBe(true) - // target header2.target = new Target(10) expect(header1.equals(header2)).toBe(false) diff --git a/ironfish/src/primitives/blockheader.ts b/ironfish/src/primitives/blockheader.ts index 92e00cc793..db8a616a5e 100644 --- a/ironfish/src/primitives/blockheader.ts +++ b/ironfish/src/primitives/blockheader.ts @@ -3,9 +3,9 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { blake3 } from '@napi-rs/blake-hash' +import bufio from 'bufio' import { Assert } from '../assert' import { BlockHashSerdeInstance, GraffitiSerdeInstance } from '../serde' -import PartialBlockHeaderSerde from '../serde/PartialHeaderSerde' import { BigIntUtils } from '../utils/bigint' import { NoteEncryptedHash, SerializedNoteEncryptedHash } from './noteEncrypted' import { Target } from './target' @@ -193,36 +193,14 @@ export class BlockHeader { this.hash = hash || this.recomputeHash() } - /** - * Construct a partial block header without the randomness and convert - * it to buffer. - * - * This is used for calculating the hash in miners and for verifying it. - */ - serializePartial(): Buffer { - return PartialBlockHeaderSerde.serialize({ - sequence: this.sequence, - previousBlockHash: this.previousBlockHash, - noteCommitment: this.noteCommitment, - transactionCommitment: this.transactionCommitment, - target: this.target, - timestamp: this.timestamp, - graffiti: this.graffiti, - }) - } - /** * Hash all the values in the block header to get a commitment to the entire * header and the global trees it models. */ recomputeHash(): BlockHash { - const partialHeader = this.serializePartial() + const header = this.serialize() - const headerBytes = Buffer.alloc(partialHeader.byteLength + 8) - headerBytes.set(BigIntUtils.writeBigU64BE(this.randomness)) - headerBytes.set(partialHeader, 8) - - const hash = hashBlockHeader(headerBytes) + const hash = hashBlockHeader(header) this.hash = hash return hash } @@ -239,11 +217,28 @@ export class BlockHeader { return Target.meets(BigIntUtils.fromBytesBE(this.recomputeHash()), this.target) } + /** + * Serialize the block header into a buffer for hashing and mining + */ + serialize(): Buffer { + const bw = bufio.write(180) + bw.writeBigU64BE(this.randomness) + bw.writeU32(this.sequence) + bw.writeHash(this.previousBlockHash) + bw.writeHash(this.noteCommitment) + bw.writeHash(this.transactionCommitment) + bw.writeBigU256BE(this.target.asBigInt()) + bw.writeU64(this.timestamp.getTime()) + bw.writeBytes(this.graffiti) + + return bw.render() + } + equals(other: BlockHeader): boolean { return ( this.noteSize === other.noteSize && this.work === other.work && - this.recomputeHash().equals(other.recomputeHash()) + this.serialize().equals(other.serialize()) ) } } diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index 6ed681b22a..7ce03e6860 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -33,8 +33,8 @@ import type { GetAccountNotesStreamResponse, GetAccountsRequest, GetAccountsResponse, - GetAccountStatusRequest, - GetAccountStatusResponse, + GetAccountsStatusRequest, + GetAccountsStatusResponse, GetAccountTransactionRequest, GetAccountTransactionResponse, GetAccountTransactionsRequest, @@ -132,6 +132,10 @@ import type { UseAccountResponse, } from '../routes' import { ApiNamespace } from '../routes/namespaces' +import { + GetAccountStatusRequest, + GetAccountStatusResponse, +} from '../routes/wallet/getAccountStatus' export abstract class RpcClient { abstract request( @@ -281,10 +285,19 @@ export abstract class RpcClient { ) }, - getAccountsStatus: ( + getAccountStatus: ( params: GetAccountStatusRequest, ): Promise> => { return this.request( + `${ApiNamespace.wallet}/getAccountStatus`, + params, + ).waitForEnd() + }, + + getAccountsStatus: ( + params: GetAccountsStatusRequest = {}, + ): Promise> => { + return this.request( `${ApiNamespace.wallet}/getAccountsStatus`, params, ).waitForEnd() diff --git a/ironfish/src/rpc/routes/peer/addPeer.test.ts b/ironfish/src/rpc/routes/peer/addPeer.test.ts index 695ce045f8..406916ed16 100644 --- a/ironfish/src/rpc/routes/peer/addPeer.test.ts +++ b/ironfish/src/rpc/routes/peer/addPeer.test.ts @@ -2,28 +2,128 @@ * 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 { formatWebSocketAddress } from '../../../network' +import { WebSocketConnection } from '../../../network/peers/connections' +import { mockIdentity } from '../../../network/testUtilities' import { createRouteTest } from '../../../testUtilities/routeTest' +jest.mock('ws') + describe('Route peer/addPeer', () => { const routeTest = createRouteTest() it('should add a peer with a correct address and port', async () => { const request = { host: 'testhost', port: 9037 } + const identity = mockIdentity('peer') + + const req = await routeTest.client.request('peer/addPeer', request).waitForRoute() + + const matchingPeers = routeTest.peerNetwork.peerManager.peers.filter( + (p) => formatWebSocketAddress(p.wsAddress) === 'ws://testhost:9037', + ) + + expect(matchingPeers.length).toBe(1) + expect(routeTest.peerNetwork.peerManager.peerCandidates.has(identity)).toBe(false) + + const peer = matchingPeers[0] + + let connection: WebSocketConnection + if (peer.state.type === 'CONNECTING' && peer.state.connections.webSocket) { + connection = peer.state.connections.webSocket + } else { + throw new Error('Peer should be CONNECTING with a WS connection') + } + + connection.setState({ + type: 'CONNECTED', + identity, + }) - const response = await routeTest.client.request('peer/addPeer', request).waitForEnd() + const response = await req.waitForEnd() + expect(response.content).toMatchObject({ + added: true, + }) expect( - routeTest.node.peerNetwork.peerManager.peerCandidates.has('ws://testhost:9037'), - ).toBe(true) + routeTest.peerNetwork.peerManager + .getConnectedPeers() + .filter((p) => p.state.identity === identity), + ).toHaveLength(1) + expect(routeTest.peerNetwork.peerManager.peerCandidates.has(identity)).toBe(true) + }) + + it('should return false if the peer closes without an error', async () => { + const request = { host: 'testhost', port: 9037 } + const identity = mockIdentity('peer') + + const req = await routeTest.client.request('peer/addPeer', request).waitForRoute() - const matchingPeers = routeTest.node.peerNetwork.peerManager.peers.filter( + const matchingPeers = routeTest.peerNetwork.peerManager.peers.filter( (p) => formatWebSocketAddress(p.wsAddress) === 'ws://testhost:9037', ) expect(matchingPeers.length).toBe(1) + expect(routeTest.peerNetwork.peerManager.peerCandidates.has(identity)).toBe(false) + + const peer = matchingPeers[0] + + let connection: WebSocketConnection + if (peer.state.type === 'CONNECTING' && peer.state.connections.webSocket) { + connection = peer.state.connections.webSocket + } else { + throw new Error('Peer should be CONNECTING with a WS connection') + } + + connection.close() + + const response = await req.waitForEnd() expect(response.content).toMatchObject({ - added: true, + added: false, + error: undefined, + }) + expect( + routeTest.peerNetwork.peerManager + .getConnectedPeers() + .filter((p) => p.state.identity === identity), + ).toHaveLength(0) + expect(routeTest.peerNetwork.peerManager.peerCandidates.has(identity)).toBe(false) + }) + + it('should return false if the peer closes with an error', async () => { + const request = { host: 'testhost', port: 9037 } + const identity = mockIdentity('peer') + + const req = await routeTest.client.request('peer/addPeer', request).waitForRoute() + + const matchingPeers = routeTest.peerNetwork.peerManager.peers.filter( + (p) => formatWebSocketAddress(p.wsAddress) === 'ws://testhost:9037', + ) + + expect(matchingPeers.length).toBe(1) + expect(routeTest.peerNetwork.peerManager.peerCandidates.has(identity)).toBe(false) + + const peer = matchingPeers[0] + + let connection: WebSocketConnection + if (peer.state.type === 'CONNECTING' && peer.state.connections.webSocket) { + connection = peer.state.connections.webSocket + } else { + throw new Error('Peer should be CONNECTING with a WS connection') + } + + connection.close(new Error('foo')) + + const response = await req.waitForEnd() + + expect(response.content).toMatchObject({ + added: false, + error: 'foo', }) + expect( + routeTest.peerNetwork.peerManager + .getConnectedPeers() + .filter((p) => p.state.identity === identity), + ).toHaveLength(0) + expect(routeTest.peerNetwork.peerManager.peerCandidates.has(identity)).toBe(false) }) }) diff --git a/ironfish/src/rpc/routes/peer/addPeer.ts b/ironfish/src/rpc/routes/peer/addPeer.ts index 7a2066a1f4..e1204337ea 100644 --- a/ironfish/src/rpc/routes/peer/addPeer.ts +++ b/ironfish/src/rpc/routes/peer/addPeer.ts @@ -4,7 +4,10 @@ import * as yup from 'yup' import { Assert } from '../../../assert' import { DEFAULT_WEBSOCKET_PORT } from '../../../fileStores/config' +import { Peer } from '../../../network' +import { PeerState } from '../../../network/peers/peer' import { FullNode } from '../../../node' +import { ErrorUtils } from '../../../utils' import { ApiNamespace } from '../namespaces' import { routes } from '../router' @@ -16,6 +19,7 @@ export type AddPeerRequest = { export type AddPeerResponse = { added: boolean + error?: string } export const AddPeerRequestSchema: yup.ObjectSchema = yup @@ -41,7 +45,6 @@ routes.register( const peerManager = node.peerNetwork.peerManager const { host, port, whitelist } = request.data - const peer = peerManager.connectToWebSocketAddress({ host, port: port || DEFAULT_WEBSOCKET_PORT, @@ -49,6 +52,36 @@ routes.register( forceConnect: true, }) - request.end({ added: peer !== undefined }) + if (peer === undefined) { + request.end({ added: false }) + return + } + + const onPeerStateChange = ({ + peer, + state, + prevState, + }: { + peer: Peer + state: PeerState + prevState: PeerState + }) => { + if (prevState.type !== 'CONNECTED' && state.type === 'CONNECTED') { + request.end({ added: true }) + peer.onStateChanged.off(onPeerStateChange) + } else if (prevState.type !== 'DISCONNECTED' && state.type === 'DISCONNECTED') { + request.end({ + added: false, + error: peer.error ? ErrorUtils.renderError(peer.error) : undefined, + }) + peer.onStateChanged.off(onPeerStateChange) + } + } + + peer.onStateChanged.on(onPeerStateChange) + + request.onClose.once(() => { + peer.onStateChanged.off(onPeerStateChange) + }) }, ) diff --git a/ironfish/src/rpc/routes/wallet/__fixtures__/getAccountsStatus.test.ts.fixture b/ironfish/src/rpc/routes/wallet/__fixtures__/getAccountsStatus.test.ts.fixture index f4ba321245..f239b967a8 100644 --- a/ironfish/src/rpc/routes/wallet/__fixtures__/getAccountsStatus.test.ts.fixture +++ b/ironfish/src/rpc/routes/wallet/__fixtures__/getAccountsStatus.test.ts.fixture @@ -1,20 +1,20 @@ { - "Route wallet/getAccountsStatus should return account head and sequence": [ + "Route wallet/getAccountsStatus should return account head": [ { "header": { "sequence": 2, "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", "noteCommitment": { "type": "Buffer", - "data": "base64:hUiDIytXmFn6ymKl6I4a7se9yFrctBBH7Cltqi6PwxU=" + "data": "base64:aHGz4sVmUhGttlZ+jvdG5IJltmyHqoIwd0CMDrX+n0c=" }, "transactionCommitment": { "type": "Buffer", - "data": "base64:HHnb2ojUI83olWlTiBuMjwxRmbnH23cTbuX7ahIO0nc=" + "data": "base64:EZGMhS8JZK9SRKZC26MWmzGOcc1ow4gNKw/l2cb+dsw=" }, "target": "9282972777491357380673661573939192202192629606981189395159182914949423", "randomness": "0", - "timestamp": 1695140329119, + "timestamp": 1702505679938, "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", "noteSize": 4, "work": "0" @@ -22,7 +22,7 @@ "transactions": [ { "type": "Buffer", - "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAE4A01Wgz6L8+ETuzXfSA5Z94sath/1dhevirwChjzz6Hhswe5KhlFGe1Y4a8lpyYLAqVEnQjmxgowqhZJ0pI29nODdJxDX9FTEegH0x7P++AkZJTSlGUO+i6z8skFPGcu01O2ybQkz1OD+J1HZtmFOQJ4DYdUNkzokQH0oyouKkZIiW+0BJ+kO2wDy5aWXyzuyQxQY4ecFlgLlFeJFBMo71hc0sIZQuiNIpxXjIzRo2saArp8f1KBJxZthyttEPh5eMCcnlVemzDolFseFKktxlVyOArhpF2XlFG7ezCKuyM6omziYzTo8YoedwJYbl1e42ixoHwYdUUBmXQyzKTEdOfymBih/Jisfp7Upw0hxPPBsTt0CfjxRAKUdV0kGZqQoK3JAbxpt5GSu4Tur3nkLNuvhprgghhkBky9QTcvjoPGu1JukG1OZ7NAQfa/X3SXHKlZSojz+d8HQVdKN7P3blmmqbJ3kmDqRa9CgxNSVwQZY5PIjcPgQ3QPaRNiVVpEPYcHtha4ObSZ705Htac7ZBi/5MiJ7FN0DjUugzaOpgVyRNPKV+nCzJ28ORZI1fx3hykStc9YEf9nef63V341h07KSaIaQaMG8rbmtQEvjoqwA0SLFTj8Ulyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwVu5uXi5vDPUUkxXOalSvHHuEWr5yYJbTNswWKjqpONO/bC+t6CjF0+eVS9ARkmxqQiam6iBxX8YRyUfG7F3kBg==" + "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAACIFiBp1GbWZtdr7trm0wljgN1tTxKD0ZgDF2xd45f9WoZnOhhWFnd+iBG65TBZYwua5LEaCzBnraevEu8JELyMPGypr+7hcFdIAP/ZfsvG6KnpJjLtvHom5xe7MBkxXTlG7DEHLMKjo8FnKhrOeKktNMuCwfGk6marmj5eSSe38DNUk8V2fMGpXYW4BbT2CMl/ucbGKhwYFh/JHXbBoRoqtH3tmZNPxlH74QMiCFXumh/5jEru2T1Mw7EbdzW/4qByU/Og1UWlU+Iar8bG7gvIVubbKUCIRcUnuk17P8qfDVxWUCpFCamCQPeMYbsJ56/JJDHMildhkIRWZpixjIkzPRlpvDxmJn2tFAp8vioGML3vWeQEpb1t6OnBtLkRlvmlE+8N9S4DetC5pxaRXaKF6ATZ/lSTIXsccBglgVfzqyHIVQ/Pj/THU6oL/pRmA1NLTnXu/p9iLnO2z5YzKOaW4RdPyDmVrMTyKWJ6+1Lx7t8K6O9LOqVywiHlMyh46ipJBflJk1u7b4EkwaN+BBXT3xJDqRMz+kMjIn5ryO3Bk5stPdyDO0svKwa6RpkITUk71vq56d2bjvNKdKjdBC5rtPwtnTy8bKaaP+X1mbO3FVCXV8DJBBDElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw0+MjHMqsy2YcShwmGNJ2z/tQhxbm3ioiAMLuT1c1Wa1A7dUIbWEgKzPYgwmcfz99OLnTRDCvXAkDIkDn/JEZCw==" } ] } diff --git a/ironfish/src/rpc/routes/wallet/getAccountStatus.test.ts b/ironfish/src/rpc/routes/wallet/getAccountStatus.test.ts new file mode 100644 index 0000000000..b68b1b5926 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/getAccountStatus.test.ts @@ -0,0 +1,49 @@ +/* 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/. */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { v4 as uuid } from 'uuid' +import { createRouteTest } from '../../../testUtilities/routeTest' + +describe('Route wallet/getAccountStatus', () => { + const routeTest = createRouteTest() + + it('returns account status information', async () => { + const account = await routeTest.node.wallet.createAccount(uuid(), { + setCreatedAt: true, + setDefault: true, + }) + const response = await routeTest.client + .request('wallet/getAccountStatus', { + account: account.name, + }) + .waitForEnd() + + expect(response.status).toBe(200) + expect(response.content).toMatchObject({ + account: { + name: account.name, + id: account.id, + head: { + hash: routeTest.chain.head.hash.toString('hex'), + sequence: routeTest.chain.head.sequence, + inChain: true, + }, + viewOnly: false, + }, + }) + }) + + it('errors if no account exists', async () => { + await expect(() => { + return routeTest.client + .request('wallet/getAccountStatus', { + account: 'asdf', + }) + .waitForEnd() + }).rejects.toThrow('No account with name asdf') + }) +}) diff --git a/ironfish/src/rpc/routes/wallet/getAccountStatus.ts b/ironfish/src/rpc/routes/wallet/getAccountStatus.ts new file mode 100644 index 0000000000..86f0a6600f --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/getAccountStatus.ts @@ -0,0 +1,41 @@ +/* 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 } from '../namespaces' +import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' +import { RpcAccountStatus, RpcAccountStatusSchema } from './types' +import { getAccount, serializeRpcAccountStatus } from './utils' + +export type GetAccountStatusRequest = { account: string } + +export type GetAccountStatusResponse = { + account: RpcAccountStatus +} + +export const GetAccountStatusRequestSchema: yup.ObjectSchema = yup + .object({ + account: yup.string().defined(), + }) + .defined() + +export const GetAccountStatusResponseSchema: yup.ObjectSchema = yup + .object({ + account: RpcAccountStatusSchema, + }) + .defined() + +routes.register( + `${ApiNamespace.wallet}/getAccountStatus`, + GetAccountStatusRequestSchema, + async (request, node): Promise => { + AssertHasRpcContext(request, node, 'wallet') + + const account = getAccount(node.wallet, request.data.account) + + const accountStatus = await serializeRpcAccountStatus(node.wallet, account) + + request.end({ account: accountStatus }) + }, +) diff --git a/ironfish/src/rpc/routes/wallet/getAccountsStatus.test.ts b/ironfish/src/rpc/routes/wallet/getAccountsStatus.test.ts index 53048b70e7..50401f8225 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountsStatus.test.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountsStatus.test.ts @@ -6,18 +6,23 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { v4 as uuid } from 'uuid' -import { Assert } from '../../../assert' import { useMinerBlockFixture } from '../../../testUtilities/fixtures' import { createRouteTest } from '../../../testUtilities/routeTest' describe('Route wallet/getAccountsStatus', () => { - const routeTest = createRouteTest(true) + const routeTest = createRouteTest() - it('should return account status information', async () => { + it('should return account head', async () => { const account = await routeTest.node.wallet.createAccount(uuid(), { setCreatedAt: true, setDefault: true, }) + + const block = await useMinerBlockFixture(routeTest.chain, 2, account, routeTest.wallet) + + await expect(routeTest.chain).toAddBlock(block) + await routeTest.wallet.updateHead() + const response = await routeTest.client .request('wallet/getAccountsStatus', {}) .waitForEnd() @@ -28,22 +33,25 @@ describe('Route wallet/getAccountsStatus', () => { { name: account.name, id: account.id, - headHash: routeTest.chain.head.hash.toString('hex'), - headInChain: true, - sequence: routeTest.chain.head.sequence, + head: { + hash: routeTest.chain.head.hash.toString('hex'), + sequence: routeTest.chain.head.sequence, + inChain: true, + }, + viewOnly: false, }, ], }) }) - it('should return account head and sequence', async () => { - const account = routeTest.wallet.getDefaultAccount() - Assert.isNotNull(account) - - const block = await useMinerBlockFixture(routeTest.chain, 2, account, routeTest.wallet) - - await expect(routeTest.chain).toAddBlock(block) - await routeTest.wallet.updateHead() + it('should return true for view-only accounts', async () => { + let account = await routeTest.wallet.createAccount('temp') + await routeTest.wallet.removeAccountByName('temp') + account = await routeTest.wallet.importAccount({ + ...account, + name: 'viewonly', + spendingKey: null, + }) const response = await routeTest.client .request('wallet/getAccountsStatus', {}) @@ -55,9 +63,8 @@ describe('Route wallet/getAccountsStatus', () => { { name: account.name, id: account.id, - headHash: routeTest.chain.head.hash.toString('hex'), - headInChain: true, - sequence: routeTest.chain.head.sequence, + head: null, + viewOnly: true, }, ], }) diff --git a/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts b/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts index bc867332c8..b7b40d3414 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts @@ -2,74 +2,41 @@ * 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 { HeadValue } from '../../../wallet/walletdb/headValue' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' +import { RpcAccountStatus, RpcAccountStatusSchema } from './types' +import { serializeRpcAccountStatus } from './utils' -export type GetAccountStatusRequest = { account?: string } +export type GetAccountsStatusRequest = Record | undefined -export type GetAccountStatusResponse = { - accounts: { - name: string - id: string - headHash: string - headInChain?: boolean - sequence: string | number - }[] +export type GetAccountsStatusResponse = { + accounts: RpcAccountStatus[] } -export const GetAccountStatusRequestSchema: yup.ObjectSchema = yup - .object({}) - .defined() +export const GetAccountsStatusRequestSchema: yup.ObjectSchema = yup + .object>({}) + .notRequired() + .default({}) -export const GetAccountStatusResponseSchema: yup.ObjectSchema = yup +export const GetAccountsStatusResponseSchema: yup.ObjectSchema = yup .object({ - accounts: yup - .array( - yup - .object({ - name: yup.string().defined(), - id: yup.string().defined(), - headHash: yup.string().defined(), - headInChain: yup.boolean().optional(), - sequence: yup.string().defined(), - }) - .defined(), - ) - .defined(), + accounts: yup.array(RpcAccountStatusSchema).defined(), }) .defined() -routes.register( +routes.register( `${ApiNamespace.wallet}/getAccountsStatus`, - GetAccountStatusRequestSchema, + GetAccountsStatusRequestSchema, async (request, node): Promise => { - const heads = new Map() AssertHasRpcContext(request, node, 'wallet') - for await (const { accountId, head } of node.wallet.walletDb.loadHeads()) { - heads.set(accountId, head) - } - - const accountsInfo: GetAccountStatusResponse['accounts'] = [] - for (const account of node.wallet.listAccounts()) { - const head = heads.get(account.id) - - let headInChain = undefined - if (node.wallet.nodeClient) { - headInChain = head?.hash ? await node.wallet.chainHasBlock(head.hash) : false - } - - accountsInfo.push({ - name: account.name, - id: account.id, - headHash: head?.hash.toString('hex') || 'NULL', - headInChain, - sequence: head?.sequence || 'NULL', - }) - } + const accounts = await Promise.all( + node.wallet + .listAccounts() + .map((account) => serializeRpcAccountStatus(node.wallet, account)), + ) - request.end({ accounts: accountsInfo }) + request.end({ accounts }) }, ) diff --git a/ironfish/src/rpc/routes/wallet/index.ts b/ironfish/src/rpc/routes/wallet/index.ts index b26cd4cc3b..026effb505 100644 --- a/ironfish/src/rpc/routes/wallet/index.ts +++ b/ironfish/src/rpc/routes/wallet/index.ts @@ -11,6 +11,7 @@ export * from './estimateFeeRates' export * from './exportAccount' export * from './getAccountNotesStream' export * from './getAccounts' +export * from './getAccountStatus' export * from './getAccountsStatus' export * from './getAccountTransaction' export * from './getAccountTransactions' diff --git a/ironfish/src/rpc/routes/wallet/types.ts b/ironfish/src/rpc/routes/wallet/types.ts index fce9c8033e..19efff7939 100644 --- a/ironfish/src/rpc/routes/wallet/types.ts +++ b/ironfish/src/rpc/routes/wallet/types.ts @@ -162,3 +162,30 @@ export const RpcAccountImportSchema: yup.ObjectSchema = yup .defined(), }) .defined() + +export type RpcAccountStatus = { + name: string + id: string + head: { + hash: string + sequence: number + inChain: boolean | null + } | null + viewOnly: boolean +} + +export const RpcAccountStatusSchema: yup.ObjectSchema = yup + .object({ + name: yup.string().defined(), + id: yup.string().defined(), + head: yup + .object({ + hash: yup.string().defined(), + sequence: yup.number().defined(), + inChain: yup.boolean().nullable().defined(), + }) + .nullable() + .defined(), + viewOnly: yup.boolean().defined(), + }) + .defined() diff --git a/ironfish/src/rpc/routes/wallet/utils.ts b/ironfish/src/rpc/routes/wallet/utils.ts index 8693970fb6..2512e56e5f 100644 --- a/ironfish/src/rpc/routes/wallet/utils.ts +++ b/ironfish/src/rpc/routes/wallet/utils.ts @@ -14,6 +14,7 @@ import { ValidationError } from '../../adapters' import { RpcAccountAssetBalanceDelta, RpcAccountImport, + RpcAccountStatus, RpcWalletNote, RpcWalletTransaction, } from './types' @@ -225,3 +226,23 @@ export function serializeRpcWalletNote( hash: note.note.hash().toString('hex'), } } + +export async function serializeRpcAccountStatus( + wallet: Wallet, + account: Account, +): Promise { + const head = await account.getHead() + + return { + name: account.name, + id: account.id, + head: head + ? { + hash: head.hash.toString('hex'), + sequence: head.sequence, + inChain: wallet.nodeClient ? await wallet.chainHasBlock(head.hash) : null, + } + : null, + viewOnly: !account.isSpendingAccount(), + } +} diff --git a/ironfish/src/serde/PartialHeaderSerde.ts b/ironfish/src/serde/PartialHeaderSerde.ts deleted file mode 100644 index 12f1679804..0000000000 --- a/ironfish/src/serde/PartialHeaderSerde.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -import bufio from 'bufio' -import { NoteEncryptedHash } from '../primitives/noteEncrypted' -import { Target } from '../primitives/target' - -export default class PartialBlockHeaderSerde { - static serialize(header: PartialBlockHeader): Buffer { - const bw = bufio.write(172) - bw.writeU32(header.sequence) - bw.writeHash(header.previousBlockHash) - bw.writeHash(header.noteCommitment) - bw.writeHash(header.transactionCommitment) - bw.writeBigU256BE(header.target.asBigInt()) - bw.writeU64(header.timestamp.getTime()) - bw.writeBytes(header.graffiti) - return bw.render() - } - - static deserialize(data: Buffer): PartialBlockHeader { - const br = bufio.read(data) - const sequence = br.readU32() - const previousBlockHash = br.readHash() - const noteCommitment = br.readHash() - const transactionCommitment = br.readHash() - const target = br.readBytes(32) - const timestamp = br.readU64() - const graffiti = br.readBytes(32) - - return { - sequence: sequence, - previousBlockHash: previousBlockHash, - target: new Target(target), - timestamp: new Date(timestamp), - graffiti: graffiti, - noteCommitment: noteCommitment, - transactionCommitment, - } - } - - static equals(): boolean { - throw new Error('You should never use this') - } -} - -export type PartialBlockHeader = { - sequence: number - previousBlockHash: Buffer - noteCommitment: NoteEncryptedHash - transactionCommitment: Buffer - target: Target - timestamp: Date - graffiti: Buffer -}