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::quote::median_paid_quote_issuer;
7use crate::data::client::Client;
8use crate::data::error::{Error, Result};
9use ant_protocol::evm::{EncodedPeerId, ProofOfPayment, Wallet};
10use ant_protocol::payment::{serialize_single_node_proof, PaymentProof, SingleNodePayment};
11use ant_protocol::transport::{MultiAddr, PeerId};
12use std::sync::Arc;
13use tracing::{debug, info};
14
15impl Client {
16    /// Get the wallet, returning an error if not configured.
17    pub(crate) fn require_wallet(&self) -> Result<&Arc<Wallet>> {
18        self.wallet().ok_or_else(|| {
19            Error::Payment("Wallet not configured — call with_wallet() first".to_string())
20        })
21    }
22
23    /// Pay for storage and return the serialized payment proof bytes.
24    ///
25    /// This orchestrates the full payment flow:
26    /// 1. Collect `CLOSE_GROUP_SIZE` quotes from the witnessed close group
27    /// 2. Build `SingleNodePayment` using node-reported prices (median 3x, others 0)
28    /// 3. Pay on-chain via the wallet
29    /// 4. Serialize `PaymentProof` with transaction hashes
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if the wallet is not set, quotes cannot be collected,
34    /// on-chain payment fails, or serialization fails.
35    /// Returns `(proof_bytes, quoted_peers)`. `quoted_peers` are the
36    /// `CLOSE_GROUP_SIZE` peers that provided quotes — callers should store
37    /// the chunk to at least `CLOSE_GROUP_MAJORITY` of these peers.
38    pub async fn pay_for_storage(
39        &self,
40        address: &[u8; 32],
41        data_size: u64,
42        data_type: u32,
43    ) -> Result<(Vec<u8>, Vec<(PeerId, Vec<MultiAddr>)>)> {
44        // Wallet is required for the on-chain payment step (step 4 below).
45        // Check early so we don't waste time collecting quotes for a misconfigured client.
46        let wallet = self.require_wallet()?;
47
48        debug!("Collecting quotes for address {}", hex::encode(address));
49
50        // 1. Collect quotes from network
51        let quote_plan = self
52            .get_store_quote_plan(address, data_size, data_type)
53            .await?;
54        let quotes_with_peers = quote_plan.quotes;
55        let median_quote_issuer =
56            median_paid_quote_issuer(&quotes_with_peers).ok_or_else(|| {
57                Error::Payment(
58                    "Failed to select median quote issuer from witnessed quotes".to_string(),
59                )
60            })?;
61
62        // Capture all quoted peers for replication by the caller.
63        let quoted_peers = quote_plan.put_peers;
64
65        // 2. Build peer_quotes for ProofOfPayment + quotes for SingleNodePayment.
66        // Use node-reported prices directly — no contract price fetch needed.
67        let mut peer_quotes = Vec::with_capacity(quotes_with_peers.len());
68        let mut quotes_for_payment = Vec::with_capacity(quotes_with_peers.len());
69
70        for (peer_id, _addrs, quote, price) in quotes_with_peers {
71            let encoded = peer_id_to_encoded(&peer_id)?;
72            peer_quotes.push((encoded, quote.clone()));
73            quotes_for_payment.push((quote, price));
74        }
75
76        // 3. Create SingleNodePayment (sorts by price, selects median)
77        let payment = SingleNodePayment::from_quotes(quotes_for_payment)
78            .map_err(|e| Error::Payment(format!("Failed to create payment: {e}")))?;
79
80        info!(
81            "Selected SNP median paid quote issuer {} for address {} (median price: {})",
82            median_quote_issuer.0,
83            hex::encode(address),
84            median_quote_issuer.1
85        );
86        info!("Payment total: {} atto", payment.total_amount());
87
88        // 4. Pay on-chain
89        let tx_hashes = payment
90            .pay(wallet)
91            .await
92            .map_err(|e| Error::Payment(format!("On-chain payment failed: {e}")))?;
93
94        info!(
95            "On-chain payment succeeded: {} transactions",
96            tx_hashes.len()
97        );
98
99        // 5. Build and serialize proof with version tag
100        let proof = PaymentProof {
101            proof_of_payment: ProofOfPayment { peer_quotes },
102            tx_hashes,
103        };
104
105        let proof_bytes = serialize_single_node_proof(&proof)
106            .map_err(|e| Error::Serialization(format!("Failed to serialize payment proof: {e}")))?;
107
108        Ok((proof_bytes, quoted_peers))
109    }
110
111    /// Approve the wallet to spend tokens on the payment vault contract.
112    ///
113    /// This must be called once before any payments can be made.
114    /// Approves `U256::MAX` (unlimited) spending.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the wallet is not set or the approval transaction fails.
119    pub async fn approve_token_spend(&self) -> Result<()> {
120        let wallet = self.require_wallet()?;
121        let evm_network = self.require_evm_network()?;
122
123        let vault_address = evm_network.payment_vault_address();
124        wallet
125            .approve_to_spend_tokens(*vault_address, ant_protocol::evm::U256::MAX)
126            .await
127            .map_err(|e| Error::Payment(format!("Token approval failed: {e}")))?;
128        info!("Token spend approved for payment vault contract");
129
130        Ok(())
131    }
132}
133
134/// Convert an ant-node `PeerId` to an `EncodedPeerId` for payment proofs.
135pub(crate) fn peer_id_to_encoded(peer_id: &PeerId) -> Result<EncodedPeerId> {
136    Ok(EncodedPeerId::new(*peer_id.as_bytes()))
137}