casper-client 5.0.1

A client library and binary for interacting with the Casper network
Documentation
mod error;

use casper_types::{
    bytesrepr::ToBytes, Deploy, DeployHash, DeployHeader, Digest, ExecutableDeployItem,
    InitiatorAddr, PublicKey, SecretKey, TimeDiff, Timestamp, TransferTarget, URef, U512,
};
pub use error::DeployBuilderError;
use itertools::Itertools;

use crate::types::InitiatorAddrAndSecretKey;

/// A builder for constructing a [`Deploy`].
pub struct DeployBuilder<'a> {
    account: Option<PublicKey>,
    secret_key: Option<&'a SecretKey>,
    timestamp: Timestamp,
    ttl: TimeDiff,
    gas_price: u64,
    dependencies: Vec<DeployHash>,
    chain_name: String,
    payment: Option<ExecutableDeployItem>,
    session: ExecutableDeployItem,
}

impl<'a> DeployBuilder<'a> {
    /// The default time-to-live for `Deploy`s, i.e. 30 minutes.
    pub const DEFAULT_TTL: TimeDiff = TimeDiff::from_millis(30 * 60 * 1_000);
    /// The default gas price for `Deploy`s, i.e. `1`.
    pub const DEFAULT_GAS_PRICE: u64 = 1;

    /// Returns a new `DeployBuilder`.
    ///
    /// # Note
    ///
    /// Before calling [`build`](Self::build), you must ensure
    ///   * that an account is provided by either calling [`with_account`](Self::with_account) or
    ///     [`with_secret_key`](Self::with_secret_key)
    ///   * that payment code is provided by calling
    ///     [`with_payment`](Self::with_payment)
    pub fn new<C: Into<String>>(chain_name: C, session: ExecutableDeployItem) -> Self {
        #[cfg(any(feature = "std-fs-io", test))]
        let timestamp = Timestamp::now();
        #[cfg(not(any(feature = "std-fs-io", test)))]
        let timestamp = Timestamp::zero();

        DeployBuilder {
            account: None,
            secret_key: None,
            timestamp,
            ttl: Self::DEFAULT_TTL,
            gas_price: Self::DEFAULT_GAS_PRICE,
            dependencies: vec![],
            chain_name: chain_name.into(),
            payment: None,
            session,
        }
    }

    /// Returns a new `DeployBuilder` with session code suitable for a transfer.
    ///
    /// If `maybe_source` is None, the account's main purse is used as the source of the transfer.
    ///
    /// # Note
    ///
    /// Before calling [`build`](Self::build), you must ensure
    ///   * that an account is provided by either calling [`with_account`](Self::with_account) or
    ///     [`with_secret_key`](Self::with_secret_key)
    ///   * that payment code is provided by calling
    ///     [`with_payment`](Self::with_payment)
    pub fn new_transfer<C: Into<String>, A: Into<U512>, T: Into<TransferTarget>>(
        chain_name: C,
        amount: A,
        maybe_source: Option<URef>,
        target: T,
        maybe_transfer_id: Option<u64>,
    ) -> Self {
        let session =
            ExecutableDeployItem::new_transfer(amount, maybe_source, target, maybe_transfer_id);
        DeployBuilder::new(chain_name, session)
    }

    /// Sets the `account` in the `Deploy`.
    ///
    /// If not provided, the public key derived from the secret key used in the `DeployBuilder` will
    /// be used as the `account` in the `Deploy`.
    pub fn with_account(mut self, account: PublicKey) -> Self {
        self.account = Some(account);
        self
    }

    /// Sets the gas price in the `Deploy` to the provided amount.
    ///
    /// If not provided, the `Deploy` will use `DEFAULT_GAS_PRICE` (1) as the gas price for the
    /// `deploy`
    pub fn with_gas_price(mut self, gas_price: u64) -> Self {
        self.gas_price = gas_price;
        self
    }

    /// Sets the secret key used to sign the `Deploy` on calling [`build`](Self::build).
    ///
    /// If not provided, the `Deploy` can still be built, but will be unsigned and will be invalid
    /// until subsequently signed.
    pub fn with_secret_key(mut self, secret_key: &'a SecretKey) -> Self {
        self.secret_key = Some(secret_key);
        self
    }

