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

Automatically set transaction capability flags in ISA #2075

Merged
merged 22 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from 21 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
4 changes: 4 additions & 0 deletions bindings/nodejs/lib/types/client/burn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { AccountId, FoundryId, NftId, TokenId } from '../block/id';

/** A DTO for [`Burn`] */
export interface Burn {
/** Burn initial excess mana (only from inputs/outputs that have been specified manually) */
mana?: boolean;
/** Burn generated mana */
generatedMana?: boolean;
/** Accounts to burn */
accounts?: AccountId[];
/** NFTs to burn */
Expand Down
2 changes: 0 additions & 2 deletions bindings/nodejs/lib/types/wallet/transaction-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ export interface TransactionOptions {
allowMicroAmount?: boolean;
/** Whether to allow the selection of additional inputs for this transaction. */
allowAdditionalInputSelection?: boolean;
/** Transaction capabilities. */
capabilities?: HexEncodedString;
/** Mana allotments for the transaction. */
manaAllotments?: { [account_id: AccountId]: u64 };
/** Optional block issuer to which the transaction will have required mana allotted. */
Expand Down
16 changes: 16 additions & 0 deletions bindings/python/iota_sdk/types/burn.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,33 @@ class Burn:
"""A DTO for `Burn`.

Attributes:
mana: Whether initial excess mana should be burned (only from inputs/outputs that have been specified manually).
generated_mana: Whether generated mana should be burned.
accounts: The accounts to burn.
nfts: The NFTs to burn.
foundries: The foundries to burn.
native_tokens: The native tokens to burn.
"""

mana: Optional[bool] = None
generated_mana: Optional[bool] = None
accounts: Optional[List[HexStr]] = None
nfts: Optional[List[HexStr]] = None
foundries: Optional[List[HexStr]] = None
native_tokens: Optional[List[NativeToken]] = None

def set_mana(self, burn_mana: bool) -> Burn:
"""Burn excess initial mana (only from inputs/outputs that have been specified manually).
"""
self.mana = burn_mana
return self

def set_generated_mana(self, burn_generated_mana: bool) -> Burn:
"""Burn generated mana.
"""
self.generated_mana = burn_generated_mana
return self

def add_account(self, account: HexStr) -> Burn:
"""Add an account to the burn.
"""
Expand Down
2 changes: 0 additions & 2 deletions bindings/python/iota_sdk/types/transaction_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ class TransactionOptions:
note: A string attached to the transaction.
allow_micro_amount: Whether to allow sending a micro amount.
allow_additional_input_selection: Whether to allow the selection of additional inputs for this transaction.
capabilities: Transaction capabilities.
mana_allotments: Mana allotments for the transaction.
issuer_id: Optional block issuer to which the transaction will have required mana allotted.
"""
Expand All @@ -68,6 +67,5 @@ class TransactionOptions:
note: Optional[str] = None
allow_micro_amount: Optional[bool] = None
allow_additional_input_selection: Optional[bool] = None
capabilities: Optional[HexStr] = None
mana_allotments: Optional[dict[HexStr, int]] = None
issuer_id: Optional[HexStr] = None
28 changes: 28 additions & 0 deletions sdk/src/client/api/block_builder/input_selection/burn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ use crate::types::block::output::{AccountId, DelegationId, FoundryId, NativeToke
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Burn {
// Whether initial excess mana should be burned (only from inputs/outputs that have been specified manually).
#[serde(default)]
pub(crate) mana: bool,
// Whether generated mana should be burned.
#[serde(default)]
pub(crate) generated_mana: bool,
/// Accounts to burn.
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
pub(crate) accounts: HashSet<AccountId>,
Expand All @@ -37,6 +43,28 @@ impl Burn {
Self::default()
}

/// Sets the flag to [`Burn`] initial excess mana.
pub fn set_mana(mut self, burn_mana: bool) -> Self {
self.mana = burn_mana;
self
}

/// Returns whether to [`Burn`] mana.
pub fn mana(&self) -> bool {
self.mana
}

/// Sets the flag to [`Burn`] generated mana.
pub fn set_generated_mana(mut self, burn_generated_mana: bool) -> Self {
self.generated_mana = burn_generated_mana;
self
}

/// Returns whether to [`Burn`] generated mana.
pub fn generated_mana(&self) -> bool {
self.generated_mana
}

/// Adds an account to [`Burn`].
pub fn add_account(mut self, account_id: AccountId) -> Self {
self.accounts.insert(account_id);
Expand Down
29 changes: 15 additions & 14 deletions sdk/src/client/api/block_builder/input_selection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::{
NativeTokensBuilder, NftOutput, NftOutputBuilder, Output, OutputId, OUTPUT_COUNT_RANGE,
},
payload::{
signed_transaction::{Transaction, TransactionCapabilities},
signed_transaction::{Transaction, TransactionCapabilities, TransactionCapabilityFlag},
TaggedDataPayload,
},
protocol::{CommittableAgeRange, ProtocolParameters},
Expand Down Expand Up @@ -160,10 +160,7 @@ impl InputSelection {
self.available_inputs
.retain(|input| !self.forbidden_inputs.contains(input.output_id()));

// This is to avoid a borrow of self since there is a mutable borrow in the loop already.
let required_inputs = std::mem::take(&mut self.required_inputs);

for required_input in required_inputs {
for required_input in self.required_inputs.clone() {
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved
// Checks that required input is not forbidden.
if self.forbidden_inputs.contains(&required_input) {
return Err(Error::RequiredInputIsForbidden(required_input));
Expand Down Expand Up @@ -270,6 +267,19 @@ impl InputSelection {
}
}

// If we're burning generated mana, set the capability flag.
if self.burn.as_ref().map_or(false, |b| b.generated_mana()) {
// Get the mana sums with generated mana to see whether there's a difference.
if !self
.transaction_capabilities
.has_capability(TransactionCapabilityFlag::BurnMana)
&& input_mana < self.total_selected_mana(true)?
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved
{
self.transaction_capabilities
.add_capability(TransactionCapabilityFlag::BurnMana);
}
}

let outputs = self
.provided_outputs
.into_iter()
Expand Down Expand Up @@ -432,15 +442,6 @@ impl InputSelection {
self
}

/// Sets the transaction capabilities.
pub fn with_transaction_capabilities(
mut self,
transaction_capabilities: impl Into<TransactionCapabilities>,
) -> Self {
self.transaction_capabilities = transaction_capabilities.into();
self
}

pub(crate) fn all_outputs(&self) -> impl Iterator<Item = &Output> {
self.non_remainder_outputs().chain(self.remainder_outputs())
}
Expand Down
29 changes: 17 additions & 12 deletions sdk/src/client/api/block_builder/input_selection/remainder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,27 +124,27 @@ impl InputSelection {

let (input_mana, output_mana) = self.mana_sums(false)?;

if input_amount == output_amount && input_mana == output_mana && native_tokens_diff.is_none() {
log::debug!("No remainder required");
return Ok((storage_deposit_returns, Vec::new()));
}

let amount_diff = input_amount.checked_sub(output_amount).expect("amount underflow");
let mut mana_diff = input_mana.checked_sub(output_mana).expect("mana underflow");

// If we are burning mana, then we can subtract out the burned amount.
if self.burn.as_ref().map_or(false, |b| b.mana()) {
mana_diff = mana_diff.saturating_sub(self.initial_mana_excess()?);
}

let (remainder_address, chain) = self
.get_remainder_address()?
.ok_or(Error::MissingInputWithEd25519Address)?;

// If there is a mana remainder, try to fit it in an existing output
if input_mana > output_mana && self.output_for_added_mana_exists(&remainder_address) {
if mana_diff > 0 && self.output_for_added_mana_exists(&remainder_address) {
log::debug!("Allocating {mana_diff} excess input mana for output with address {remainder_address}");
self.remainders.added_mana = std::mem::take(&mut mana_diff);
// If we have no other remainders, we are done
if input_amount == output_amount && native_tokens_diff.is_none() {
log::debug!("No more remainder required");
return Ok((storage_deposit_returns, Vec::new()));
}
}

if input_amount == output_amount && mana_diff == 0 && native_tokens_diff.is_none() {
log::debug!("No remainder required");
return Ok((storage_deposit_returns, Vec::new()));
}

let remainder_outputs = create_remainder_outputs(
Expand Down Expand Up @@ -232,10 +232,15 @@ impl InputSelection {
let remainder_address = self.get_remainder_address()?.map(|v| v.0);

// Mana can potentially be added to an appropriate existing output instead of a new remainder output
let mana_remainder = selected_mana > required_mana
let mut mana_remainder = selected_mana > required_mana
&& remainder_address.map_or(true, |remainder_address| {
!self.output_for_added_mana_exists(&remainder_address)
});
// If we are burning mana, we may not need a mana remainder
if self.burn.as_ref().map_or(false, |b| b.mana()) {
let initial_excess = self.initial_mana_excess()?;
mana_remainder &= selected_mana > required_mana + initial_excess;
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved
}

Ok((remainder_amount, native_tokens_remainder, mana_remainder))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,10 @@ impl InputSelection {
if !self.allow_additional_input_selection {
return Err(Error::AdditionalInputsRequired(Requirement::Mana));
}
let include_generated = self.burn.as_ref().map_or(true, |b| !b.generated_mana());
// TODO we should do as for the amount and have preferences on which inputs to pick.
while let Some(input) = self.available_inputs.pop() {
selected_mana += self.total_mana(&input)?;
selected_mana += self.total_mana(&input, include_generated)?;
if let Some(output) = self.select_input(input)? {
required_mana += output.mana();
}
Expand All @@ -259,6 +260,22 @@ impl InputSelection {
Ok(added_inputs)
}

pub(crate) fn initial_mana_excess(&self) -> Result<u64, Error> {
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
let output_mana = self.provided_outputs.iter().map(|o| o.mana()).sum::<u64>();
let mut input_mana = 0;
let include_generated = self.burn.as_ref().map_or(true, |b| !b.generated_mana());

for input in self
.selected_inputs
.iter()
.filter(|i| self.required_inputs.contains(i.output_id()))
{
input_mana += self.total_mana(input, include_generated)?;
}

Ok(input_mana.saturating_sub(output_mana))
}

pub(crate) fn mana_sums(&self, include_remainders: bool) -> Result<(u64, u64), Error> {
let mut required_mana =
self.non_remainder_outputs().map(|o| o.mana()).sum::<u64>() + self.mana_allotments.values().sum::<u64>();
Expand All @@ -268,20 +285,32 @@ impl InputSelection {
required_mana += self.remainder_outputs().map(|o| o.mana()).sum::<u64>() + self.remainders.added_mana;
}

Ok((self.total_selected_mana(None)?, required_mana))
}

pub(crate) fn total_selected_mana(&self, include_generated: impl Into<Option<bool>> + Copy) -> Result<u64, Error> {
let mut selected_mana = 0;
let include_generated = include_generated
.into()
.unwrap_or(self.burn.as_ref().map_or(true, |b| !b.generated_mana()));

for input in &self.selected_inputs {
selected_mana += self.total_mana(input)?;
selected_mana += self.total_mana(input, include_generated)?;
}
Ok((selected_mana, required_mana))

Ok(selected_mana)
}

fn total_mana(&self, input: &InputSigningData) -> Result<u64, Error> {
fn total_mana(&self, input: &InputSigningData, include_generated: bool) -> Result<u64, Error> {
Ok(self.mana_rewards.get(input.output_id()).copied().unwrap_or_default()
+ input.output.available_mana(
&self.protocol_parameters,
input.output_id().transaction_id().slot_index(),
self.creation_slot,
)?)
+ if include_generated {
input.output.available_mana(
&self.protocol_parameters,
input.output_id().transaction_id().slot_index(),
self.creation_slot,
)?
} else {
input.output.mana()
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::{
types::block::{
address::Address,
output::{AccountId, ChainId, DelegationId, Features, FoundryId, NftId, Output},
payload::signed_transaction::TransactionCapabilityFlag,
},
};

Expand Down Expand Up @@ -163,6 +164,11 @@ impl InputSelection {
/// Gets requirements from burn.
pub(crate) fn burn_requirements(&mut self) -> Result<(), Error> {
if let Some(burn) = self.burn.as_ref() {
if burn.mana() && self.initial_mana_excess()? > 0 {
self.transaction_capabilities
.add_capability(TransactionCapabilityFlag::BurnMana);
}

for account_id in &burn.accounts {
if self
.non_remainder_outputs()
Expand All @@ -174,6 +180,8 @@ impl InputSelection {
let requirement = Requirement::Account(*account_id);
log::debug!("Adding {requirement:?} from burn");
self.requirements.push(requirement);
self.transaction_capabilities
.add_capability(TransactionCapabilityFlag::DestroyAccountOutputs);
}

for foundry_id in &burn.foundries {
Expand All @@ -187,6 +195,8 @@ impl InputSelection {
let requirement = Requirement::Foundry(*foundry_id);
log::debug!("Adding {requirement:?} from burn");
self.requirements.push(requirement);
self.transaction_capabilities
.add_capability(TransactionCapabilityFlag::DestroyFoundryOutputs);
}

for nft_id in &burn.nfts {
Expand All @@ -200,6 +210,8 @@ impl InputSelection {
let requirement = Requirement::Nft(*nft_id);
log::debug!("Adding {requirement:?} from burn");
self.requirements.push(requirement);
self.transaction_capabilities
.add_capability(TransactionCapabilityFlag::DestroyNftOutputs);
}

for delegation_id in &burn.delegations {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ use primitive_types::U256;
use super::{Error, InputSelection};
use crate::{
client::secret::types::InputSigningData,
types::block::output::{NativeToken, NativeTokens, NativeTokensBuilder, Output, TokenScheme},
types::block::{
output::{NativeToken, NativeTokens, NativeTokensBuilder, Output, TokenScheme},
payload::signed_transaction::TransactionCapabilityFlag,
},
};

pub(crate) fn get_native_tokens<'a>(outputs: impl Iterator<Item = &'a Output>) -> Result<NativeTokensBuilder, Error> {
Expand Down Expand Up @@ -59,7 +62,9 @@ impl InputSelection {
input_native_tokens.merge(minted_native_tokens)?;
output_native_tokens.merge(melted_native_tokens)?;

if let Some(burn) = self.burn.as_ref() {
if let Some(burn) = self.burn.as_ref().filter(|burn| !burn.native_tokens.is_empty()) {
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved
self.transaction_capabilities
.add_capability(TransactionCapabilityFlag::BurnNativeTokens);
output_native_tokens.merge(NativeTokensBuilder::from(burn.native_tokens.clone()))?;
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down
Loading
Loading