Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Balance refactoring part 1 #1085

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Stable release.

- `StrongholdAdapterBuilder` updated to be slightly more ergonomic;
- `Wallet::{set_stronghold_password, change_stronghold_password, set_stronghold_password_clear_interval, store_mnemonic}` return an `Err` instead of `Ok` in case of a non-stronghold secret manager;
- Balance computation internal refactoring;

## 1.0.4 - 2023-MM-DD

Expand Down
253 changes: 95 additions & 158 deletions sdk/src/wallet/account/operations/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use crate::{
client::secret::SecretManage,
types::block::{
address::Bech32Address,
output::{unlock_condition::UnlockCondition, FoundryId, NativeTokensBuilder, Output, Rent},
output::{
unlock_condition::{UnlockCondition, UnlockConditions},
FoundryId, NativeTokensBuilder, Output, Rent,
},
ConvertTo,
},
wallet::{
Expand Down Expand Up @@ -89,178 +92,112 @@ where

let output = &data.output;
let rent = output.rent_cost(&rent_structure);
let mut output_balance = Balance::default();

output_balance.base_coin.total += output.amount();

// Add alias and foundry outputs here because they can't have a
// [`StorageDepositReturnUnlockCondition`] or time related unlock conditions
match output {
Output::Basic(_) => {
output_balance.required_storage_deposit.basic += rent;
}
Output::Alias(output) => {
// Add amount
balance.base_coin.total += output.amount();
// Add storage deposit
balance.required_storage_deposit.alias += rent;
if !account_details.locked_outputs.contains(output_id) {
total_rent_amount += rent;
}
// Add native tokens
total_native_tokens.add_native_tokens(output.native_tokens().clone())?;

let alias_id = output.alias_id_non_null(output_id);
balance.aliases.push(alias_id);
output_balance.required_storage_deposit.alias += rent;
output_balance.aliases.push(output.alias_id_non_null(output_id));
}
Output::Foundry(output) => {
// Add amount
balance.base_coin.total += output.amount();
// Add storage deposit
balance.required_storage_deposit.foundry += rent;
if !account_details.locked_outputs.contains(output_id) {
output_balance.required_storage_deposit.foundry += rent;
output_balance.foundries.push(output.id());
}
Output::Nft(output) => {
output_balance.required_storage_deposit.nft += rent;
output_balance.nfts.push(output.nft_id_non_null(output_id));
}
_ => {}
}

if !account_details.locked_outputs.contains(output_id) {
if output.is_basic() {
// Amount for basic outputs isn't added to total_rent_amount if there aren't native tokens,
// since we can spend it without burning.
if output
.native_tokens()
.map(|native_tokens| !native_tokens.is_empty())
.unwrap_or(false)
{
total_rent_amount += rent;
}
// Add native tokens
total_native_tokens.add_native_tokens(output.native_tokens().clone())?;

balance.foundries.push(output.id());
} else {
total_rent_amount += rent;
}
_ => {
// If there is only an [AddressUnlockCondition], then we can spend the output at any time
// without restrictions
if let [UnlockCondition::Address(_)] = output
.unlock_conditions()
.expect("output needs to have unlock conditions")
.as_ref()
{
// add nft_id for nft outputs
if let Output::Nft(output) = &output {
let nft_id = output.nft_id_non_null(output_id);
balance.nfts.push(nft_id);
}
}

if let Some(native_tokens) = output.native_tokens() {
total_native_tokens.add_native_tokens(native_tokens.clone())?;
}
Comment on lines +134 to +136
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So where do you check if we can actually unlock these?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah you're right. I wanted to split more changes to later but it won't work without it.


// Add amount
balance.base_coin.total += output.amount();

// Add storage deposit
if output.is_basic() {
balance.required_storage_deposit.basic += rent;
if output
.native_tokens()
.map(|native_tokens| !native_tokens.is_empty())
.unwrap_or(false)
&& !account_details.locked_outputs.contains(output_id)
// If there is only an [AddressUnlockCondition], then we can spend the output at any time
// without restrictions
if let [UnlockCondition::Address(_)] = output
.unlock_conditions()
.expect("output needs to have unlock conditions")
.as_ref()
{
balance += output_balance;
} else {
// if we have multiple unlock conditions for basic or nft outputs, then we might can't
// spend the balance at the moment or in the future

let account_addresses = self.addresses().await?;
let local_time = self.client().get_time_checked().await?;
let is_claimable = self.claimable_outputs(OutputsToClaim::All).await?.contains(output_id);

// For outputs that are expired or have a timelock unlock condition, but no expiration
// unlock condition and we then can unlock them, then
// they can never be not available for us anymore
// and should be added to the balance
if is_claimable {
// check if output can be unlocked always from now on, in that case it should be
// added to the total amount
let output_can_be_unlocked_now_and_in_future = can_output_be_unlocked_forever_from_now_on(
// We use the addresses with unspent outputs, because other addresses of
// the account without unspent
// outputs can't be related to this output
&account_details.addresses_with_unspent_outputs,
output,
local_time,
);

if output_can_be_unlocked_now_and_in_future {
// If output has a StorageDepositReturnUnlockCondition, the amount of it should be
// subtracted, because this part needs to be sent back.
if let Some(sdr) = output
.unlock_conditions()
.and_then(UnlockConditions::storage_deposit_return)
{
// Sending to someone else
if !account_addresses
.iter()
.any(|a| a.address.inner == *sdr.return_address())
{
total_rent_amount += rent;
}
} else if output.is_nft() {
balance.required_storage_deposit.nft += rent;
if !account_details.locked_outputs.contains(output_id) {
total_rent_amount += rent;
output_balance.base_coin.total -= sdr.amount();
}
}

// Add native tokens
if let Some(native_tokens) = output.native_tokens() {
total_native_tokens.add_native_tokens(native_tokens.clone())?;
}
balance += output_balance;
} else {
// if we have multiple unlock conditions for basic or nft outputs, then we might can't
// spend the balance at the moment or in the future

let account_addresses = self.addresses().await?;
let local_time = self.client().get_time_checked().await?;
let is_claimable =
self.claimable_outputs(OutputsToClaim::All).await?.contains(output_id);

// For outputs that are expired or have a timelock unlock condition, but no expiration
// unlock condition and we then can unlock them, then
// they can never be not available for us anymore
// and should be added to the balance
if is_claimable {
// check if output can be unlocked always from now on, in that case it should be
// added to the total amount
let output_can_be_unlocked_now_and_in_future =
can_output_be_unlocked_forever_from_now_on(
// We use the addresses with unspent outputs, because other addresses of
// the account without unspent
// outputs can't be related to this output
&account_details.addresses_with_unspent_outputs,
output,
local_time,
);

if output_can_be_unlocked_now_and_in_future {
// If output has a StorageDepositReturnUnlockCondition, the amount of it should
// be subtracted, because this part
// needs to be sent back
let amount = output
.unlock_conditions()
.and_then(|u| u.storage_deposit_return())
.map_or_else(
|| output.amount(),
|sdr| {
if account_addresses
.iter()
.any(|a| a.address.inner == *sdr.return_address())
{
// sending to ourself, we get the full amount
output.amount()
} else {
// Sending to someone else
output.amount() - sdr.amount()
}
},
);

// add nft_id for nft outputs
if let Output::Nft(output) = &output {
let nft_id = output.nft_id_non_null(output_id);
balance.nfts.push(nft_id);
}

// Add amount
balance.base_coin.total += amount;

// Add storage deposit
if output.is_basic() {
balance.required_storage_deposit.basic += rent;
// Amount for basic outputs isn't added to total_rent_amount if there aren't
// native tokens, since we can
// spend it without burning.
if output
.native_tokens()
.map(|native_tokens| !native_tokens.is_empty())
.unwrap_or(false)
&& !account_details.locked_outputs.contains(output_id)
{
total_rent_amount += rent;
}
} else if output.is_nft() {
balance.required_storage_deposit.nft += rent;
if !account_details.locked_outputs.contains(output_id) {
total_rent_amount += rent;
}
}

// Add native tokens
if let Some(native_tokens) = output.native_tokens() {
total_native_tokens.add_native_tokens(native_tokens.clone())?;
}
} else {
// only add outputs that can't be locked now and at any point in the future
balance.potentially_locked_outputs.insert(*output_id, true);
}
} else {
// Don't add expired outputs that can't ever be unlocked by us
if let Some(expiration) = output
.unlock_conditions()
.expect("output needs to have unlock conditions")
.expiration()
{
// Not expired, could get unlockable when it's expired, so we insert it
if local_time < expiration.timestamp() {
balance.potentially_locked_outputs.insert(*output_id, false);
}
} else {
balance.potentially_locked_outputs.insert(*output_id, false);
}
// only add outputs that can't be locked now and at any point in the future
balance.potentially_locked_outputs.insert(*output_id, true);
}
} else {
// Don't add expired outputs that can't ever be unlocked by us
if let Some(expiration) = output.unlock_conditions().and_then(UnlockConditions::expiration)
{
// Not expired, could get unlockable when it's expired, so we insert it
if local_time < expiration.timestamp() {
balance.potentially_locked_outputs.insert(*output_id, false);
}
} else {
balance.potentially_locked_outputs.insert(*output_id, false);
}
}
}
Expand Down
Loading