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

Split provided outputs in ISA #2089

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 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
5 changes: 4 additions & 1 deletion bindings/core/src/method/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,10 @@ pub enum WalletMethod {
/// Prepare transaction.
/// Expected response: [`PreparedTransaction`](crate::Response::PreparedTransaction)
PrepareTransaction {
DaughterOfMars marked this conversation as resolved.
Show resolved Hide resolved
outputs: Vec<Output>,
#[serde(default)]
immutable_outputs: Vec<Output>,
#[serde(default)]
mutable_outputs: Vec<Output>,
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
#[serde(default)]
options: Option<TransactionOptions>,
},
Expand Down
10 changes: 8 additions & 2 deletions bindings/core/src/method_handler/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,14 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM
// let data = wallet.prepare_stop_participating(event_id).await?;
// Response::PreparedTransaction(data)
// }
WalletMethod::PrepareTransaction { outputs, options } => {
let data = wallet.prepare_transaction(outputs, options).await?;
WalletMethod::PrepareTransaction {
immutable_outputs,
mutable_outputs,
options,
} => {
let data = wallet
.prepare_transaction(mutable_outputs, immutable_outputs, options)
.await?;
Response::PreparedTransaction(data)
}
// #[cfg(feature = "participation")]
Expand Down
3 changes: 2 additions & 1 deletion bindings/nodejs/lib/types/wallet/bridge/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,8 @@ export type __PrepareSendMethod__ = {
export type __PrepareTransactionMethod__ = {
name: 'prepareTransaction';
data: {
outputs: Output[];
immutableOutputs?: Output[];
mutableOutputs?: Output[];
options?: TransactionOptions;
};
};
Expand Down
23 changes: 17 additions & 6 deletions bindings/nodejs/lib/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1465,32 +1465,43 @@ export class Wallet {
/**
* Send a transaction.
*
* @param outputs Outputs to use in the transaction.
* @param immutableOutputs Outputs to use in the transaction that cannot be changed in any way.
* @param mutableOutputs Outputs to use in the transaction that may have amount/mana changes made to fulfill requirements.
* @param options Additional transaction options.
* @returns The transaction data.
*/
async sendTransaction(
outputs: Output[],
immutableOutputs?: Output[],
mutableOutputs?: Output[],
options?: TransactionOptions,
): Promise<TransactionWithMetadata> {
return (await this.prepareTransaction(outputs, options)).send();
return (
await this.prepareTransaction(
immutableOutputs,
mutableOutputs,
options,
)
).send();
}

/**
* Prepare a transaction, useful for offline signing.
*
* @param outputs Outputs to use in the transaction.
* @param immutableOutputs Outputs to use in the transaction that cannot be changed in any way.
* @param mutableOutputs Outputs to use in the transaction that may have amount/mana changes made to fulfill requirements.
* @param options Additional transaction options.
* @returns The prepared transaction data.
*/
async prepareTransaction(
outputs: Output[],
immutableOutputs?: Output[],
mutableOutputs?: Output[],
options?: TransactionOptions,
): Promise<PreparedTransaction> {
const response = await this.methodHandler.callMethod({
name: 'prepareTransaction',
data: {
outputs,
immutableOutputs,
mutableOutputs,
options,
},
});
Expand Down
11 changes: 7 additions & 4 deletions bindings/python/iota_sdk/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,18 +702,21 @@ def announce_candidacy(self, account_id: HexStr) -> BlockId:
))

def send_transaction(
self, outputs: List[Output], options: Optional[TransactionOptions] = None) -> TransactionWithMetadata:
self, immutable_outputs: Optional[List[Output]] = None, mutable_outputs: Optional[List[Output]] = None, options: Optional[TransactionOptions] = None) -> TransactionWithMetadata:
"""Send a transaction.
"""
return self.prepare_transaction(outputs, options).send()
return self.prepare_transaction(immutable_outputs, mutable_outputs, options).send()

def prepare_transaction(
self, outputs: List[Output], options: Optional[TransactionOptions] = None) -> PreparedTransaction:
self, immutable_outputs: Optional[List[Output]] = None, mutable_outputs: Optional[List[Output]] = None, options: Optional[TransactionOptions] = None) -> PreparedTransaction:
"""Prepare transaction.
Immutable outputs are not allowed to be changed in any way.
Mutable outputs may have amount/mana changes made to fulfill requirements.
Copy link
Member

@Thoralf-M Thoralf-M Mar 1, 2024

Choose a reason for hiding this comment

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

Mutable outputs may have amount/mana changes made to fulfill requirements.

The amount can only increase and this can only happen if the address is controlled by the sender, right?

Copy link
Author

Choose a reason for hiding this comment

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

no, the amount of chains can also be reduced

Copy link
Author

Choose a reason for hiding this comment

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

well actually not 100% sure the conditions for that reduction so maybe

Copy link
Member

Choose a reason for hiding this comment

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

Would be good to have this cleared up (unless we go another route without the mutable outputs)

"""
prepared = PreparedTransactionData.from_dict(self._call_method(
'prepareTransaction', {
'outputs': outputs,
'immutableOutputs': immutable_outputs,
'mutableOutputs': mutable_outputs,
'options': options
}
))
Expand Down
38 changes: 25 additions & 13 deletions sdk/src/client/api/block_builder/input_selection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ pub struct InputSelection {
forbidden_inputs: HashSet<OutputId>,
selected_inputs: Vec<InputSigningData>,
context_inputs: HashSet<ContextInput>,
provided_outputs: Vec<Output>,
added_outputs: Vec<Output>,
immutable_outputs: Vec<Output>,
mutable_outputs: Vec<Output>,
addresses: HashSet<Address>,
burn: Option<Burn>,
remainders: Remainders,
Expand Down Expand Up @@ -85,7 +85,6 @@ impl InputSelection {
/// Creates a new [`InputSelection`].
pub fn new(
available_inputs: impl IntoIterator<Item = InputSigningData>,
outputs: impl IntoIterator<Item = Output>,
addresses: impl IntoIterator<Item = Address>,
creation_slot_index: impl Into<SlotIndex>,
latest_slot_commitment_id: SlotCommitmentId,
Expand Down Expand Up @@ -119,8 +118,8 @@ impl InputSelection {
forbidden_inputs: HashSet::new(),
selected_inputs: Vec::new(),
context_inputs: HashSet::new(),
provided_outputs: outputs.into_iter().collect(),
added_outputs: Vec::new(),
immutable_outputs: Vec::new(),
mutable_outputs: Vec::new(),
addresses,
burn: None,
remainders: Default::default(),
Expand Down Expand Up @@ -199,13 +198,14 @@ impl InputSelection {
/// Selects inputs that meet the requirements of the outputs to satisfy the semantic validation of the overall
/// transaction. Also creates a remainder output and chain transition outputs if required.
pub fn select(mut self) -> Result<PreparedTransactionData, Error> {
if !OUTPUT_COUNT_RANGE.contains(&(self.provided_outputs.len() as u16)) {
let num_provided_outputs = self.immutable_outputs.len() + self.mutable_outputs.len();
if !OUTPUT_COUNT_RANGE.contains(&(num_provided_outputs as u16)) {
// If burn or mana allotments are provided, outputs will be added later, in the other cases it will just
// create remainder outputs.
if !self.provided_outputs.is_empty()
if num_provided_outputs > 0
|| (self.burn.is_none() && self.mana_allotments.is_empty() && self.required_inputs.is_empty())
{
return Err(Error::InvalidOutputCount(self.provided_outputs.len()));
return Err(Error::InvalidOutputCount(num_provided_outputs));
}
}

Expand Down Expand Up @@ -271,9 +271,9 @@ impl InputSelection {
}

let outputs = self
.provided_outputs
.immutable_outputs
.into_iter()
.chain(self.added_outputs)
.chain(self.mutable_outputs)
.chain(self.remainders.storage_deposit_returns)
.chain(self.remainders.data.iter().map(|r| r.output.clone()))
.collect::<Vec<_>>();
Expand Down Expand Up @@ -343,8 +343,8 @@ impl InputSelection {
// - the issuer feature doesn't need to be verified as the chain is not new
// - input doesn't need to be checked for as we just transitioned it
// - foundry account requirement should have been met already by a prior `required_account_nft_addresses`
self.added_outputs.push(output);
added_output = self.added_outputs.last();
self.mutable_outputs.push(output);
added_output = self.mutable_outputs.last();
}

if let Some(requirement) = self.required_account_nft_addresses(&input)? {
Expand All @@ -362,6 +362,18 @@ impl InputSelection {
Ok(added_output)
}

/// Sets outputs that can be mutated by [`InputSelection`].
pub fn with_mutable_outputs(mut self, outputs: impl IntoIterator<Item = Output>) -> Self {
self.mutable_outputs = outputs.into_iter().collect();
self
}

/// Sets outputs that cannot be mutated by [`InputSelection`].
pub fn with_immutable_outputs(mut self, outputs: impl IntoIterator<Item = Output>) -> Self {
self.immutable_outputs = outputs.into_iter().collect();
self
}

/// Sets the required inputs of an [`InputSelection`].
pub fn with_required_inputs(mut self, inputs: impl IntoIterator<Item = OutputId>) -> Self {
self.required_inputs = inputs.into_iter().collect();
Expand Down Expand Up @@ -446,7 +458,7 @@ impl InputSelection {
}

pub(crate) fn non_remainder_outputs(&self) -> impl Iterator<Item = &Output> {
self.provided_outputs.iter().chain(&self.added_outputs)
self.immutable_outputs.iter().chain(&self.mutable_outputs)
}

pub(crate) fn remainder_outputs(&self) -> impl Iterator<Item = &Output> {
Expand Down
5 changes: 2 additions & 3 deletions sdk/src/client/api/block_builder/input_selection/remainder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ impl InputSelection {

fn output_for_added_mana_exists(&self, remainder_address: &Address) -> bool {
// Find the first value that matches the remainder address
self.non_remainder_outputs().any(|o| {
self.mutable_outputs.iter().any(|o| {
(o.is_basic() || o.is_account() || o.is_anchor() || o.is_nft())
&& o.unlock_conditions()
.map_or(true, |uc| uc.expiration().is_none() && uc.timelock().is_none())
Expand All @@ -182,9 +182,8 @@ impl InputSelection {
]);
// Remove those that do not have an ordering and sort
let ordered_outputs = self
.provided_outputs
.mutable_outputs
.iter_mut()
.chain(&mut self.added_outputs)
.filter(|o| {
o.unlock_conditions()
.map_or(true, |uc| uc.expiration().is_none() && uc.timelock().is_none())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ impl InputSelection {

fn reduce_funds_of_chains(&mut self, amount_selection: &mut AmountSelection) -> Result<(), Error> {
// Only consider automatically transitioned outputs.
for output in self.added_outputs.iter_mut() {
for output in self.mutable_outputs.iter_mut() {
let diff = amount_selection.missing_amount();
let amount = output.amount();
let minimum_amount = output.minimum_amount(self.protocol_parameters.storage_score_parameters());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ impl InputSelection {
needs_commitment_context = true;
}
}
for output in self
.provided_outputs
.iter_mut()
.chain(&mut self.added_outputs)
.filter(|o| o.is_delegation())
{
if self.non_remainder_outputs().any(|o| o.is_delegation()) {
log::debug!("Adding commitment context input for delegation output");
needs_commitment_context = true;
}
// TODO: move this to the delegation functions
for output in self.mutable_outputs.iter_mut().filter(|o| o.is_delegation()) {
// Created delegations have their start epoch set, and delayed delegations have their end set
if output.as_delegation().delegation_id().is_null() {
let start_epoch = self
Expand All @@ -77,8 +77,6 @@ impl InputSelection {
.with_end_epoch(end_epoch)
.finish_output()?;
}
log::debug!("Adding commitment context input for delegation output");
needs_commitment_context = true;
}
// BlockIssuanceCreditContextInput requires a CommitmentContextInput.
if self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,8 @@ impl InputSelection {
.as_mut()
.ok_or(Error::UnfulfillableRequirement(Requirement::Mana))?;
if let Some(output) = self
.provided_outputs
.mutable_outputs
.iter_mut()
.chain(&mut self.added_outputs)
.filter(|o| o.is_account() && o.mana() != 0)
.find(|o| o.as_account().account_id() == issuer_id)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl InputSelection {
pub(crate) fn outputs_requirements(&mut self) {
let inputs = self.available_inputs.iter().chain(self.selected_inputs.iter());

for output in self.provided_outputs.iter().chain(&self.added_outputs) {
for output in self.immutable_outputs.iter().chain(&self.mutable_outputs) {
let is_created = match output {
// Add an account requirement if the account output is transitioning and then required in the inputs.
Output::Account(account_output) => {
Expand Down
1 change: 1 addition & 0 deletions sdk/src/wallet/operations/output_claiming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ where
self.prepare_transaction(
// We only need to provide the NFT outputs, ISA automatically creates basic outputs as remainder outputs
nft_outputs_to_send,
None,
TransactionOptions {
required_inputs: outputs_to_claim
.iter()
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/wallet/operations/output_consolidation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ where
..Default::default()
});

self.prepare_transaction([], options).await
self.prepare_transaction(None, None, options).await
}

/// Determines whether an output should be consolidated or not.
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/wallet/operations/transaction/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ where
..Default::default()
};

self.prepare_transaction(vec![account], transaction_options.clone())
self.prepare_transaction(vec![account], None, transaction_options.clone())
.await
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ where
*options.mana_allotments.entry(account_id).or_default() += mana;
}

self.prepare_transaction([], options).await
self.prepare_transaction(None, None, options).await
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ where
)?))
.finish_output()?];
// Input selection will detect that we're melting native tokens and add the required inputs if available
self.prepare_transaction(outputs, options).await
self.prepare_transaction(outputs, None, options).await
}

/// Find and return unspent `OutputData` for given `account_id` and `foundry_id`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ impl Wallet {

// The empty list of outputs is used. Outputs will be generated by
// the input selection algorithm based on the content of the [`Burn`] object.
self.prepare_transaction([], options).await
self.prepare_transaction(None, None, options).await
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ where

let outputs = [account_output_builder.finish_output()?];

self.prepare_transaction(outputs, options).await
self.prepare_transaction(outputs, None, options).await
}

/// Gets an existing account output.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ where
.add_unlock_condition(AddressUnlockCondition::new(address))
.finish_output()?;

let transaction = self.prepare_transaction([output], options).await?;
let transaction = self.prepare_transaction([output], None, options).await?;

Ok(PreparedCreateDelegationTransaction {
delegation_id: DelegationId::from(&transaction.transaction.id().into_output_id(0)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,6 @@ where
}
};

self.prepare_transaction(outputs, None).await
self.prepare_transaction(outputs, None, None).await
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ where
}, // Native Tokens will be added automatically in the remainder output in try_select_inputs()
];

self.prepare_transaction(outputs, options)
self.prepare_transaction(outputs, None, options)
.await
.map(|transaction| PreparedCreateNativeTokenTransaction { token_id, transaction })
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,6 @@ where
// Native Tokens will be added automatically in the remainder output in try_select_inputs()
];

self.prepare_transaction(outputs, options).await
self.prepare_transaction(outputs, None, options).await
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,6 @@ where
outputs.push(nft_builder.finish_output()?);
}

self.prepare_transaction(outputs, options).await
self.prepare_transaction(outputs, None, options).await
}
}
Loading
Loading