use serde::{Deserialize, Serialize};
use crate::{
client::{api::options::RemainderValueStrategy, secret::SecretManage, ClientError},
types::block::{
address::{Address, Bech32Address, Ed25519Address},
output::{
feature::{IssuerFeature, MetadataFeature, NativeTokenFeature, SenderFeature, TagFeature},
unlock_condition::{
AddressUnlockCondition, ExpirationUnlockCondition, StorageDepositReturnUnlockCondition,
TimelockUnlockCondition,
},
BasicOutput, BasicOutputBuilder, MinimumOutputAmount, NativeToken, NftId, NftOutputBuilder, Output,
StorageScoreParameters, UnlockCondition,
},
slot::SlotIndex,
BlockError,
},
utils::serde::string,
wallet::{operations::transaction::TransactionOptions, types::OutputData, Wallet, WalletError},
};
impl<S: 'static + SecretManage> Wallet<S> {
pub async fn prepare_output(
&self,
params: OutputParams,
transaction_options: impl Into<Option<TransactionOptions>> + Send,
) -> Result<Output, WalletError> {
log::debug!("[OUTPUT] prepare_output {params:?}");
let transaction_options = transaction_options.into();
self.client().bech32_hrp_matches(params.recipient_address.hrp()).await?;
let storage_score_params = self.client().get_storage_score_parameters().await?;
let nft_id = params.assets.as_ref().and_then(|a| a.nft_id);
let (mut first_output_builder, existing_nft_output_data) = self
.create_initial_output_builder(params.recipient_address, nft_id, storage_score_params)
.await?;
if let Some(features) = params.features {
if let Some(tag) = features.tag {
first_output_builder = first_output_builder.add_feature(TagFeature::new(
prefix_hex::decode::<Vec<u8>>(tag).map_err(ClientError::PrefixHex)?,
)?);
}
if let Some(metadata) = features.metadata {
first_output_builder = first_output_builder.add_feature(metadata);
}
if let Some(sender) = features.sender {
first_output_builder = first_output_builder.add_feature(SenderFeature::new(sender))
}
if let Some(issuer) = features.issuer {
if let OutputBuilder::Basic(_) = first_output_builder {
return Err(WalletError::MissingParameter("nft_id"));
}
first_output_builder = first_output_builder.add_immutable_feature(IssuerFeature::new(issuer));
}
if let Some(native_token) = &features.native_token {
first_output_builder = first_output_builder.with_native_token(*native_token);
}
}
if let Some(unlocks) = params.unlocks {
if let Some(expiration_slot_index) = unlocks.expiration_slot_index {
let remainder_address = self.get_remainder_address(transaction_options.clone()).await?;
first_output_builder = first_output_builder.add_unlock_condition(ExpirationUnlockCondition::new(
remainder_address,
expiration_slot_index,
)?);
}
if let Some(timelock_slot_index) = unlocks.timelock_slot_index {
first_output_builder =
first_output_builder.add_unlock_condition(TimelockUnlockCondition::new(timelock_slot_index)?);
}
}
let first_output = first_output_builder
.with_minimum_amount(storage_score_params)
.finish_output()?;
let mut second_output_builder = if nft_id.is_some() {
OutputBuilder::Nft(NftOutputBuilder::from(first_output.as_nft()))
} else {
OutputBuilder::Basic(BasicOutputBuilder::from(first_output.as_basic()))
};
let min_amount_basic_output =
BasicOutput::minimum_amount(&Address::from(Ed25519Address::null()), storage_score_params);
let min_required_storage_deposit = first_output.amount();
if params.amount > min_required_storage_deposit {
second_output_builder = second_output_builder.with_amount(params.amount);
}
let return_strategy = params
.storage_deposit
.clone()
.unwrap_or_default()
.return_strategy
.unwrap_or_default();
let remainder_address = self.get_remainder_address(transaction_options).await?;
if params.amount < min_required_storage_deposit {
if return_strategy == ReturnStrategy::Gift {
second_output_builder = second_output_builder.with_amount(min_required_storage_deposit);
}
if return_strategy == ReturnStrategy::Return {
second_output_builder =
second_output_builder.add_unlock_condition(StorageDepositReturnUnlockCondition::new(
remainder_address.clone(),
min_amount_basic_output,
)?);
let new_amount = params.amount + min_amount_basic_output;
let min_storage_deposit_new_amount = second_output_builder
.clone()
.with_minimum_amount(storage_score_params)
.finish_output()?
.amount();
if new_amount < min_storage_deposit_new_amount {
let additional_required_amount = min_storage_deposit_new_amount - new_amount;
second_output_builder = second_output_builder.with_amount(new_amount + additional_required_amount);
second_output_builder =
second_output_builder.replace_unlock_condition(StorageDepositReturnUnlockCondition::new(
remainder_address.clone(),
min_amount_basic_output + additional_required_amount,
)?);
} else {
second_output_builder = second_output_builder.with_amount(new_amount);
}
}
}
let third_output = second_output_builder.clone().finish_output()?;
let mut final_amount = third_output.amount();
let mut available_base_coin = self.balance().await?.base_coin.available;
if let Some(existing_nft_output_data) = existing_nft_output_data {
available_base_coin += existing_nft_output_data.output.minimum_amount(storage_score_params);
}
if final_amount > available_base_coin {
return Err(WalletError::InsufficientFunds {
available: available_base_coin,
required: final_amount,
});
}
if final_amount == available_base_coin {
return Ok(third_output);
}
if final_amount < available_base_coin {
let remaining_balance = available_base_coin - final_amount;
if remaining_balance < min_amount_basic_output {
if params
.storage_deposit
.unwrap_or_default()
.use_excess_if_low
.unwrap_or_default()
{
final_amount += remaining_balance;
second_output_builder = second_output_builder.with_amount(final_amount);
if let Some(sdr) = third_output.unlock_conditions().storage_deposit_return() {
let new_sdr_amount = sdr.amount() + remaining_balance;
second_output_builder =
second_output_builder.replace_unlock_condition(StorageDepositReturnUnlockCondition::new(
remainder_address,
new_sdr_amount,
)?);
}
} else {
return Err(WalletError::InsufficientFunds {
available: available_base_coin,
required: available_base_coin + min_amount_basic_output - remaining_balance,
});
}
}
}
Ok(second_output_builder.finish_output()?)
}
async fn create_initial_output_builder(
&self,
recipient_address: Bech32Address,
nft_id: Option<NftId>,
params: StorageScoreParameters,
) -> Result<(OutputBuilder, Option<OutputData>), WalletError> {
let (mut first_output_builder, existing_nft_output_data) = if let Some(nft_id) = &nft_id {
if nft_id.is_null() {
(
OutputBuilder::Nft(NftOutputBuilder::new_with_minimum_amount(params, *nft_id)),
None,
)
} else {
let unspent_nft_output = self.ledger().await.unspent_nft_output(nft_id).cloned();
let mut first_output_builder = if let Some(nft_output_data) = &unspent_nft_output {
let nft_output = nft_output_data.output.as_nft();
NftOutputBuilder::from(nft_output).with_nft_id(*nft_id)
} else {
return Err(WalletError::NftNotFoundInUnspentOutputs);
};
first_output_builder = first_output_builder.clear_features();
first_output_builder = first_output_builder.clear_unlock_conditions();
(OutputBuilder::Nft(first_output_builder), unspent_nft_output)
}
} else {
(
OutputBuilder::Basic(BasicOutputBuilder::new_with_minimum_amount(params)),
None,
)
};
first_output_builder =
first_output_builder.add_unlock_condition(AddressUnlockCondition::new(recipient_address));
Ok((first_output_builder, existing_nft_output_data))
}
pub(crate) async fn get_remainder_address(
&self,
transaction_options: impl Into<Option<TransactionOptions>> + Send,
) -> Result<Address, WalletError> {
let transaction_options = transaction_options.into();
Ok(if let Some(options) = &transaction_options {
match &options.remainder_value_strategy {
RemainderValueStrategy::ReuseAddress => self.address().await.into_inner(),
RemainderValueStrategy::CustomAddress(address) => address.clone(),
}
} else {
self.address().await.into_inner()
})
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OutputParams {
pub recipient_address: Bech32Address,
#[serde(with = "string")]
pub amount: u64,
#[serde(default)]
pub assets: Option<Assets>,
#[serde(default)]
pub features: Option<Features>,
#[serde(default)]
pub unlocks: Option<Unlocks>,
#[serde(default)]
pub storage_deposit: Option<StorageDeposit>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Assets {
pub nft_id: Option<NftId>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Features {
pub tag: Option<String>,
pub metadata: Option<MetadataFeature>,
pub issuer: Option<Bech32Address>,
pub sender: Option<Bech32Address>,
pub native_token: Option<NativeToken>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Unlocks {
pub expiration_slot_index: Option<SlotIndex>,
pub timelock_slot_index: Option<SlotIndex>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageDeposit {
pub return_strategy: Option<ReturnStrategy>,
pub use_excess_if_low: Option<bool>,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ReturnStrategy {
#[default]
Return,
Gift,
}
#[derive(Clone)]
enum OutputBuilder {
Basic(BasicOutputBuilder),
Nft(NftOutputBuilder),
}
impl OutputBuilder {
fn add_feature(mut self, feature: impl Into<crate::types::block::output::Feature>) -> Self {
match self {
Self::Basic(b) => self = Self::Basic(b.add_feature(feature)),
Self::Nft(b) => self = Self::Nft(b.add_feature(feature)),
}
self
}
fn add_immutable_feature(mut self, feature: impl Into<crate::types::block::output::Feature>) -> Self {
match self {
Self::Basic(_) => { }
Self::Nft(b) => {
self = Self::Nft(b.add_immutable_feature(feature));
}
}
self
}
fn add_unlock_condition(mut self, unlock_condition: impl Into<UnlockCondition>) -> Self {
match self {
Self::Basic(b) => {
self = Self::Basic(b.add_unlock_condition(unlock_condition));
}
Self::Nft(b) => {
self = Self::Nft(b.add_unlock_condition(unlock_condition));
}
}
self
}
fn replace_unlock_condition(mut self, unlock_condition: impl Into<UnlockCondition>) -> Self {
match self {
Self::Basic(b) => {
self = Self::Basic(b.replace_unlock_condition(unlock_condition));
}
Self::Nft(b) => {
self = Self::Nft(b.replace_unlock_condition(unlock_condition));
}
}
self
}
fn with_amount(mut self, amount: u64) -> Self {
match self {
Self::Basic(b) => {
self = Self::Basic(b.with_amount(amount));
}
Self::Nft(b) => {
self = Self::Nft(b.with_amount(amount));
}
}
self
}
fn with_minimum_amount(mut self, params: StorageScoreParameters) -> Self {
match self {
Self::Basic(b) => {
self = Self::Basic(b.with_minimum_amount(params));
}
Self::Nft(b) => {
self = Self::Nft(b.with_minimum_amount(params));
}
}
self
}
fn with_native_token(mut self, native_token: NativeToken) -> Self {
if let Self::Basic(b) = self {
self = Self::Basic(b.add_feature(NativeTokenFeature::from(native_token)));
}
self
}
fn finish_output(self) -> Result<Output, BlockError> {
Ok(match self {
Self::Basic(b) => b.finish_output()?,
Self::Nft(b) => b.finish_output()?,
})
}
}