pub mod input_selection;
pub mod pow;
pub mod transaction;
use std::ops::Range;
use packable::bounded::TryIntoBoundedU16Error;
use serde::{Deserialize, Serialize};
use self::input_selection::BurnDto;
pub use self::transaction::verify_semantic;
use crate::{
client::{
api::block_builder::input_selection::Burn, constants::SHIMMER_COIN_TYPE, secret::SecretManager, Client, Error,
Result,
},
types::{
block::{
address::{Address, Bech32Address, Ed25519Address},
input::{dto::UtxoInputDto, UtxoInput, INPUT_COUNT_MAX},
output::{
dto::OutputDto, unlock_condition::AddressUnlockCondition, BasicOutputBuilder, Output,
OUTPUT_COUNT_RANGE,
},
parent::Parents,
payload::{Payload, TaggedDataPayload},
Block, BlockId, ConvertTo,
},
TryFromDto,
},
};
#[must_use]
pub struct ClientBlockBuilder<'a> {
client: &'a Client,
secret_manager: Option<&'a SecretManager>,
coin_type: u32,
account_index: u32,
initial_address_index: u32,
inputs: Option<Vec<UtxoInput>>,
input_range: Range<u32>,
outputs: Vec<Output>,
custom_remainder_address: Option<Address>,
tag: Option<Vec<u8>>,
data: Option<Vec<u8>>,
parents: Option<Parents>,
burn: Option<Burn>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientBlockBuilderOutputAddress {
pub address: String,
pub amount: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientBlockBuilderOptions {
pub coin_type: Option<u32>,
pub account_index: Option<u32>,
pub initial_address_index: Option<u32>,
pub inputs: Option<Vec<UtxoInputDto>>,
pub input_range: Option<Range<u32>>,
pub output: Option<ClientBlockBuilderOutputAddress>,
pub output_hex: Option<ClientBlockBuilderOutputAddress>,
pub outputs: Option<Vec<OutputDto>>,
pub custom_remainder_address: Option<String>,
pub tag: Option<String>,
pub data: Option<String>,
pub parents: Option<Vec<BlockId>>,
pub burn: Option<BurnDto>,
}
impl<'a> ClientBlockBuilder<'a> {
pub fn new(client: &'a Client) -> Self {
Self {
client,
secret_manager: None,
coin_type: SHIMMER_COIN_TYPE,
account_index: 0,
initial_address_index: 0,
inputs: None,
input_range: 0..100,
outputs: Vec::new(),
custom_remainder_address: None,
tag: None,
data: None,
parents: None,
burn: None,
}
}
pub fn with_burn(mut self, burn: impl Into<Option<Burn>>) -> Self {
self.burn = burn.into();
self
}
pub fn with_secret_manager(mut self, manager: &'a SecretManager) -> Self {
self.secret_manager.replace(manager);
self
}
pub fn with_coin_type(mut self, coin_type: u32) -> Self {
self.coin_type = coin_type;
self
}
pub fn with_account_index(mut self, account_index: u32) -> Self {
self.account_index = account_index;
self
}
pub fn with_initial_address_index(mut self, initial_address_index: u32) -> Self {
self.initial_address_index = initial_address_index;
self
}
pub fn with_input(mut self, input: UtxoInput) -> Result<Self> {
let inputs = self.inputs.get_or_insert_with(Vec::new);
if inputs.len() >= INPUT_COUNT_MAX as _ {
return Err(Error::ConsolidationRequired(inputs.len()));
}
inputs.push(input);
Ok(self)
}
pub fn with_input_range(mut self, range: Range<u32>) -> Self {
self.input_range = range;
self
}
pub async fn with_output(
mut self,
address: impl ConvertTo<Bech32Address>,
amount: u64,
) -> Result<ClientBlockBuilder<'a>> {
let address = address.convert()?;
self.client.bech32_hrp_matches(address.hrp()).await?;
let output = BasicOutputBuilder::new_with_amount(amount)
.add_unlock_condition(AddressUnlockCondition::new(address))
.finish_output(self.client.get_token_supply().await?)?;
self.outputs.push(output);
if !OUTPUT_COUNT_RANGE.contains(&(self.outputs.len() as u16)) {
return Err(crate::client::Error::Block(
crate::types::block::Error::InvalidOutputCount(TryIntoBoundedU16Error::Truncated(self.outputs.len())),
));
}
Ok(self)
}
pub fn with_outputs(mut self, outputs: impl IntoIterator<Item = Output>) -> Result<Self> {
self.outputs.extend(outputs);
if !OUTPUT_COUNT_RANGE.contains(&(self.outputs.len() as u16)) {
return Err(crate::client::Error::Block(
crate::types::block::Error::InvalidOutputCount(TryIntoBoundedU16Error::Truncated(self.outputs.len())),
));
}
Ok(self)
}
pub async fn with_output_hex(mut self, address: &str, amount: u64) -> Result<ClientBlockBuilder<'a>> {
let output = BasicOutputBuilder::new_with_amount(amount)
.add_unlock_condition(AddressUnlockCondition::new(address.parse::<Ed25519Address>()?))
.finish_output(self.client.get_token_supply().await?)?;
self.outputs.push(output);
if !OUTPUT_COUNT_RANGE.contains(&(self.outputs.len() as u16)) {
return Err(crate::client::Error::Block(
crate::types::block::Error::InvalidOutputCount(TryIntoBoundedU16Error::Truncated(self.outputs.len())),
));
}
Ok(self)
}
pub fn with_custom_remainder_address(mut self, address: impl ConvertTo<Bech32Address>) -> Result<Self> {
self.custom_remainder_address.replace(address.convert()?.into());
Ok(self)
}
pub fn with_tag(mut self, tag: impl Into<Option<Vec<u8>>>) -> Self {
self.tag = tag.into();
self
}
pub fn with_data(mut self, data: impl Into<Option<Vec<u8>>>) -> Self {
self.data = data.into();
self
}
pub fn with_parents(mut self, parent_ids: impl Into<Option<Vec<BlockId>>>) -> Result<Self> {
self.parents = parent_ids.into().map(Parents::from_vec).transpose()?;
Ok(self)
}
pub async fn set_options(mut self, options: ClientBlockBuilderOptions) -> Result<ClientBlockBuilder<'a>> {
if let Some(coin_type) = options.coin_type {
self = self.with_coin_type(coin_type);
}
if let Some(account_index) = options.account_index {
self = self.with_account_index(account_index);
}
if let Some(initial_address_index) = options.initial_address_index {
self = self.with_initial_address_index(initial_address_index);
}
if let Some(inputs) = options.inputs {
for input in inputs {
self = self.with_input(UtxoInput::try_from(input)?)?;
}
}
if let Some(input_range) = options.input_range {
self = self.with_input_range(input_range);
}
if let Some(output) = options.output {
self = self
.with_output(
&output.address,
output
.amount
.parse::<u64>()
.map_err(|_| Error::InvalidAmount(output.amount))?,
)
.await?;
}
if let Some(output_hex) = options.output_hex {
self = self
.with_output_hex(
&output_hex.address,
output_hex
.amount
.parse::<u64>()
.map_err(|_| Error::InvalidAmount(output_hex.amount))?,
)
.await?;
}
if let Some(outputs) = options.outputs {
let token_supply = self.client.get_token_supply().await?;
self = self.with_outputs(
outputs
.into_iter()
.map(|o| Ok(Output::try_from_dto_with_params(o, token_supply)?))
.collect::<Result<Vec<Output>>>()?,
)?;
}
if let Some(custom_remainder_address) = options.custom_remainder_address {
self = self.with_custom_remainder_address(&custom_remainder_address)?;
}
if let Some(tag) = options.tag {
self = self.with_tag(prefix_hex::decode::<Vec<_>>(tag)?);
}
if let Some(data) = options.data {
self = self.with_data(prefix_hex::decode::<Vec<_>>(data)?);
}
if let Some(parents) = options.parents {
self = self.with_parents(parents)?;
}
if let Some(burn) = options.burn {
self = self.with_burn(Burn::from(burn));
}
Ok(self)
}
pub async fn finish(self) -> Result<Block> {
if self.data.is_some() && self.tag.is_none() {
return Err(Error::MissingParameter("tag"));
}
if self.inputs.is_some() && self.outputs.is_empty() {
return Err(Error::MissingParameter("output"));
}
if !self.outputs.is_empty() {
if self.secret_manager.is_none() && self.inputs.is_none() {
return Err(Error::MissingParameter("seed"));
}
let prepared_transaction_data = self.prepare_transaction().await?;
let tx_payload = self.sign_transaction(prepared_transaction_data).await?;
self.finish_block(Some(Payload::from(tx_payload))).await
} else if self.tag.is_some() {
self.finish_tagged_data().await
} else {
self.finish_block(None).await
}
}
pub async fn finish_tagged_data(mut self) -> Result<Block> {
let payload: Payload;
{
let index = &self.tag.as_ref();
let data = self.data.take().unwrap_or_default();
let index = TaggedDataPayload::new(index.expect("no tagged_data tag").to_vec(), data)
.map_err(|e| Error::TaggedData(e.to_string()))?;
payload = Payload::from(index);
}
self.finish_block(Some(payload)).await
}
pub async fn finish_block(self, payload: Option<Payload>) -> Result<Block> {
let final_block = self.client.finish_block_builder(self.parents, payload).await?;
let block_id = self.client.post_block_raw(&final_block).await?;
if self.client.get_local_pow().await {
Ok(final_block)
} else {
for time in 1..3 {
if let Ok(block) = self.client.get_block(&block_id).await {
return Ok(block);
}
#[cfg(not(target_family = "wasm"))]
tokio::time::sleep(std::time::Duration::from_millis(time * 50)).await;
#[cfg(target_family = "wasm")]
gloo_timers::future::TimeoutFuture::new((time * 50).try_into().unwrap()).await;
}
self.client.get_block(&block_id).await
}
}
}