urma-runtime 0.1.0

URMA publication planning, network transports and recovery
Documentation
use crate::config;
use crate::error::{Context, Error, ensure};
use crate::publication::{PublicPlan, prepare_signed_bytes};
use crate::transaction::decode;
use crate::{
    node::Node,
    plan::{PlanLimits, PublicationPlan},
};
use sha2::{Digest, Sha256};
use std::num::NonZeroU64;
use urma_core::multipart::ChildReference;
use urma_identity::identity::IdentitySigner;
use urma_wallet::{
    funding::Funding,
    wallet::{FeeBudget, FeeRate},
};

pub(crate) struct Planner<'a, S> {
    signer: &'a S,
    funding: Funding,
    plan: PublicationPlan,
    limits: PlanLimits,
}

impl<'a, S: IdentitySigner> Planner<'a, S> {
    pub(crate) fn new(
        node: &Node,
        signer: &'a S,
        limits: PlanLimits,
        records: u32,
    ) -> Result<Self, Error> {
        ensure!(
            records <= PublicationPlan::MAX_RECORDS,
            "in-memory publication exceeds client capacity"
        );
        Self::streaming(node, signer, limits, records)
    }

    pub(crate) fn streaming(
        node: &Node,
        signer: &'a S,
        limits: PlanLimits,
        records: u32,
    ) -> Result<Self, Error> {
        ensure!(
            records > 0 && records <= limits.max_records,
            "publication exceeds caller record budget"
        );
        ensure!(
            (1..=100).contains(&limits.fee_rate) && limits.max_fee > 0,
            "invalid publication fee limits"
        );
        let minimum = limits
            .max_fee
            .checked_add((u64::from(records) + 1) * config::publication_return(node.chain()))
            .context("funding budget overflow")?;
        let funding = node.select_funding(signer, minimum)?;
        Ok(Self {
            signer,
            funding,
            limits,
            plan: PublicationPlan {
                version: 1,
                chain: node.chain(),
                author: signer.public_key().inner.x_only_public_key().0.to_string(),
                root_txid: String::new(),
                total_fee: 0,
                maximum_fee: limits.max_fee,
                records: Vec::new(),
            },
        })
    }

    pub(crate) fn append(&mut self, record: &[u8]) -> Result<ChildReference, Error> {
        let (reference, pair) = self.prepare_record(record)?;
        self.plan.records.push(pair);
        Ok(reference)
    }

    pub(crate) fn prepare_record(
        &mut self,
        record: &[u8],
    ) -> Result<(ChildReference, PublicPlan), Error> {
        let budget = FeeBudget {
            chain: self.plan.chain.genesis()?,
            rate: FeeRate(NonZeroU64::new(self.limits.fee_rate).context("zero fee rate")?),
            maximum_base_units: self.limits.max_fee,
        };
        let prepared = prepare_signed_bytes(
            record,
            self.signer,
            self.funding.clone(),
            self.plan.chain,
            budget,
        )?;
        let pair = prepared.plan;
        let fee = prepared.fee;
        let reveal = decode(
            &pair.reveal,
            usize::try_from(config::STANDARD_TX_WEIGHT)? * 2,
        )?;
        let total_fee = self
            .plan
            .total_fee
            .checked_add(fee)
            .context("fee sum overflow")?;
        ensure!(
            total_fee <= self.limits.max_fee,
            "publication exceeds approved maximum fee"
        );
        self.plan.total_fee = total_fee;
        self.funding = Funding {
            raw_transaction: pair.commit.clone(),
            vout: 1,
        };
        self.plan.root_txid = reveal.compute_txid().to_string();
        let reference = ChildReference {
            txid: reveal.compute_txid(),
            record_hash: Sha256::digest(record).into(),
        };
        Ok((reference, pair))
    }

    pub(crate) fn metadata(&self) -> &PublicationPlan {
        &self.plan
    }

    pub(crate) fn finish(self) -> Result<PublicationPlan, Error> {
        self.plan.validate()?;
        Ok(self.plan)
    }
}