use time::Duration;
use crate::signer::AnySigner;
use crate::staked_id::StakedId;
use crate::{
AccountId,
Client,
ContractCreateTransaction,
Error,
FileAppendTransaction,
FileCreateTransaction,
FileDeleteTransaction,
FileId,
Hbar,
Key,
PrivateKey,
PublicKey,
TransactionResponse,
};
#[derive(Default, Debug)]
pub struct ContractCreateFlow {
bytecode: Vec<u8>,
file_append_max_chunks: Option<usize>,
node_account_ids: Option<Vec<AccountId>>,
contract_data: ContractData,
}
impl ContractCreateFlow {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn get_bytecode(&self) -> &[u8] {
&self.bytecode
}
pub fn bytecode(&mut self, bytecode: Vec<u8>) -> &mut Self {
self.bytecode = bytecode;
self
}
pub fn bytecode_hex(&mut self, bytecode: &str) -> crate::Result<&mut Self> {
self.bytecode = hex::decode(bytecode).map_err(Error::basic_parse)?;
Ok(self)
}
#[must_use]
pub fn get_node_account_ids(&self) -> Option<&[AccountId]> {
self.node_account_ids.as_deref()
}
pub fn node_account_ids(
&mut self,
node_account_ids: impl IntoIterator<Item = AccountId>,
) -> &mut Self {
self.node_account_ids = Some(node_account_ids.into_iter().collect());
self
}
#[must_use]
pub fn get_max_chunks(&self) -> Option<usize> {
self.file_append_max_chunks
}
pub fn max_chunks(&mut self, max_chunks: usize) -> &mut Self {
self.file_append_max_chunks = Some(max_chunks);
self
}
#[must_use]
pub fn get_constructor_parameters(&self) -> &[u8] {
&self.contract_data.constructor_parameters
}
pub fn constructor_parameters(
&mut self,
constructor_parameters: impl Into<Vec<u8>>,
) -> &mut Self {
self.contract_data.constructor_parameters = constructor_parameters.into();
self
}
#[must_use]
pub fn get_gas(&self) -> u64 {
self.contract_data.gas
}
pub fn gas(&mut self, gas: u64) -> &mut Self {
self.contract_data.gas = gas;
self
}
#[must_use]
pub fn get_initial_balance(&self) -> Hbar {
self.contract_data.initial_balance
}
pub fn initial_balance(&mut self, initial_balance: Hbar) -> &mut Self {
self.contract_data.initial_balance = initial_balance;
self
}
#[must_use]
pub fn get_max_automatic_token_associations(&self) -> i32 {
self.contract_data.max_automatic_token_associations
}
pub fn max_automatic_token_associations(
&mut self,
max_automatic_token_associations: i32,
) -> &mut Self {
self.contract_data.max_automatic_token_associations = max_automatic_token_associations;
self
}
#[must_use]
pub fn get_decline_staking_reward(&self) -> bool {
self.contract_data.decline_staking_reward
}
pub fn decline_staking_reward(&mut self, decline_staking_reward: bool) -> &mut Self {
self.contract_data.decline_staking_reward = decline_staking_reward;
self
}
#[must_use]
pub fn get_admin_key(&self) -> Option<&Key> {
self.contract_data.admin_key.as_ref()
}
pub fn admin_key(&mut self, admin_key: impl Into<Key>) -> &mut Self {
self.contract_data.admin_key = Some(admin_key.into());
self
}
#[must_use]
pub fn get_auto_renew_account_id(&self) -> Option<AccountId> {
self.contract_data.auto_renew_account_id
}
pub fn auto_renew_account_id(&mut self, auto_renew_account_id: AccountId) -> &mut Self {
self.contract_data.auto_renew_account_id = Some(auto_renew_account_id);
self
}
#[must_use]
pub fn get_auto_renew_period(&self) -> Option<Duration> {
self.contract_data.auto_renew_period
}
pub fn auto_renew_period(&mut self, auto_renew_period: Duration) -> &mut Self {
self.contract_data.auto_renew_period = Some(auto_renew_period);
self
}
#[must_use]
pub fn get_contract_memo(&self) -> Option<&str> {
self.contract_data.contract_memo.as_deref()
}
pub fn contract_memo(&mut self, contract_memo: String) -> &mut Self {
self.contract_data.contract_memo = Some(contract_memo);
self
}
pub fn get_staked_account_id(&self) -> Option<AccountId> {
self.contract_data.staked_id.and_then(StakedId::to_account_id)
}
pub fn staked_account_id(&mut self, staked_account_id: AccountId) -> &mut Self {
self.contract_data.staked_id = Some(StakedId::AccountId(staked_account_id));
self
}
pub fn get_staked_node_id(&self) -> Option<u64> {
self.contract_data.staked_id.and_then(StakedId::to_node_id)
}
pub fn staked_node_id(&mut self, staked_node_id: u64) -> &mut Self {
self.contract_data.staked_id = Some(StakedId::NodeId(staked_node_id));
self
}
pub fn freeze_with(&mut self, client: Client) -> &mut Self {
self.contract_data.freeze_with_client = Some(client);
self
}
pub fn sign(&mut self, key: PrivateKey) -> &mut Self {
self.contract_data.signer = Some(AnySigner::PrivateKey(key));
self
}
pub fn sign_with<F: Fn(&[u8]) -> Vec<u8> + Send + Sync + 'static>(
&mut self,
public_key: PublicKey,
signer: F,
) -> &mut Self {
self.contract_data.signer = Some(AnySigner::arbitrary(Box::new(public_key), signer));
self
}
pub async fn execute(&self, client: &Client) -> crate::Result<TransactionResponse> {
self.execute_with_optional_timeout(client, None).await
}
pub async fn execute_with_timeout(
&self,
client: &Client,
timeout_per_transaction: std::time::Duration,
) -> crate::Result<TransactionResponse> {
self.execute_with_optional_timeout(client, Some(timeout_per_transaction)).await
}
async fn execute_with_optional_timeout(
&self,
client: &Client,
timeout_per_transaction: Option<std::time::Duration>,
) -> crate::Result<TransactionResponse> {
let operator_public_key = client
.load_operator()
.as_deref()
.map(|it| it.signer.public_key())
.expect("Must call `Client.set_operator` to use contract create flow");
let bytecode = split_bytecode(&self.bytecode);
let file_id = make_file_create_transaction(
bytecode.0,
operator_public_key,
self.node_account_ids.clone(),
)
.execute_with_optional_timeout(client, timeout_per_transaction)
.await?
.get_receipt_query()
.execute_with_optional_timeout(client, timeout_per_transaction)
.await?
.file_id
.expect("Creating a file means there's a file ID");
if let Some(file_append_bytecode) = bytecode.1 {
make_file_append_transaction(
file_id,
file_append_bytecode,
self.file_append_max_chunks,
self.node_account_ids.clone(),
)
.execute_all_with_optional_timeout(client, timeout_per_transaction)
.await?;
}
let response = make_contract_create_transaction(
file_id,
&self.contract_data,
self.node_account_ids.clone(),
)?
.execute_with_optional_timeout(client, timeout_per_transaction)
.await?;
response
.get_receipt_query()
.execute_with_optional_timeout(client, timeout_per_transaction)
.await?;
make_file_delete_transaction(file_id, self.node_account_ids.clone())
.execute_with_optional_timeout(client, timeout_per_transaction)
.await?
.get_receipt_query()
.execute_with_optional_timeout(client, timeout_per_transaction)
.await?;
Ok(response)
}
}
#[derive(Default, Debug)]
struct ContractData {
constructor_parameters: Vec<u8>,
gas: u64,
initial_balance: Hbar,
max_automatic_token_associations: i32,
decline_staking_reward: bool,
admin_key: Option<Key>,
auto_renew_account_id: Option<AccountId>,
auto_renew_period: Option<time::Duration>,
contract_memo: Option<String>,
staked_id: Option<StakedId>,
freeze_with_client: Option<Client>,
signer: Option<AnySigner>,
}
fn split_bytecode(bytecode: &[u8]) -> (Vec<u8>, Option<Vec<u8>>) {
const MAX_FILE_CREATE_DATA_BYTES: usize = 2048;
let bytecode = hex::encode(bytecode).into_bytes();
if bytecode.len() <= MAX_FILE_CREATE_DATA_BYTES {
return (bytecode, None);
}
let mut file_create_bytecode = bytecode;
let file_append_bytecode = file_create_bytecode.split_off(MAX_FILE_CREATE_DATA_BYTES);
(file_create_bytecode, Some(file_append_bytecode))
}
fn make_file_create_transaction(
bytecode: Vec<u8>,
key: PublicKey,
node_account_ids: Option<Vec<AccountId>>,
) -> FileCreateTransaction {
let mut tmp = FileCreateTransaction::new();
tmp.contents(bytecode).keys([key]);
if let Some(node_account_ids) = node_account_ids {
tmp.node_account_ids(node_account_ids);
}
tmp
}
fn make_file_append_transaction(
file_id: FileId,
bytecode: Vec<u8>,
max_chunks: Option<usize>,
node_account_ids: Option<Vec<AccountId>>,
) -> FileAppendTransaction {
let mut tmp = FileAppendTransaction::new();
tmp.file_id(file_id).contents(bytecode);
if let Some(max_chunks) = max_chunks {
tmp.max_chunks(max_chunks);
}
if let Some(node_account_ids) = node_account_ids {
tmp.node_account_ids(node_account_ids);
}
tmp
}
fn make_contract_create_transaction(
file_id: FileId,
data: &ContractData,
node_account_ids: Option<Vec<AccountId>>,
) -> crate::Result<ContractCreateTransaction> {
let mut tmp = ContractCreateTransaction::new();
tmp.bytecode_file_id(file_id)
.constructor_parameters(data.constructor_parameters.clone())
.gas(data.gas)
.initial_balance(data.initial_balance)
.max_automatic_token_associations(data.max_automatic_token_associations)
.decline_staking_reward(data.decline_staking_reward);
if let Some(admin_key) = &data.admin_key {
tmp.admin_key(admin_key.clone());
}
if let Some(auto_renew_account_id) = data.auto_renew_account_id {
tmp.auto_renew_account_id(auto_renew_account_id);
}
if let Some(auto_renew_period) = data.auto_renew_period {
tmp.auto_renew_period(auto_renew_period);
}
if let Some(contract_memo) = &data.contract_memo {
tmp.contract_memo(contract_memo.clone());
}
match data.staked_id {
Some(StakedId::AccountId(account_id)) => {
tmp.staked_account_id(account_id);
}
Some(StakedId::NodeId(node_id)) => {
tmp.staked_node_id(node_id);
}
None => {}
}
if let Some(node_account_ids) = node_account_ids {
tmp.node_account_ids(node_account_ids);
}
if let Some(client) = &data.freeze_with_client {
tmp.freeze_with(client)?;
}
if let Some(signer) = &data.signer {
tmp.sign_signer(signer.clone());
}
Ok(tmp)
}
fn make_file_delete_transaction(
file_id: FileId,
node_account_ids: Option<Vec<AccountId>>,
) -> FileDeleteTransaction {
let mut tmp = FileDeleteTransaction::new();
tmp.file_id(file_id);
if let Some(node_account_ids) = node_account_ids {
tmp.node_account_ids(node_account_ids);
}
tmp
}
#[cfg(test)]
mod tests {
use time::Duration;
use crate::{
AccountId,
ContractCreateFlow,
Hbar,
PrivateKey,
};
#[test]
fn get_set_bytecode() {
const BYTECODE: [u8; 3] = [2, 3, 4];
let mut flow = ContractCreateFlow::new();
flow.bytecode(BYTECODE.into());
assert_eq!(flow.get_bytecode(), &BYTECODE)
}
#[test]
fn get_set_max_chunks() {
let mut flow = ContractCreateFlow::new();
flow.max_chunks(15);
assert_eq!(flow.get_max_chunks(), Some(15));
}
#[test]
fn get_set_node_account_ids() {
const ACCOUNT_IDS: [AccountId; 3] =
[AccountId::new(1, 2, 3), AccountId::new(1, 3, 2), AccountId::new(2, 1, 3)];
let mut flow = ContractCreateFlow::new();
flow.node_account_ids(ACCOUNT_IDS);
assert_eq!(flow.get_node_account_ids(), Some(ACCOUNT_IDS.as_slice()))
}
#[test]
fn get_set_constructor_parameters() {
const PARAMS: [u8; 3] = *b"123";
let mut flow = ContractCreateFlow::new();
flow.constructor_parameters(PARAMS);
assert_eq!(flow.get_constructor_parameters(), PARAMS);
}
#[test]
fn get_set_gas() {
let mut flow = ContractCreateFlow::new();
flow.gas(31415);
assert_eq!(flow.get_gas(), 31415);
}
#[test]
fn get_set_initial_balance() {
let mut flow = ContractCreateFlow::new();
flow.initial_balance(Hbar::new(2));
assert_eq!(flow.get_initial_balance(), Hbar::new(2));
}
#[test]
fn get_set_max_automatic_token_associations() {
let mut flow = ContractCreateFlow::new();
flow.max_automatic_token_associations(15);
assert_eq!(flow.get_max_automatic_token_associations(), 15);
}
#[test]
fn get_set_decline_staking_reward() {
let mut flow = ContractCreateFlow::new();
flow.decline_staking_reward(true);
assert_eq!(flow.get_decline_staking_reward(), true);
}
#[test]
fn get_set_admin_key() {
let key = PrivateKey::generate_ecdsa().public_key();
let mut flow = ContractCreateFlow::new();
flow.admin_key(key);
assert_eq!(flow.get_admin_key(), Some(&key.into()));
}
#[test]
fn get_set_auto_renew_account_id() {
let mut flow = ContractCreateFlow::new();
flow.auto_renew_account_id(AccountId::new(0, 1, 2));
assert_eq!(flow.get_auto_renew_account_id(), Some(AccountId::new(0, 1, 2)));
}
#[test]
fn get_set_auto_renew_period() {
let mut flow = ContractCreateFlow::new();
flow.auto_renew_period(Duration::seconds(1231));
assert_eq!(flow.get_auto_renew_period(), Some(Duration::seconds(1231)));
}
#[test]
fn get_set_contract_memo() {
let mut flow = ContractCreateFlow::new();
flow.contract_memo("xyz abc".to_owned());
assert_eq!(flow.get_contract_memo(), Some("xyz abc"));
}
#[test]
fn get_set_staked_account_id() {
let mut flow = ContractCreateFlow::new();
flow.staked_account_id(AccountId::new(0, 1, 2));
assert_eq!(flow.get_staked_account_id(), Some(AccountId::new(0, 1, 2)));
}
#[test]
fn get_set_staked_node_id() {
let mut flow = ContractCreateFlow::new();
flow.staked_node_id(4);
assert_eq!(flow.get_staked_node_id(), Some(4));
}
}