diff --git a/protobufs/protobufs b/protobufs/protobufs index a55d4942..6096d432 160000 --- a/protobufs/protobufs +++ b/protobufs/protobufs @@ -1 +1 @@ -Subproject commit a55d49421ec2bf415063d0242437fa4b9b959ec8 +Subproject commit 6096d432efcb9bc76bb0b813cf17c1b001e36335 diff --git a/src/account/account_create_transaction.rs b/src/account/account_create_transaction.rs index ee8c3dae..b56e2093 100644 --- a/src/account/account_create_transaction.rs +++ b/src/account/account_create_transaction.rs @@ -82,7 +82,8 @@ pub struct AccountCreateTransactionData { /// The maximum number of tokens that an Account can be implicitly associated with. /// /// Defaults to `0`. Allows up to a maximum value of `1000`. - max_automatic_token_associations: u16, + /// If the value is set to `-1`, unlimited automatic token associations are allowed. + max_automatic_token_associations: i32, // notably *not* a PublicKey. /// A 20-byte EVM address to be used as the account's alias. @@ -201,12 +202,12 @@ impl AccountCreateTransaction { /// /// Defaults to `0`. Allows up to a maximum value of `1000`. #[must_use] - pub fn get_max_automatic_token_associations(&self) -> u16 { + pub fn get_max_automatic_token_associations(&self) -> i32 { self.data().max_automatic_token_associations } /// Sets the maximum number of tokens that an Account can be implicitly associated with. - pub fn max_automatic_token_associations(&mut self, amount: u16) -> &mut Self { + pub fn max_automatic_token_associations(&mut self, amount: i32) -> &mut Self { self.data_mut().max_automatic_token_associations = amount; self } @@ -326,7 +327,7 @@ impl FromProtobuf for AccountCreateTransa auto_renew_period: pb.auto_renew_period.map(Into::into), auto_renew_account_id: None, account_memo: pb.memo, - max_automatic_token_associations: pb.max_automatic_token_associations as u16, + max_automatic_token_associations: pb.max_automatic_token_associations, alias, staked_id: Option::from_protobuf(pb.staked_id)?, decline_staking_reward: pb.decline_reward, @@ -410,7 +411,7 @@ mod tests { const STAKED_ACCOUNT_ID: AccountId = AccountId::new(0, 0, 3); const STAKED_NODE_ID: u64 = 4; const ALIAS: EvmAddress = EvmAddress(hex!("5c562e90feaf0eebd33ea75d21024f249d451417")); - const MAX_AUTOMATIC_TOKEN_ASSOCIATIONS: u16 = 100; + const MAX_AUTOMATIC_TOKEN_ASSOCIATIONS: i32 = 100; fn make_transaction() -> AccountCreateTransaction { let mut tx = AccountCreateTransaction::new_for_tests(); @@ -699,7 +700,7 @@ mod tests { realm_id: None, new_realm_admin_key: None, memo: ACCOUNT_MEMO.to_owned(), - max_automatic_token_associations: MAX_AUTOMATIC_TOKEN_ASSOCIATIONS as i32, + max_automatic_token_associations: MAX_AUTOMATIC_TOKEN_ASSOCIATIONS, decline_reward: false, alias: ALIAS.to_bytes().to_vec(), staked_id: Some(services::crypto_create_transaction_body::StakedId::StakedAccountId( diff --git a/src/account/account_update_transaction.rs b/src/account/account_update_transaction.rs index 02054f48..52aa16d5 100644 --- a/src/account/account_update_transaction.rs +++ b/src/account/account_update_transaction.rs @@ -99,8 +99,8 @@ pub struct AccountUpdateTransactionData { /// The maximum number of tokens that an Account can be implicitly associated with. /// /// Defaults to `0`. Allows up to a maximum value of `1000`. - /// - max_automatic_token_associations: Option, + /// If the value is set to `-1`, unlimited automatic token associations are allowed. + max_automatic_token_associations: Option, /// ID of the account or node to which this account is staking, if any. staked_id: Option, @@ -227,12 +227,13 @@ impl AccountUpdateTransaction { /// Returns the maximum number of tokens that an Account can be implicitly associated with. #[must_use] - pub fn get_max_automatic_token_associations(&self) -> Option { + pub fn get_max_automatic_token_associations(&self) -> Option { self.data().max_automatic_token_associations } /// Sets the maximum number of tokens that an Account can be implicitly associated with. - pub fn max_automatic_token_associations(&mut self, amount: u16) -> &mut Self { + /// + pub fn max_automatic_token_associations(&mut self, amount: i32) -> &mut Self { self.data_mut().max_automatic_token_associations = Some(amount); self } @@ -353,9 +354,7 @@ impl FromProtobuf for AccountUpdateTransa proxy_account_id: Option::from_protobuf(pb.proxy_account_id)?, expiration_time: pb.expiration_time.map(Into::into), account_memo: pb.memo, - max_automatic_token_associations: pb - .max_automatic_token_associations - .map(|it| it as u16), + max_automatic_token_associations: pb.max_automatic_token_associations, staked_id: Option::from_protobuf(pb.staked_id)?, decline_staking_reward: pb.decline_reward, }) @@ -445,7 +444,7 @@ mod tests { }; const RECEIVER_SIGNATURE_REQUIRED: bool = false; - const MAX_AUTOMATIC_TOKEN_ASSOCIATIONS: u16 = 100; + const MAX_AUTOMATIC_TOKEN_ASSOCIATIONS: i32 = 100; const ACCOUNT_MEMO: &str = "Some memo"; const STAKED_ACCOUNT_ID: AccountId = AccountId::new(0, 0, 3); const STAKED_NODE_ID: u64 = 4; diff --git a/src/contract/contract_create_flow.rs b/src/contract/contract_create_flow.rs index 4afa12d8..148b7874 100644 --- a/src/contract/contract_create_flow.rs +++ b/src/contract/contract_create_flow.rs @@ -161,14 +161,14 @@ impl ContractCreateFlow { /// Retunrs the maximum number of tokens that the contract can be automatically associated with. #[must_use] - pub fn get_max_automatic_token_associations(&self) -> u32 { + pub fn get_max_automatic_token_associations(&self) -> i32 { self.contract_data.max_automatic_token_associations } /// Sets the maximum number of tokens that the contract can be automatically associated with. pub fn max_automatic_token_associations( &mut self, - max_automatic_token_associations: u32, + max_automatic_token_associations: i32, ) -> &mut Self { self.contract_data.max_automatic_token_associations = max_automatic_token_associations; @@ -382,7 +382,7 @@ struct ContractData { constructor_parameters: Vec, gas: u64, initial_balance: Hbar, - max_automatic_token_associations: u32, + max_automatic_token_associations: i32, decline_staking_reward: bool, admin_key: Option, // proxy_account_id: Option diff --git a/src/contract/contract_create_transaction.rs b/src/contract/contract_create_transaction.rs index ded6da7f..50f7b57b 100644 --- a/src/contract/contract_create_transaction.rs +++ b/src/contract/contract_create_transaction.rs @@ -67,7 +67,7 @@ pub struct ContractCreateTransactionData { contract_memo: String, - max_automatic_token_associations: u32, + max_automatic_token_associations: i32, auto_renew_account_id: Option, @@ -196,12 +196,12 @@ impl ContractCreateTransaction { /// Returns the maximum number of tokens that the contract can be automatically associated with. #[must_use] - pub fn get_max_automatic_token_associations(&self) -> u32 { + pub fn get_max_automatic_token_associations(&self) -> i32 { self.data().max_automatic_token_associations } /// Sets the maximum number of tokens that this contract can be automatically associated with. - pub fn max_automatic_token_associations(&mut self, max: u32) -> &mut Self { + pub fn max_automatic_token_associations(&mut self, max: i32) -> &mut Self { self.data_mut().max_automatic_token_associations = max; self } @@ -327,7 +327,7 @@ impl FromProtobuf for ContractCreateTra auto_renew_period: pb_getf!(pb, auto_renew_period)?.into(), constructor_parameters: pb.constructor_parameters, contract_memo: pb.memo, - max_automatic_token_associations: pb.max_automatic_token_associations as u32, + max_automatic_token_associations: pb.max_automatic_token_associations, auto_renew_account_id: Option::from_protobuf(pb.auto_renew_account_id)?, staked_id: Option::from_protobuf(pb.staked_id)?, decline_staking_reward: pb.decline_reward, @@ -385,7 +385,7 @@ impl ToProtobuf for ContractCreateTransactionData { realm_id: None, new_realm_admin_key: None, memo: self.contract_memo.clone(), - max_automatic_token_associations: self.max_automatic_token_associations as i32, + max_automatic_token_associations: self.max_automatic_token_associations, auto_renew_account_id, decline_reward: self.decline_staking_reward, initcode_source, @@ -429,7 +429,7 @@ mod tests { const GAS: u64 = 0; const INITIAL_BALANCE: Hbar = Hbar::from_tinybars(1000); - const MAX_AUTOMATIC_TOKEN_ASSOCIATIONS: u32 = 101; + const MAX_AUTOMATIC_TOKEN_ASSOCIATIONS: i32 = 101; const AUTO_RENEW_PERIOD: Duration = Duration::hours(10); const CONSTRUCTOR_PARAMETERS: [u8; 5] = [10, 11, 12, 13, 25]; const AUTO_RENEW_ACCOUNT_ID: AccountId = AccountId::new(0, 0, 30); @@ -729,7 +729,7 @@ mod tests { realm_id: None, new_realm_admin_key: None, memo: String::new(), - max_automatic_token_associations: MAX_AUTOMATIC_TOKEN_ASSOCIATIONS as i32, + max_automatic_token_associations: MAX_AUTOMATIC_TOKEN_ASSOCIATIONS, decline_reward: false, staked_id: Some(services::contract_create_transaction_body::StakedId::StakedAccountId( STAKED_ACCOUNT_ID.to_protobuf(), diff --git a/src/contract/contract_update_transaction.rs b/src/contract/contract_update_transaction.rs index 2e6a61ab..35e3b617 100644 --- a/src/contract/contract_update_transaction.rs +++ b/src/contract/contract_update_transaction.rs @@ -63,7 +63,7 @@ pub struct ContractUpdateTransactionData { contract_memo: Option, - max_automatic_token_associations: Option, + max_automatic_token_associations: Option, auto_renew_account_id: Option, @@ -138,12 +138,12 @@ impl ContractUpdateTransaction { /// Returns the maximum number of tokens that this contract can be automatically associated with. #[must_use] - pub fn get_max_automatic_token_associations(&self) -> Option { + pub fn get_max_automatic_token_associations(&self) -> Option { self.data().max_automatic_token_associations } /// Sets the maximum number of tokens that this contract can be automatically associated with. - pub fn max_automatic_token_associations(&mut self, max: u32) -> &mut Self { + pub fn max_automatic_token_associations(&mut self, max: i32) -> &mut Self { self.data_mut().max_automatic_token_associations = Some(max); self } @@ -268,9 +268,7 @@ impl FromProtobuf for ContractUpdateTra contract_memo: pb.memo_field.map(|it| match it { MemoField::Memo(it) | MemoField::MemoWrapper(it) => it, }), - max_automatic_token_associations: pb - .max_automatic_token_associations - .map(|it| it as u32), + max_automatic_token_associations: pb.max_automatic_token_associations, auto_renew_account_id: Option::from_protobuf(pb.auto_renew_account_id)?, proxy_account_id: Option::from_protobuf(pb.proxy_account_id)?, staked_id: Option::from_protobuf(pb.staked_id)?, @@ -365,7 +363,7 @@ mod tests { const CONTRACT_ID: ContractId = ContractId::new(0, 0, 5007); - const MAX_AUTOMATIC_TOKEN_ASSOCIATIONS: u32 = 101; + const MAX_AUTOMATIC_TOKEN_ASSOCIATIONS: i32 = 101; const AUTO_RENEW_PERIOD: Duration = Duration::days(1); const CONTRACT_MEMO: &str = "3"; const EXPIRATION_TIME: OffsetDateTime = @@ -693,7 +691,7 @@ mod tests { admin_key: Some(admin_key().to_protobuf()), proxy_account_id: Some(PROXY_ACCOUNT_ID.to_protobuf()), auto_renew_period: Some(AUTO_RENEW_PERIOD.to_protobuf()), - max_automatic_token_associations: Some(MAX_AUTOMATIC_TOKEN_ASSOCIATIONS as _), + max_automatic_token_associations: Some(MAX_AUTOMATIC_TOKEN_ASSOCIATIONS), auto_renew_account_id: Some(AUTO_RENEW_ACCOUNT_ID.to_protobuf()), decline_reward: None, memo_field: Some(services::contract_update_transaction_body::MemoField::MemoWrapper( diff --git a/tests/e2e/account/create.rs b/tests/e2e/account/create.rs index 42810955..47e739b1 100644 --- a/tests/e2e/account/create.rs +++ b/tests/e2e/account/create.rs @@ -389,3 +389,29 @@ async fn alias_with_receiver_sig_required_missing_signature_fails() -> anyhow::R Ok(()) } + +#[tokio::test] +async fn cannot_create_account_with_invalid_negative_max_auto_token_assocation( +) -> anyhow::Result<()> { + let Some(TestEnvironment { config: _, client }) = setup_nonfree() else { + return Ok(()); + }; + + let key = PrivateKey::generate_ed25519(); + + let res = AccountCreateTransaction::new() + .key(key.public_key()) + .max_automatic_token_associations(-2) + .execute(&client) + .await; + + assert_matches::assert_matches!( + res, + Err(hedera::Error::TransactionPreCheckStatus { + status: hedera::Status::InvalidMaxAutoAssociations, + .. + }) + ); + + Ok(()) +} diff --git a/tests/e2e/account/update.rs b/tests/e2e/account/update.rs index 2c0e6e2c..4e223e09 100644 --- a/tests/e2e/account/update.rs +++ b/tests/e2e/account/update.rs @@ -5,6 +5,8 @@ use hedera::{ Hbar, Key, PrivateKey, + TokenCreateTransaction, + TransferTransaction, }; use time::Duration; @@ -84,3 +86,67 @@ async fn missing_account_id_fails() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn cannot_update_max_token_association_to_lower_value_fails() -> anyhow::Result<()> { + let Some(TestEnvironment { config: _, client }) = setup_nonfree() else { + return Ok(()); + }; + + let account_key = PrivateKey::generate_ed25519(); + + // Create account with max token associations of 1 + let account_id = AccountCreateTransaction::new() + .key(account_key.public_key()) + .max_automatic_token_associations(1) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .account_id + .unwrap(); + + // Create token + let token_id = TokenCreateTransaction::new() + .name("ffff") + .symbol("F") + .initial_supply(100_000) + .treasury_account_id(client.get_operator_account_id().unwrap()) + .admin_key(client.get_operator_public_key().unwrap()) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .token_id + .unwrap(); + + // Associate token with account + _ = TransferTransaction::new() + .token_transfer(token_id, client.get_operator_account_id().unwrap(), -10) + .token_transfer(token_id, account_id, 10) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + // Update account max token associations to 0 + let res = AccountUpdateTransaction::new() + .account_id(account_id) + .max_automatic_token_associations(0) + .freeze_with(&client)? + .sign(account_key) + .execute(&client) + .await? + .get_receipt(&client) + .await; + + assert_matches::assert_matches!( + res, + Err(hedera::Error::ReceiptStatus { + status: hedera::Status::ExistingAutomaticAssociationsExceedGivenLimit, + .. + }) + ); + + Ok(()) +} diff --git a/tests/e2e/token/transfer.rs b/tests/e2e/token/transfer.rs index 54354d0c..45e59f74 100644 --- a/tests/e2e/token/transfer.rs +++ b/tests/e2e/token/transfer.rs @@ -1,8 +1,10 @@ use assert_matches::assert_matches; use hedera::{ + AccountCreateTransaction, FixedFee, FixedFeeData, Hbar, + PrivateKey, Status, TokenAssociateTransaction, TokenCreateTransaction, @@ -329,3 +331,75 @@ async fn incorrect_decimals_fails() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn transfer_to_account_with_unlimited_associations() -> anyhow::Result<()> { + let Some(TestEnvironment { config: _, client }) = setup_nonfree() else { + return Ok(()); + }; + + let sender_key = PrivateKey::generate_ed25519(); + let receiver_key = PrivateKey::generate_ed25519(); + + let token_id = TokenCreateTransaction::new() + .name("ffff") + .symbol("F") + .initial_supply(100_000) + .treasury_account_id(client.get_operator_account_id().unwrap()) + .admin_key(client.get_operator_public_key().unwrap()) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .token_id + .unwrap(); + + let sender_id = AccountCreateTransaction::new() + .key(sender_key.public_key()) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .account_id + .unwrap(); + + let receiver_id = AccountCreateTransaction::new() + .key(receiver_key.public_key()) + .max_automatic_token_associations(-1) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .account_id + .unwrap(); + + _ = TokenAssociateTransaction::new() + .account_id(sender_id) + .token_ids([token_id]) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + _ = TransferTransaction::new() + .token_transfer(token_id, client.get_operator_account_id().unwrap(), -10) + .token_transfer(token_id, sender_id, 10) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + _ = TransferTransaction::new() + .token_transfer(token_id, sender_id, -10) + .token_transfer(token_id, receiver_id, 10) + .freeze_with(&client)? + .sign(sender_key) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + Ok(()) +}