use std::{cmp::Ordering, str::FromStr};
use iota_client::block::{
address::Address,
output::{
dto::NativeTokenDto,
feature::{Feature, IssuerFeature, MetadataFeature, SenderFeature, TagFeature},
unlock_condition::{
AddressUnlockCondition, ExpirationUnlockCondition, StorageDepositReturnUnlockCondition,
TimelockUnlockCondition, UnlockCondition,
},
BasicOutputBuilder, NativeToken, NftId, NftOutputBuilder, Output, Rent,
},
DtoError,
};
use serde::{Deserialize, Serialize};
use crate::account::{handle::AccountHandle, operations::transaction::RemainderValueStrategy, TransactionOptions};
impl AccountHandle {
pub async fn prepare_output(
&self,
options: OutputOptions,
transaction_options: Option<TransactionOptions>,
) -> crate::Result<Output> {
log::debug!("[OUTPUT] prepare_output {options:?}");
let token_supply = self.client.get_token_supply().await?;
if let Some(assets) = &options.assets {
if let Some(nft_id) = assets.nft_id {
return self
.prepare_nft_output(options.clone(), transaction_options, nft_id)
.await;
}
}
let rent_structure = self.client.get_rent_structure().await?;
let mut first_output_builder = BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure.clone())?
.add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(
Address::try_from_bech32(options.recipient_address.clone())?.1,
)));
if let Some(assets) = options.assets {
if let Some(native_tokens) = assets.native_tokens {
first_output_builder = first_output_builder.with_native_tokens(native_tokens);
}
}
if let Some(features) = options.features {
if let Some(_issuer) = features.issuer {
return Err(crate::Error::MissingParameter("nft_id"));
}
if let Some(tag) = features.tag {
first_output_builder = first_output_builder.add_feature(Feature::Tag(TagFeature::new(
prefix_hex::decode(&tag).map_err(|_| DtoError::InvalidField("tag"))?,
)?));
}
if let Some(metadata) = features.metadata {
first_output_builder = first_output_builder.add_feature(Feature::Metadata(MetadataFeature::new(
prefix_hex::decode(&metadata).map_err(|_| DtoError::InvalidField("metadata"))?,
)?));
}
if let Some(sender) = features.sender {
first_output_builder = first_output_builder
.add_feature(Feature::Sender(SenderFeature::new(Address::try_from_bech32(sender)?.1)))
}
}
if let Some(unlocks) = options.unlocks {
if let Some(expiration_unix_time) = unlocks.expiration_unix_time {
let remainder_address = self.get_remainder_address(transaction_options.clone()).await?;
first_output_builder = first_output_builder.add_unlock_condition(UnlockCondition::Expiration(
ExpirationUnlockCondition::new(remainder_address, expiration_unix_time)?,
));
}
if let Some(timelock_unix_time) = unlocks.timelock_unix_time {
first_output_builder = first_output_builder.add_unlock_condition(UnlockCondition::Timelock(
TimelockUnlockCondition::new(timelock_unix_time)?,
));
}
}
let first_output = first_output_builder.finish(token_supply)?;
let mut second_output_builder = BasicOutputBuilder::from(&first_output);
match options.amount.cmp(&first_output.amount()) {
Ordering::Greater | Ordering::Equal => {
second_output_builder = second_output_builder.with_amount(options.amount)?;
}
Ordering::Less => {
let storage_deposit = options.storage_deposit.unwrap_or_default();
if let ReturnStrategy::Return = storage_deposit.return_strategy.unwrap_or_default() {
let remainder_address = self.get_remainder_address(transaction_options).await?;
let min_storage_deposit_return_amount =
BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure.clone())?
.add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(
Address::try_from_bech32(options.recipient_address.clone())?.1,
)))
.finish_output(token_supply)?
.amount();
second_output_builder = second_output_builder.add_unlock_condition(
UnlockCondition::StorageDepositReturn(StorageDepositReturnUnlockCondition::new(
remainder_address,
min_storage_deposit_return_amount,
token_supply,
)?),
);
}
if storage_deposit.use_excess_if_low.unwrap_or_default() {
let balance = self.balance().await?;
if let Ordering::Greater = balance.base_coin.available.cmp(&first_output.amount()) {
let balance_minus_output = balance.base_coin.available - first_output.amount();
let minimum_required_storage_deposit =
BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure.clone())?
.add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(
Address::try_from_bech32(options.recipient_address)?.1,
)))
.finish_output(token_supply)?
.amount();
if balance_minus_output < minimum_required_storage_deposit {
second_output_builder =
second_output_builder.with_amount(first_output.amount() + balance_minus_output)?;
}
}
}
}
}
let second_output = second_output_builder.finish(token_supply)?;
let required_storage_deposit = Output::Basic(second_output.clone()).rent_cost(&rent_structure);
let mut third_output_builder = BasicOutputBuilder::from(&second_output);
if second_output.amount() < required_storage_deposit {
third_output_builder = third_output_builder.with_amount(required_storage_deposit)?;
let new_sdr_amount = required_storage_deposit - options.amount;
if let Some(sdr) = second_output.unlock_conditions().storage_deposit_return() {
let new_sdr_unlock_condition = UnlockCondition::StorageDepositReturn(
StorageDepositReturnUnlockCondition::new(*sdr.return_address(), new_sdr_amount, token_supply)?,
);
third_output_builder = third_output_builder.replace_unlock_condition(new_sdr_unlock_condition)?;
}
}
Ok(third_output_builder.finish_output(token_supply)?)
}
async fn prepare_nft_output(
&self,
options: OutputOptions,
transaction_options: Option<TransactionOptions>,
nft_id: NftId,
) -> crate::Result<Output> {
log::debug!("[OUTPUT] prepare_nft_output {options:?}");
let token_supply = self.client.get_token_supply().await?;
let rent_structure = self.client.get_rent_structure().await?;
let unspent_outputs = self.unspent_outputs(None).await?;
let mut first_output_builder = if let Some(nft_output_data) = unspent_outputs.iter().find(|o| {
if let Output::Nft(nft_output) = &o.output {
nft_id == nft_output.nft_id_non_null(&o.output_id)
} else {
false
}
}) {
if let Output::Nft(nft_output) = &nft_output_data.output {
NftOutputBuilder::from(nft_output).with_nft_id(nft_output.nft_id_non_null(&nft_output_data.output_id))
} else {
unreachable!("We checked before if it's an nft output")
}
} else if nft_id.is_null() {
NftOutputBuilder::new_with_minimum_storage_deposit(rent_structure.clone(), nft_id)?
} else {
return Err(crate::Error::NftNotFoundInUnspentOutputs);
};
first_output_builder = first_output_builder.with_unlock_conditions(vec![UnlockCondition::Address(
AddressUnlockCondition::new(Address::try_from_bech32(options.recipient_address.clone())?.1),
)]);
if let Some(assets) = options.assets {
if let Some(native_tokens) = assets.native_tokens {
first_output_builder = first_output_builder.with_native_tokens(native_tokens);
}
}
if let Some(features) = options.features {
if let Some(tag) = features.tag {
first_output_builder =
first_output_builder.add_feature(Feature::Tag(TagFeature::new(tag.as_bytes().to_vec())?));
}
if let Some(metadata) = features.metadata {
first_output_builder = first_output_builder
.add_feature(Feature::Metadata(MetadataFeature::new(metadata.as_bytes().to_vec())?));
}
if let Some(issuer) = features.issuer {
first_output_builder = first_output_builder
.add_immutable_feature(Feature::Issuer(IssuerFeature::new(Address::try_from_bech32(issuer)?.1)));
}
if let Some(sender) = features.sender {
first_output_builder = first_output_builder
.add_feature(Feature::Sender(SenderFeature::new(Address::try_from_bech32(sender)?.1)))
}
}
if let Some(unlocks) = options.unlocks {
if let Some(expiration_unix_time) = unlocks.expiration_unix_time {
let remainder_address = self.get_remainder_address(transaction_options.clone()).await?;
first_output_builder = first_output_builder.add_unlock_condition(UnlockCondition::Expiration(
ExpirationUnlockCondition::new(remainder_address, expiration_unix_time)?,
));
}
if let Some(timelock_unix_time) = unlocks.timelock_unix_time {
first_output_builder = first_output_builder.add_unlock_condition(UnlockCondition::Timelock(
TimelockUnlockCondition::new(timelock_unix_time)?,
));
}
}
let first_output = first_output_builder
.with_minimum_storage_deposit(rent_structure.clone())
.finish(token_supply)?;
let mut second_output_builder = NftOutputBuilder::from(&first_output);
match options.amount.cmp(&first_output.amount()) {
Ordering::Greater | Ordering::Equal => {
second_output_builder = second_output_builder.with_amount(options.amount)?;
}
Ordering::Less => {
let storage_deposit = options.storage_deposit.unwrap_or_default();
if let ReturnStrategy::Return = storage_deposit.return_strategy.unwrap_or_default() {
let remainder_address = self.get_remainder_address(transaction_options).await?;
let min_storage_deposit_return_amount =
BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure.clone())?
.add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(
Address::try_from_bech32(options.recipient_address.clone())?.1,
)))
.finish_output(token_supply)?
.amount();
second_output_builder = second_output_builder.add_unlock_condition(
UnlockCondition::StorageDepositReturn(StorageDepositReturnUnlockCondition::new(
remainder_address,
min_storage_deposit_return_amount,
token_supply,
)?),
);
}
if storage_deposit.use_excess_if_low.unwrap_or_default() {
let balance = self.balance().await?;
if let Ordering::Greater = balance.base_coin.available.cmp(&first_output.amount()) {
let balance_minus_output = balance.base_coin.available - first_output.amount();
let minimum_required_storage_deposit =
BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure.clone())?
.add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(
Address::try_from_bech32(options.recipient_address)?.1,
)))
.finish_output(token_supply)?
.amount();
if balance_minus_output < minimum_required_storage_deposit {
second_output_builder =
second_output_builder.with_amount(first_output.amount() + balance_minus_output)?;
}
}
}
}
}
let second_output = second_output_builder.finish(token_supply)?;
let required_storage_deposit = Output::Nft(second_output.clone()).rent_cost(&rent_structure);
let mut third_output_builder = NftOutputBuilder::from(&second_output);
if second_output.amount() < required_storage_deposit {
third_output_builder = third_output_builder.with_amount(required_storage_deposit)?;
let new_sdr_amount = required_storage_deposit - options.amount;
if let Some(sdr) = second_output.unlock_conditions().storage_deposit_return() {
let new_sdr_unlock_condition = UnlockCondition::StorageDepositReturn(
StorageDepositReturnUnlockCondition::new(*sdr.return_address(), new_sdr_amount, token_supply)?,
);
third_output_builder = third_output_builder.replace_unlock_condition(new_sdr_unlock_condition)?;
}
}
Ok(third_output_builder.finish_output(token_supply)?)
}
async fn get_remainder_address(&self, transaction_options: Option<TransactionOptions>) -> crate::Result<Address> {
let remainder_address = match &transaction_options {
Some(options) => {
match &options.remainder_value_strategy {
RemainderValueStrategy::ReuseAddress => {
None
}
RemainderValueStrategy::ChangeAddress => {
let remainder_address = self.generate_remainder_address().await?;
Some(remainder_address.address().inner)
}
RemainderValueStrategy::CustomAddress(address) => Some(address.address().inner),
}
}
None => None,
};
let remainder_address = match remainder_address {
Some(address) => address,
None => {
self.addresses()
.await?
.first()
.ok_or(crate::Error::FailedToGetRemainder)?
.address()
.inner
}
};
Ok(remainder_address)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct OutputOptions {
pub recipient_address: String,
pub amount: u64,
pub assets: Option<Assets>,
pub features: Option<Features>,
pub unlocks: Option<Unlocks>,
pub storage_deposit: Option<StorageDeposit>,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Assets {
#[serde(rename = "nativeToken")]
pub native_tokens: Option<Vec<NativeToken>>,
#[serde(rename = "nftId")]
pub nft_id: Option<NftId>,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Features {
pub tag: Option<String>,
pub metadata: Option<String>,
pub issuer: Option<String>,
pub sender: Option<String>,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Unlocks {
#[serde(rename = "expirationUnixTime")]
pub expiration_unix_time: Option<u32>,
#[serde(rename = "timelockUnixTime")]
pub timelock_unix_time: Option<u32>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct StorageDeposit {
#[serde(rename = "returnStrategy")]
pub return_strategy: Option<ReturnStrategy>,
#[serde(rename = "useExcessIfLow")]
pub use_excess_if_low: Option<bool>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub enum ReturnStrategy {
Return,
Gift,
}
impl Default for ReturnStrategy {
fn default() -> Self {
ReturnStrategy::Return
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct OutputOptionsDto {
#[serde(rename = "recipientAddress")]
recipient_address: String,
amount: String,
#[serde(default)]
assets: Option<AssetsDto>,
#[serde(default)]
features: Option<Features>,
#[serde(default)]
unlocks: Option<Unlocks>,
#[serde(rename = "storageDeposit", default)]
storage_deposit: Option<StorageDeposit>,
}
impl TryFrom<&OutputOptionsDto> for OutputOptions {
type Error = crate::Error;
fn try_from(value: &OutputOptionsDto) -> crate::Result<Self> {
Ok(Self {
recipient_address: value.recipient_address.clone(),
amount: u64::from_str(&value.amount)
.map_err(|_| iota_client::Error::InvalidAmount(value.amount.clone()))?,
assets: match &value.assets {
Some(r) => Some(Assets::try_from(r)?),
None => None,
},
features: value.features.clone(),
unlocks: value.unlocks.clone(),
storage_deposit: value.storage_deposit.clone(),
})
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct AssetsDto {
#[serde(rename = "nativeTokens")]
native_tokens: Option<Vec<NativeTokenDto>>,
#[serde(rename = "nftId")]
nft_id: Option<String>,
}
impl TryFrom<&AssetsDto> for Assets {
type Error = crate::Error;
fn try_from(value: &AssetsDto) -> crate::Result<Self> {
Ok(Self {
native_tokens: match &value.native_tokens {
Some(r) => Some(
r.iter()
.map(|r| Ok(NativeToken::try_from(r)?))
.collect::<crate::Result<Vec<NativeToken>>>()?,
),
None => None,
},
nft_id: match &value.nft_id {
Some(r) => Some(NftId::from_str(r)?),
None => None,
},
})
}
}