Skip to content

Commit

Permalink
Better transitions (#2152)
Browse files Browse the repository at this point in the history
* Add some transitions that the transaction builder can do automatically. Attempt to put excess chain mana on the chained output. Disallow editing provided outputs.

* Allow adding remainder amount too

* transition outputs with min amount and zero mana

* Update sdk/src/client/api/block_builder/transaction_builder/error.rs

Co-authored-by: /alex/ <[email protected]>

* review

* while let

* little cleanup

* fix borked merge

* fix transitions and change priority logic to use a scoring system

* fix dumb test

* add score based on number of inputs

* remove allotment amounts from mana gained. Re-add account mana reduction process.

* factor in remainder amounts and rework scoring

* Factor calculated allotment into mana required and fix test

* do not select amount or mana that gains nothing

---------

Co-authored-by: /alex/ <[email protected]>
  • Loading branch information
DaughterOfMars and Alex6323 authored Mar 20, 2024
1 parent b4fca53 commit 76fab4d
Show file tree
Hide file tree
Showing 19 changed files with 1,027 additions and 777 deletions.
5 changes: 4 additions & 1 deletion sdk/src/client/api/block_builder/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use alloc::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};

use crate::{
client::api::transaction_builder::Burn,
client::api::transaction_builder::{transition::Transitions, Burn},
types::block::{
address::Address,
output::{AccountId, OutputId},
Expand All @@ -25,6 +25,8 @@ pub struct TransactionOptions {
pub tagged_data_payload: Option<TaggedDataPayload>,
/// Inputs that must be used for the transaction.
pub required_inputs: BTreeSet<OutputId>,
/// Specifies what needs to be transitioned in the transaction and how.
pub transitions: Option<Transitions>,
/// Specifies what needs to be burned in the transaction.
pub burn: Option<Burn>,
/// A string attached to the transaction.
Expand All @@ -45,6 +47,7 @@ impl Default for TransactionOptions {
remainder_value_strategy: Default::default(),
tagged_data_payload: Default::default(),
required_inputs: Default::default(),
transitions: Default::default(),
burn: Default::default(),
note: Default::default(),
allow_micro_amount: false,
Expand Down
19 changes: 18 additions & 1 deletion sdk/src/client/api/block_builder/transaction_builder/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ use super::Requirement;
use crate::types::block::{
context_input::ContextInputError,
mana::ManaError,
output::{ChainId, NativeTokenError, OutputError, OutputId, TokenId},
output::{feature::FeatureError, AccountId, ChainId, NativeTokenError, OutputError, OutputId, TokenId},
payload::PayloadError,
semantic::TransactionFailureReason,
signature::SignatureError,
slot::EpochIndex,
unlock::UnlockError,
BlockError,
};
Expand All @@ -25,9 +26,16 @@ use crate::types::block::{
pub enum TransactionBuilderError {
#[error("additional inputs required for {0:?}, but additional input selection is disabled")]
AdditionalInputsRequired(Requirement),
#[error("account {0} is already staking")]
AlreadyStaking(AccountId),
/// Can't burn and transition an output at the same time.
#[error("can't burn and transition an output at the same time, chain ID: {0}")]
BurnAndTransition(ChainId),
#[error("account {account_id} cannot end staking until {end_epoch}")]
CannotEndStaking {
account_id: AccountId,
end_epoch: EpochIndex,
},
#[error("mana rewards provided without an associated burn or custom input, output ID: {0}")]
ExtraManaRewards(OutputId),
/// Insufficient amount provided.
Expand Down Expand Up @@ -72,9 +80,15 @@ pub enum TransactionBuilderError {
/// No available inputs were provided to transaction builder.
#[error("no available inputs provided")]
NoAvailableInputsProvided,
#[error("account {0} is not staking")]
NotStaking(AccountId),
/// Required input is not available.
#[error("required input {0} is not available")]
RequiredInputIsNotAvailable(OutputId),
#[error("new staking period {additional_epochs} is less than the minimum {min}")]
StakingPeriodLessThanMin { additional_epochs: u32, min: u32 },
#[error("cannot transition non-implicit-account output {0}")]
TransitionNonImplicitAccount(OutputId),
/// Unfulfillable requirement.
#[error("unfulfillable requirement {0:?}")]
UnfulfillableRequirement(Requirement),
Expand Down Expand Up @@ -105,6 +119,9 @@ pub enum TransactionBuilderError {
/// Unlock errors.
#[error("{0}")]
Unlock(#[from] UnlockError),
/// Feature errors.
#[error("{0}")]
Feature(#[from] FeatureError),
/// Semantic errors.
#[error("{0}")]
Semantic(#[from] TransactionFailureReason),
Expand Down
93 changes: 69 additions & 24 deletions sdk/src/client/api/block_builder/transaction_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::collections::{HashMap, HashSet};

use crypto::keys::bip44::Bip44;

pub use self::{burn::Burn, error::TransactionBuilderError, requirement::Requirement};
pub use self::{burn::Burn, error::TransactionBuilderError, requirement::Requirement, transition::Transitions};
use crate::{
client::{
api::{
Expand All @@ -30,11 +30,11 @@ use crate::{
types::block::{
address::{AccountAddress, Address, NftAddress, ToBech32Ext},
context_input::{BlockIssuanceCreditContextInput, CommitmentContextInput, ContextInput, RewardContextInput},
input::{Input, UtxoInput, INPUT_COUNT_RANGE},
input::{Input, UtxoInput, INPUT_COUNT_MAX, INPUT_COUNT_RANGE},
mana::ManaAllotment,
output::{
AccountId, AccountOutputBuilder, AnchorOutputBuilder, BasicOutputBuilder, NftOutputBuilder, Output,
OutputId, OUTPUT_COUNT_RANGE,
AccountId, AccountOutputBuilder, BasicOutputBuilder, ChainId, NftOutputBuilder, Output, OutputId,
OUTPUT_COUNT_RANGE,
},
payload::{
signed_transaction::{Transaction, TransactionCapabilities, TransactionCapabilityFlag},
Expand Down Expand Up @@ -185,6 +185,7 @@ pub struct TransactionBuilder {
provided_outputs: Vec<Output>,
added_outputs: Vec<Output>,
addresses: HashSet<Address>,
transitions: Option<Transitions>,
burn: Option<Burn>,
remainders: Remainders,
creation_slot: SlotIndex,
Expand All @@ -205,14 +206,16 @@ pub(crate) struct MinManaAllotment {
issuer_id: AccountId,
reference_mana_cost: u64,
allotment_debt: u64,
required_allotment: Option<u64>,
}

#[derive(Clone, Debug, Default)]
pub(crate) struct Remainders {
address: Option<Address>,
data: Vec<RemainderData>,
storage_deposit_returns: Vec<Output>,
added_mana: u64,
added_amount: HashMap<Option<ChainId>, u64>,
added_mana: HashMap<Option<ChainId>, u64>,
}

impl TransactionBuilder {
Expand Down Expand Up @@ -257,6 +260,7 @@ impl TransactionBuilder {
provided_outputs: outputs.into_iter().collect(),
added_outputs: Vec::new(),
addresses,
transitions: None,
burn: None,
remainders: Default::default(),
creation_slot: creation_slot_index.into(),
Expand Down Expand Up @@ -374,25 +378,47 @@ impl TransactionBuilder {
return Err(TransactionBuilderError::InvalidInputCount(self.selected_inputs.len()));
}

if self.remainders.added_mana > 0 {
let remainder_address = self
.get_remainder_address()?
.ok_or(TransactionBuilderError::MissingInputWithEd25519Address)?
.0;
let added_mana = self.remainders.added_mana;
if let Some(output) = self.get_output_for_added_mana(&remainder_address) {
log::debug!("Adding {added_mana} excess input mana to output with address {remainder_address}");
let remainder_address = self
.get_remainder_address()?
.ok_or(TransactionBuilderError::MissingInputWithEd25519Address)?
.0;

let mut added_amount_mana = HashMap::<Option<ChainId>, (u64, u64)>::new();
for (chain_id, added_amount) in self.remainders.added_amount.drain() {
added_amount_mana.entry(chain_id).or_default().0 = added_amount;
}
for (chain_id, added_mana) in self.remainders.added_mana.drain() {
added_amount_mana.entry(chain_id).or_default().1 = added_mana;
}

for (chain_id, (added_amount, added_mana)) in added_amount_mana {
let mut output = self.get_output_for_remainder(chain_id, &remainder_address);
if output.is_none() {
output = self.get_output_for_remainder(None, &remainder_address);
}
if let Some(output) = output {
log::debug!(
"Adding {added_amount} excess amount and {added_mana} excess mana to output with address {remainder_address} and {chain_id:?}"
);
let new_amount = output.amount() + added_amount;
let new_mana = output.mana() + added_mana;
*output = match output {
Output::Basic(b) => BasicOutputBuilder::from(&*b).with_mana(new_mana).finish_output()?,
Output::Account(a) => AccountOutputBuilder::from(&*a).with_mana(new_mana).finish_output()?,
Output::Anchor(a) => AnchorOutputBuilder::from(&*a).with_mana(new_mana).finish_output()?,
Output::Nft(n) => NftOutputBuilder::from(&*n).with_mana(new_mana).finish_output()?,
Output::Basic(b) => BasicOutputBuilder::from(&*b)
.with_amount(new_amount)
.with_mana(new_mana)
.finish_output()?,
Output::Account(a) => AccountOutputBuilder::from(&*a)
.with_amount(new_amount)
.with_mana(new_mana)
.finish_output()?,
Output::Nft(n) => NftOutputBuilder::from(&*n)
.with_amount(new_amount)
.with_mana(new_mana)
.finish_output()?,
_ => unreachable!(),
};
}
}

// 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.
Expand Down Expand Up @@ -474,9 +500,16 @@ impl TransactionBuilder {
Ok(data)
}

fn select_input(&mut self, input: InputSigningData) -> Result<Option<&Output>, TransactionBuilderError> {
/// Select an input and return whether an output was created.
fn select_input(&mut self, input: InputSigningData) -> Result<bool, TransactionBuilderError> {
log::debug!("Selecting input {:?}", input.output_id());

if self.selected_inputs.len() >= INPUT_COUNT_MAX as usize {
return Err(TransactionBuilderError::InvalidInputCount(
self.selected_inputs.len() + 1,
));
}

let mut added_output = false;
if let Some(output) = self.transition_input(&input)? {
// No need to check for `outputs_requirements` because
Expand Down Expand Up @@ -507,28 +540,39 @@ impl TransactionBuilder {
.expect("expiration unlockable outputs already filtered out");
self.selected_inputs.insert(required_address, input);

Ok(added_output.then(|| self.added_outputs.last().unwrap()))
// Remove the cached allotment value because it's no longer valid
if let Some(MinManaAllotment { required_allotment, .. }) = self.min_mana_allotment.as_mut() {
*required_allotment = None;
}

Ok(added_output)
}

/// Sets the required inputs of an [`TransactionBuilder`].
/// Sets the required inputs of a [`TransactionBuilder`].
pub fn with_required_inputs(mut self, inputs: impl IntoIterator<Item = OutputId>) -> Self {
self.required_inputs = inputs.into_iter().collect();
self
}

/// Sets the burn of an [`TransactionBuilder`].
/// Sets the transitions of a [`TransactionBuilder`].
pub fn with_transitions(mut self, transitions: impl Into<Option<Transitions>>) -> Self {
self.transitions = transitions.into();
self
}

/// Sets the burn of a [`TransactionBuilder`].
pub fn with_burn(mut self, burn: impl Into<Option<Burn>>) -> Self {
self.burn = burn.into();
self
}

/// Sets the remainder address of an [`TransactionBuilder`].
/// Sets the remainder address of a [`TransactionBuilder`].
pub fn with_remainder_address(mut self, address: impl Into<Option<Address>>) -> Self {
self.remainders.address = address.into();
self
}

/// Sets the mana allotments of an [`TransactionBuilder`].
/// Sets the mana allotments of a [`TransactionBuilder`].
pub fn with_mana_allotments(mut self, mana_allotments: impl IntoIterator<Item = (AccountId, u64)>) -> Self {
self.mana_allotments = mana_allotments.into_iter().collect();
self
Expand Down Expand Up @@ -558,6 +602,7 @@ impl TransactionBuilder {
issuer_id: account_id,
reference_mana_cost,
allotment_debt: 0,
required_allotment: None,
});
self
}
Expand Down
Loading

0 comments on commit 76fab4d

Please sign in to comment.