1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// Copyright 2022 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
use getset::Getters;
use serde::{Deserialize, Serialize};
use crate::{
client::{api::PreparedTransactionData, secret::SecretManage},
types::block::{
address::Bech32Address,
output::{
unlock_condition::{
AddressUnlockCondition, ExpirationUnlockCondition, StorageDepositReturnUnlockCondition,
},
BasicOutputBuilder, MinimumStorageDepositBasicOutput,
},
ConvertTo,
},
wallet::{
account::{
constants::DEFAULT_EXPIRATION_TIME, operations::transaction::Transaction, Account, TransactionOptions,
},
Error,
},
};
/// Parameters for `send()`
#[derive(Debug, Clone, Serialize, Deserialize, Getters)]
pub struct SendParams {
/// Amount
#[serde(with = "crate::utils::serde::string")]
#[getset(get = "pub")]
amount: u64,
/// Bech32 encoded address
#[getset(get = "pub")]
address: Bech32Address,
/// Bech32 encoded return address, to which the storage deposit will be returned if one is necessary
/// given the provided amount. If a storage deposit is needed and a return address is not provided, it will
/// default to the first address of the account.
#[getset(get = "pub")]
return_address: Option<Bech32Address>,
/// Expiration in seconds, after which the output will be available for the sender again, if not spent by the
/// receiver already. The expiration will only be used if one is necessary given the provided amount. If an
/// expiration is needed but not provided, it will default to one day.
#[getset(get = "pub")]
expiration: Option<u32>,
}
impl SendParams {
pub fn new(amount: u64, address: impl ConvertTo<Bech32Address>) -> Result<Self, crate::wallet::Error> {
Ok(Self {
amount,
address: address.convert()?,
return_address: None,
expiration: None,
})
}
pub fn try_with_return_address(
mut self,
address: impl ConvertTo<Bech32Address>,
) -> Result<Self, crate::wallet::Error> {
self.return_address = Some(address.convert()?);
Ok(self)
}
pub fn with_return_address(mut self, address: impl Into<Option<Bech32Address>>) -> Self {
self.return_address = address.into();
self
}
pub fn with_expiration(mut self, expiration: impl Into<Option<u32>>) -> Self {
self.expiration = expiration.into();
self
}
}
impl<S: 'static + SecretManage> Account<S>
where
crate::wallet::Error: From<S::Error>,
{
/// Sends a certain amount of base coins to a single address.
///
/// Calls [Account::send_with_params()](crate::wallet::Account::send_with_params) internally.
/// The options may define the remainder value strategy or custom inputs.
/// The provided Addresses provided with [`SendParams`] need to be bech32-encoded.
pub async fn send(
&self,
amount: u64,
address: impl ConvertTo<Bech32Address>,
options: impl Into<Option<TransactionOptions>> + Send,
) -> crate::wallet::Result<Transaction> {
let params = [SendParams::new(amount, address)?];
self.send_with_params(params, options).await
}
/// Sends a certain amount of base coins with full customizability of the transaction.
///
/// Calls [Account::send_outputs()](crate::wallet::Account::send_outputs) internally.
/// The options may define the remainder value strategy or custom inputs.
/// Addresses provided with [`SendParams`] need to be bech32-encoded.
/// ```ignore
/// let params = [SendParams::new(
/// "rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu",
/// 1_000_000)?
/// ];
///
/// let tx = account.send(params, None ).await?;
/// println!("Transaction created: {}", tx.transaction_id);
/// if let Some(block_id) = tx.block_id {
/// println!("Block sent: {}", block_id);
/// }
/// ```
pub async fn send_with_params<I: IntoIterator<Item = SendParams> + Send>(
&self,
params: I,
options: impl Into<Option<TransactionOptions>> + Send,
) -> crate::wallet::Result<Transaction>
where
I::IntoIter: Send,
{
let options = options.into();
let prepared_transaction = self.prepare_send(params, options.clone()).await?;
self.sign_and_submit_transaction(prepared_transaction, options).await
}
/// Prepares the transaction for
/// [Account::send()](crate::wallet::Account::send).
pub async fn prepare_send<I: IntoIterator<Item = SendParams> + Send>(
&self,
params: I,
options: impl Into<Option<TransactionOptions>> + Send,
) -> crate::wallet::Result<PreparedTransactionData>
where
I::IntoIter: Send,
{
log::debug!("[TRANSACTION] prepare_send");
let options = options.into();
let rent_structure = self.client().get_rent_structure().await?;
let token_supply = self.client().get_token_supply().await?;
let account_addresses = self.addresses().await?;
let default_return_address = account_addresses.first().ok_or(Error::FailedToGetRemainder)?;
let local_time = self.client().get_time_checked().await?;
let mut outputs = Vec::new();
for SendParams {
address,
amount,
return_address,
expiration,
} in params
{
self.client().bech32_hrp_matches(address.hrp()).await?;
let return_address = return_address
.map(|return_address| {
if return_address.hrp() != address.hrp() {
Err(crate::client::Error::Bech32HrpMismatch {
provided: return_address.hrp().to_string(),
expected: address.hrp().to_string(),
})?;
}
Ok::<_, Error>(return_address)
})
.transpose()?
.unwrap_or(default_return_address.address);
// Get the minimum required amount for an output assuming it does not need a storage deposit.
let output = BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure)
.add_unlock_condition(AddressUnlockCondition::new(address))
.finish_output(token_supply)?;
if amount >= output.amount() {
outputs.push(
BasicOutputBuilder::from(output.as_basic())
.with_amount(amount)
.finish_output(token_supply)?,
)
} else {
let expiration_time = expiration.map_or(local_time + DEFAULT_EXPIRATION_TIME, |expiration_time| {
local_time + expiration_time
});
// Since it does need a storage deposit, calculate how much that should be
let storage_deposit_amount = MinimumStorageDepositBasicOutput::new(rent_structure, token_supply)
.with_storage_deposit_return()?
.with_expiration()?
.finish()?;
if !options.as_ref().map(|o| o.allow_micro_amount).unwrap_or_default() {
return Err(Error::InsufficientFunds {
available: amount,
required: amount + storage_deposit_amount,
});
}
outputs.push(
// Add address_and_amount.amount+storage_deposit_amount, so receiver can get
// address_and_amount.amount
BasicOutputBuilder::from(output.as_basic())
.with_amount(amount + storage_deposit_amount)
.add_unlock_condition(
// We send the storage_deposit_amount back to the sender, so only the additional amount is
// sent
StorageDepositReturnUnlockCondition::new(
return_address,
storage_deposit_amount,
token_supply,
)?,
)
.add_unlock_condition(ExpirationUnlockCondition::new(return_address, expiration_time)?)
.finish_output(token_supply)?,
)
}
}
self.prepare_transaction(outputs, options).await
}
}