Skip to content

Commit

Permalink
Automatically set transaction capability flags in ISA (#2075)
Browse files Browse the repository at this point in the history
* Automatically set transaction capability flags in ISA

* use initial mana excess when setting flag

* fmt and unused

* python: Optional mana

* burn generated mana

* docs

* add check

* cleaning

* 😤

* slightly improve calls

---------

Co-authored-by: Thibault Martinez <[email protected]>
  • Loading branch information
DaughterOfMars and thibault-martinez authored Mar 1, 2024
1 parent f84b5a0 commit d2dc74d
Show file tree
Hide file tree
Showing 19 changed files with 644 additions and 98 deletions.
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() {
// 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)?
{
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;
}

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> {
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()) {
self.transaction_capabilities
.add_capability(TransactionCapabilityFlag::BurnNativeTokens);
output_native_tokens.merge(NativeTokensBuilder::from(burn.native_tokens.clone()))?;
}

Expand Down
Loading

0 comments on commit d2dc74d

Please sign in to comment.