From f253cb1b2f502792d747feed0733fb34ee3526d1 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Fri, 1 Dec 2023 14:56:14 -0500 Subject: [PATCH 01/13] Rahul/ifl 1849 transaction summary with note details (#4469) --- ironfish-cli/src/commands/wallet/send.ts | 58 ++++++++++++++---------- 1 file changed, 34 insertions(+), 24 deletions(-) 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)?') - } } From afc2d87b1d462edb6e76a5b6eb53410edfff39dd Mon Sep 17 00:00:00 2001 From: jowparks Date: Mon, 4 Dec 2023 12:30:05 -0800 Subject: [PATCH 02/13] chore: removes reference to transmission key (#4420) --- ironfish-rust/src/keys/public_address.rs | 49 +++++++++--------------- ironfish-rust/src/keys/test.rs | 14 +++---- ironfish-rust/src/merkle_note.rs | 14 +++---- ironfish-rust/src/note.rs | 20 ++++------ ironfish-rust/src/transaction/mints.rs | 2 +- ironfish-rust/src/transaction/outputs.rs | 2 +- ironfish-rust/src/transaction/spends.rs | 4 +- 7 files changed, 41 insertions(+), 64 deletions(-) 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..3f4fc57533 100644 --- a/ironfish-rust/src/transaction/mints.rs +++ b/ironfish-rust/src/transaction/mints.rs @@ -201,7 +201,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/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..3ebd4ab631 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 From 5fa56812cfd7d69833d449bb328d6feb72078f33 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Mon, 4 Dec 2023 14:35:34 -0800 Subject: [PATCH 03/13] Rahul/ifl 1851 move notes command into folder (#4468) * moving notes file to notes folder * removing old notes file --- .../src/commands/wallet/{notes.ts => notes/index.ts} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename ironfish-cli/src/commands/wallet/{notes.ts => notes/index.ts} (93%) 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 { From 48f2f2e86a0407ce62a7ad8a362ef6f0a7fbc53d Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 4 Dec 2023 14:12:04 -0800 Subject: [PATCH 04/13] Add warnings about a change to the RedDSA spec The RedDSA specification (and the Rust crates that implement it) has slightly deviated from the code we're using. The changes make the signatures backward compatible, but the code we have written needs to change in order to be compatible. This commit adds some comments to the relevant code so that if in the future someone attempts to update the RedDSA crates, they won't have to spend hours to figure out what's wrong. --- ironfish-rust/src/transaction/mints.rs | 11 +++++++++++ ironfish-rust/src/transaction/mod.rs | 5 +++++ ironfish-rust/src/transaction/spends.rs | 11 +++++++++++ 3 files changed, 27 insertions(+) diff --git a/ironfish-rust/src/transaction/mints.rs b/ironfish-rust/src/transaction/mints.rs index 3f4fc57533..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[..]); 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/spends.rs b/ironfish-rust/src/transaction/spends.rs index 3ebd4ab631..3e646851c0 100644 --- a/ironfish-rust/src/transaction/spends.rs +++ b/ironfish-rust/src/transaction/spends.rs @@ -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[..]); From 8189f15ae64a4a6f8cd3397e9d81bccfef268dd2 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Wed, 6 Dec 2023 20:39:02 -0500 Subject: [PATCH 05/13] Update GitHub actions to newer versions (#4473) * Update checkout to v4 * Update setup-node to v4 --- .github/workflows/build-ironfish-rust-nodejs.yml | 8 ++++---- .github/workflows/ci-regenerate-fixtures.yml | 4 ++-- .github/workflows/ci.yml | 10 +++++----- .github/workflows/deploy-brew.yml | 4 ++-- .github/workflows/deploy-node-docker-image.yml | 2 +- .github/workflows/deploy-npm-ironfish-cli.yml | 4 ++-- .github/workflows/deploy-npm-ironfish-rust-nodejs.yml | 4 ++-- .github/workflows/deploy-npm-ironfish.yml | 4 ++-- .github/workflows/perf_test.yml | 4 ++-- .github/workflows/publish-binaries.yml | 2 +- .github/workflows/push-version-to-api.yml | 2 +- .github/workflows/rust_ci.yml | 6 +++--- .github/workflows/rust_ci_cache.yml | 2 +- 13 files changed, 28 insertions(+), 28 deletions(-) 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 From a555929b1f5a5e5bf06ace888951432ac2166cba Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 8 Dec 2023 15:30:59 -0800 Subject: [PATCH 06/13] Fix types using '' instead of string (#4477) --- ironfish/src/fileStores/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From b583a5991faed3d4ce90c358349bd5ded8e4718b Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Wed, 13 Dec 2023 09:25:35 -0500 Subject: [PATCH 07/13] Use performance.now for snapshot duration (#4480) --- ironfish-cli/src/snapshot.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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) => { From 9c2dd4335aae817b61242817ddc16ea489b7e86e Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Wed, 13 Dec 2023 12:25:32 -0500 Subject: [PATCH 08/13] Return whether account is view only from getAccountsStatus RPC (#4481) --- ironfish-cli/src/commands/wallet/status.ts | 3 ++ .../routes/wallet/getAccountsStatus.test.ts | 39 +++++++++++++++++-- .../rpc/routes/wallet/getAccountsStatus.ts | 3 ++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/status.ts b/ironfish-cli/src/commands/wallet/status.ts index cfae466c11..2e884c0f9b 100644 --- a/ironfish-cli/src/commands/wallet/status.ts +++ b/ironfish-cli/src/commands/wallet/status.ts @@ -33,6 +33,9 @@ export class StatusCommand extends IronfishCommand { id: { header: 'Account ID', }, + viewOnly: { + header: 'View Only', + }, headHash: { header: 'Head Hash', }, diff --git a/ironfish/src/rpc/routes/wallet/getAccountsStatus.test.ts b/ironfish/src/rpc/routes/wallet/getAccountsStatus.test.ts index 53048b70e7..649c7f3df4 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountsStatus.test.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountsStatus.test.ts @@ -6,12 +6,11 @@ /* 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 () => { const account = await routeTest.node.wallet.createAccount(uuid(), { @@ -31,14 +30,17 @@ describe('Route wallet/getAccountsStatus', () => { headHash: routeTest.chain.head.hash.toString('hex'), headInChain: true, sequence: routeTest.chain.head.sequence, + viewOnly: false, }, ], }) }) it('should return account head and sequence', async () => { - const account = routeTest.wallet.getDefaultAccount() - Assert.isNotNull(account) + const account = await routeTest.node.wallet.createAccount(uuid(), { + setCreatedAt: true, + setDefault: true, + }) const block = await useMinerBlockFixture(routeTest.chain, 2, account, routeTest.wallet) @@ -58,6 +60,35 @@ describe('Route wallet/getAccountsStatus', () => { headHash: routeTest.chain.head.hash.toString('hex'), headInChain: true, sequence: routeTest.chain.head.sequence, + viewOnly: false, + }, + ], + }) + }) + + 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', {}) + .waitForEnd() + + expect(response.status).toBe(200) + expect(response.content).toMatchObject({ + accounts: [ + { + name: account.name, + id: account.id, + headHash: 'NULL', + headInChain: false, + sequence: 'NULL', + viewOnly: true, }, ], }) diff --git a/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts b/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts index bc867332c8..1767ee8895 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts @@ -16,6 +16,7 @@ export type GetAccountStatusResponse = { headHash: string headInChain?: boolean sequence: string | number + viewOnly: boolean }[] } @@ -34,6 +35,7 @@ export const GetAccountStatusResponseSchema: yup.ObjectSchema( headHash: head?.hash.toString('hex') || 'NULL', headInChain, sequence: head?.sequence || 'NULL', + viewOnly: !account.isSpendingAccount(), }) } From 9a116b66d08995f7f7fac410ec77993cf8bfd3ec Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Wed, 13 Dec 2023 13:33:36 -0800 Subject: [PATCH 09/13] Remove hashing algorithm from unit test assertions (#4478) --- ironfish/src/network/peerNetwork.test.ts | 4 +- ironfish/src/network/testUtilities/helpers.ts | 9 +-- ironfish/src/primitives/blockheader.test.ts | 6 -- ironfish/src/primitives/blockheader.ts | 47 +++++++--------- ironfish/src/serde/PartialHeaderSerde.ts | 56 ------------------- 5 files changed, 24 insertions(+), 98 deletions(-) delete mode 100644 ironfish/src/serde/PartialHeaderSerde.ts 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/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 -} From 208c749e1e92c4775e34dc8e8ab47810a390263b Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Wed, 13 Dec 2023 17:17:09 -0500 Subject: [PATCH 10/13] Display a clearer error when passing --path and an invalid argument (#4483) --- ironfish-cli/src/commands/wallet/import.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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) { From d6e44e693a913331d6e67a6fc9fa22d75edebb80 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Thu, 14 Dec 2023 11:20:19 -0500 Subject: [PATCH 11/13] Remove unused account parameter in getAccountsStatus and add getAccountStatus (#4482) --- ironfish-cli/src/commands/wallet/status.ts | 12 +-- ironfish/src/rpc/clients/client.ts | 19 ++++- .../getAccountsStatus.test.ts.fixture | 10 +-- .../routes/wallet/getAccountStatus.test.ts | 49 ++++++++++++ .../src/rpc/routes/wallet/getAccountStatus.ts | 41 ++++++++++ .../routes/wallet/getAccountsStatus.test.ts | 38 ++-------- .../rpc/routes/wallet/getAccountsStatus.ts | 74 +++++-------------- ironfish/src/rpc/routes/wallet/index.ts | 1 + ironfish/src/rpc/routes/wallet/types.ts | 27 +++++++ ironfish/src/rpc/routes/wallet/utils.ts | 21 ++++++ 10 files changed, 192 insertions(+), 100 deletions(-) create mode 100644 ironfish/src/rpc/routes/wallet/getAccountStatus.test.ts create mode 100644 ironfish/src/rpc/routes/wallet/getAccountStatus.ts diff --git a/ironfish-cli/src/commands/wallet/status.ts b/ironfish-cli/src/commands/wallet/status.ts index 2e884c0f9b..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, @@ -37,12 +34,15 @@ export class StatusCommand extends IronfishCommand { 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/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/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 649c7f3df4..50401f8225 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountsStatus.test.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountsStatus.test.ts @@ -12,31 +12,7 @@ import { createRouteTest } from '../../../testUtilities/routeTest' describe('Route wallet/getAccountsStatus', () => { const routeTest = createRouteTest() - it('should return account status information', async () => { - const account = await routeTest.node.wallet.createAccount(uuid(), { - setCreatedAt: true, - setDefault: true, - }) - const response = await routeTest.client - .request('wallet/getAccountsStatus', {}) - .waitForEnd() - - expect(response.status).toBe(200) - expect(response.content).toMatchObject({ - accounts: [ - { - name: account.name, - id: account.id, - headHash: routeTest.chain.head.hash.toString('hex'), - headInChain: true, - sequence: routeTest.chain.head.sequence, - viewOnly: false, - }, - ], - }) - }) - - it('should return account head and sequence', async () => { + it('should return account head', async () => { const account = await routeTest.node.wallet.createAccount(uuid(), { setCreatedAt: true, setDefault: true, @@ -57,9 +33,11 @@ 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, }, ], @@ -85,9 +63,7 @@ describe('Route wallet/getAccountsStatus', () => { { name: account.name, id: account.id, - headHash: 'NULL', - headInChain: false, - sequence: 'NULL', + head: null, viewOnly: true, }, ], diff --git a/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts b/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts index 1767ee8895..b7b40d3414 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts @@ -2,77 +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 - viewOnly: boolean - }[] +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(), - viewOnly: yup.boolean().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', - viewOnly: !account.isSpendingAccount(), - }) - } + 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(), + } +} From 39da0f7bc4c822a3db46c7dc1dcb74421ce27357 Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Thu, 14 Dec 2023 11:22:54 -0800 Subject: [PATCH 12/13] Wait for connection status on add peer command (#4448) --- ironfish-cli/src/commands/peers/add.ts | 5 + ironfish/src/rpc/routes/peer/addPeer.test.ts | 110 ++++++++++++++++++- ironfish/src/rpc/routes/peer/addPeer.ts | 37 ++++++- 3 files changed, 145 insertions(+), 7 deletions(-) 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/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) + }) }, ) From 8b37052e4d71b3dc3eb7ca003cbf3c0d8b8afd35 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Thu, 14 Dec 2023 15:50:39 -0500 Subject: [PATCH 13/13] SDK and CLI v1.14.0 (#4485) --- 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 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-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/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",