    /// Sets the `payment` in the `Deploy`.
    pub fn with_payment(mut self, payment: ExecutableDeployItem) -> Self {
        self.payment = Some(payment);
        self
    }

    /// Sets the `timestamp` in the `Deploy`.
    ///
    /// If not provided, the timestamp will be set to the time when the `DeployBuilder` was
    /// constructed.
    pub fn with_timestamp(mut self, timestamp: Timestamp) -> Self {
        self.timestamp = timestamp;
        self
    }

    /// Sets the `ttl` (time-to-live) in the `Deploy`.
    ///
    /// If not provided, the ttl will be set to [`Self::DEFAULT_TTL`].
    pub fn with_ttl(mut self, ttl: TimeDiff) -> Self {
        self.ttl = ttl;
        self
    }

    #[allow(clippy::too_many_arguments)]
    fn build_deploy_inner(
        timestamp: Timestamp,
        ttl: TimeDiff,
        gas_price: u64,
        dependencies: Vec<DeployHash>,
        chain_name: String,
        payment: ExecutableDeployItem,
        session: ExecutableDeployItem,
        initiator_addr_and_secret_key: InitiatorAddrAndSecretKey,
    ) -> Deploy {
        let serialized_body = serialize_body(&payment, &session);
        let body_hash = Digest::hash(serialized_body);

        let account = match initiator_addr_and_secret_key.initiator_addr() {
            InitiatorAddr::PublicKey(public_key) => public_key,
            InitiatorAddr::AccountHash(_) => unreachable!(),
        };

        let dependencies = dependencies.into_iter().unique().collect();
        let header = DeployHeader::new(
            account,
            timestamp,
            ttl,
            gas_price,
            body_hash,
            dependencies,
            chain_name,
        );
        let serialized_header = serialize_header(&header);
        let hash = DeployHash::new(Digest::hash(serialized_header));

        let mut deploy = Deploy::new(hash, header, payment, session);

        if let Some(secret_key) = initiator_addr_and_secret_key.secret_key() {
            deploy.sign(secret_key);
        }
        deploy
    }

    /// Returns the new `Deploy`, or an error if
    /// [`with_payment`](Self::with_payment) wasn't previously called.
    pub fn build(self) -> Result<Deploy, DeployBuilderError> {
        let initiator_addr_and_secret_key = match (self.account, self.secret_key) {
            (Some(account), Some(secret_key)) => InitiatorAddrAndSecretKey::Both {
                initiator_addr: InitiatorAddr::PublicKey(account),
                secret_key,
            },
            (Some(account), None) => {
                InitiatorAddrAndSecretKey::InitiatorAddr(InitiatorAddr::PublicKey(account))
            }
            (None, Some(secret_key)) => InitiatorAddrAndSecretKey::SecretKey(secret_key),
            (None, None) => return Err(DeployBuilderError::DeployMissingSessionAccount),
        };

        let payment = self
            .payment
            .ok_or(DeployBuilderError::DeployMissingPaymentCode)?;
        let deploy = Self::build_deploy_inner(
            self.timestamp,
            self.ttl,
            self.gas_price,
            self.dependencies,
            self.chain_name,
            payment,
            self.session,
            initiator_addr_and_secret_key,
        );
        Ok(deploy)
    }
}

fn serialize_header(header: &DeployHeader) -> Vec<u8> {
    header
        .to_bytes()
        .unwrap_or_else(|error| panic!("should serialize deploy header: {}", error))
}

fn serialize_body(payment: &ExecutableDeployItem, session: &ExecutableDeployItem) -> Vec<u8> {
    let mut buffer = Vec::with_capacity(payment.serialized_length() + session.serialized_length());
    payment
        .write_bytes(&mut buffer)
        .unwrap_or_else(|error| panic!("should serialize payment code: {}", error));
    session
        .write_bytes(&mut buffer)
        .unwrap_or_else(|error| panic!("should serialize session code: {}", error));
    buffer
}