Skip to main content

ant_core/data/client/
payment.rs

1//! Payment orchestration for the Autonomi client.
2//!
3//! Connects quote collection, on-chain EVM payment, and proof serialization.
4//! Every PUT to the network requires a valid payment proof.
5
6use crate::data::client::batch::SingleNodeQuotePayment;
7use crate::data::client::quote::median_paid_quote_issuer;
8use crate::data::client::Client;
9use crate::data::error::{Error, Result};
10use ant_protocol::evm::{EncodedPeerId, ProofOfPayment, Wallet};
11use ant_protocol::payment::{serialize_single_node_proof, PaymentProof};
12use ant_protocol::transport::{MultiAddr, PeerId};
13use std::sync::Arc;
14use tracing::{debug, info};
15
16/// Single-node payment pays the selected median quote at 3x its quoted price.
17pub(crate) const SINGLE_NODE_PAYMENT_MULTIPLIER: u64 = 3;
18
19impl Client {
20    /// Get the wallet, returning an error if not configured.
21    pub(crate) fn require_wallet(&self) -> Result<&Arc<Wallet>> {
22        self.wallet().ok_or_else(|| {
23            Error::Payment("Wallet not configured — call with_wallet() first".to_string())
24        })
25    }
26
27    /// Pay for storage and return the serialized payment proof bytes.
28    ///
29    /// This orchestrates the full payment flow:
30    /// 1. Collect at least one witnessed quote plus ordered PUT targets
31    /// 2. Build single-node payment using node-reported prices (median 3x, others 0)
32    /// 3. Pay on-chain via the wallet
33    /// 4. Serialize `PaymentProof` with transaction hashes
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if the wallet is not set, quotes cannot be collected,
38    /// on-chain payment fails, or serialization fails.
39    /// Returns `(proof_bytes, put_targets)`. The peer list is the ordered PUT
40    /// target set from quote planning: it starts with peers expected to accept
41    /// the paid proof and can include non-quoted fallback peers beyond the
42    /// quoted close group.
43    pub async fn pay_for_storage(
44        &self,
45        address: &[u8; 32],
46        data_size: u64,
47        data_type: u32,
48    ) -> Result<(Vec<u8>, Vec<(PeerId, Vec<MultiAddr>)>)> {
49        // Wallet is required for the on-chain payment step (step 4 below).
50        // Check early so we don't waste time collecting quotes for a misconfigured client.
51        let wallet = self.require_wallet()?;
52
53        debug!("Collecting quotes for address {}", hex::encode(address));
54
55        // 1. Collect at least one witnessed quote from the network
56        let quote_plan = self
57            .get_store_quote_plan(address, data_size, data_type)
58            .await?;
59        let quotes_with_peers = quote_plan.quotes;
60        let median_quote_issuer =
61            median_paid_quote_issuer(&quotes_with_peers).ok_or_else(|| {
62                Error::Payment(
63                    "Failed to select median quote issuer from witnessed quotes".to_string(),
64                )
65            })?;
66
67        // Capture the ordered PUT target set for replication by the caller.
68        // This can be wider than the peers that supplied the paid quotes.
69        let quoted_peers = quote_plan.put_peers;
70
71        // 2. Build peer_quotes for ProofOfPayment + quotes for single-node payment.
72        // Use node-reported prices directly — no contract price fetch needed.
73        let mut peer_quotes = Vec::with_capacity(quotes_with_peers.len());
74        let mut quotes_for_payment = Vec::with_capacity(quotes_with_peers.len());
75        // ADR-0004: forward the signed commitment each bound quote shipped, so
76        // the storers can cross-check the quote's count against the original
77        // commitment synchronously ("the commitment arrived with the quote").
78        // A baseline quote ships none. `get_store_quotes` already verified each
79        // quote's forced-price binding, so anything here is payable.
80        let mut commitment_sidecars = Vec::new();
81
82        for (peer_id, _addrs, quote, _price, commitment) in quotes_with_peers {
83            let encoded = peer_id_to_encoded(&peer_id)?;
84            peer_quotes.push((encoded, quote.clone()));
85            quotes_for_payment.push(quote);
86            if let Some(sidecar) = commitment {
87                commitment_sidecars.push(sidecar);
88            }
89        }
90
91        // 3. Create single-node payment (sorts by price, selects median)
92        let payment = SingleNodeQuotePayment::from_quotes(quotes_for_payment)
93            .map_err(|e| Error::Payment(format!("Failed to create payment: {e}")))?;
94
95        info!(
96            "Selected SNP median paid quote issuer {} for address {} (median price: {})",
97            median_quote_issuer.0,
98            hex::encode(address),
99            median_quote_issuer.1
100        );
101        info!("Payment total: {} atto", payment.total_amount());
102
103        // 4. Pay on-chain
104        let tx_hashes = payment
105            .pay(wallet)
106            .await
107            .map_err(|e| Error::Payment(format!("On-chain payment failed: {e}")))?;
108
109        info!(
110            "On-chain payment succeeded: {} transactions",
111            tx_hashes.len()
112        );
113
114        // 5. Build and serialize proof with version tag
115        let proof = PaymentProof {
116            proof_of_payment: ProofOfPayment { peer_quotes },
117            tx_hashes,
118            commitment_sidecars,
119        };
120
121        let proof_bytes = serialize_single_node_proof(&proof)
122            .map_err(|e| Error::Serialization(format!("Failed to serialize payment proof: {e}")))?;
123
124        Ok((proof_bytes, quoted_peers))
125    }
126
127    /// Approve the wallet to spend tokens on the payment vault contract.
128    ///
129    /// This must be called once before any payments can be made.
130    /// Approves `U256::MAX` (unlimited) spending.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if the wallet is not set or the approval transaction fails.
135    pub async fn approve_token_spend(&self) -> Result<()> {
136        let wallet = self.require_wallet()?;
137        let evm_network = self.require_evm_network()?;
138
139        let vault_address = evm_network.payment_vault_address();
140        wallet
141            .approve_to_spend_tokens(*vault_address, ant_protocol::evm::U256::MAX)
142            .await
143            .map_err(|e| Error::Payment(format!("Token approval failed: {e}")))?;
144        info!("Token spend approved for payment vault contract");
145
146        Ok(())
147    }
148}
149
150/// Convert an ant-node `PeerId` to an `EncodedPeerId` for payment proofs.
151pub(crate) fn peer_id_to_encoded(peer_id: &PeerId) -> Result<EncodedPeerId> {
152    Ok(EncodedPeerId::new(*peer_id.as_bytes()))
153}