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        // A refusal established by any earlier upload on this client stops
50        // this one before it spends. The verdict is about this build, not
51        // about one operation.
52        if let Some(refusal) = self.corroborated_settlement_refusal() {
53            return Err(Error::ClientUpdateRequired(refusal));
54        }
55
56        // Wallet is required for the on-chain payment step (step 4 below).
57        // Check early so we don't waste time collecting quotes for a misconfigured client.
58        let wallet = self.require_wallet()?;
59
60        debug!("Collecting quotes for address {}", hex::encode(address));
61
62        // 1. Collect at least one witnessed quote from the network
63        let quote_plan = self
64            .get_store_quote_plan(address, data_size, data_type)
65            .await?;
66        let quotes_with_peers = quote_plan.quotes;
67        let median_quote_issuer =
68            median_paid_quote_issuer(&quotes_with_peers).ok_or_else(|| {
69                Error::Payment(
70                    "Failed to select median quote issuer from witnessed quotes".to_string(),
71                )
72            })?;
73
74        // Capture the ordered PUT target set for replication by the caller.
75        // This can be wider than the peers that supplied the paid quotes.
76        let quoted_peers = quote_plan.put_peers;
77
78        // 2. Build peer_quotes for ProofOfPayment + quotes for single-node payment.
79        // Use node-reported prices directly — no contract price fetch needed.
80        let mut peer_quotes = Vec::with_capacity(quotes_with_peers.len());
81        let mut quotes_for_payment = Vec::with_capacity(quotes_with_peers.len());
82        // ADR-0004: forward the signed commitment each bound quote shipped, so
83        // the storers can cross-check the quote's count against the original
84        // commitment synchronously ("the commitment arrived with the quote").
85        // A baseline quote ships none. `get_store_quotes` already verified each
86        // quote's forced-price binding, so anything here is payable.
87        let mut commitment_sidecars = Vec::new();
88
89        for (peer_id, _addrs, quote, _price, commitment) in quotes_with_peers {
90            let encoded = peer_id_to_encoded(&peer_id)?;
91            peer_quotes.push((encoded, quote.clone()));
92            quotes_for_payment.push(quote);
93            if let Some(sidecar) = commitment {
94                commitment_sidecars.push(sidecar);
95            }
96        }
97
98        // 3. Create single-node payment (sorts by price, selects median)
99        let payment = SingleNodeQuotePayment::from_quotes(quotes_for_payment)
100            .map_err(|e| Error::Payment(format!("Failed to create payment: {e}")))?;
101
102        info!(
103            "Selected SNP median paid quote issuer {} for address {} (median price: {})",
104            median_quote_issuer.0,
105            hex::encode(address),
106            median_quote_issuer.1
107        );
108        info!("Payment total: {} atto", payment.total_amount());
109
110        // 4. Pay on-chain
111        let tx_hashes = payment
112            .pay(wallet)
113            .await
114            .map_err(|e| Error::Payment(format!("On-chain payment failed: {e}")))?;
115
116        info!(
117            "On-chain payment succeeded: {} transactions",
118            tx_hashes.len()
119        );
120
121        // 5. Build and serialize proof with version tag
122        let proof = PaymentProof {
123            proof_of_payment: ProofOfPayment { peer_quotes },
124            tx_hashes,
125            commitment_sidecars,
126        };
127
128        let proof_bytes = serialize_single_node_proof(&proof)
129            .map_err(|e| Error::Serialization(format!("Failed to serialize payment proof: {e}")))?;
130
131        Ok((proof_bytes, quoted_peers))
132    }
133
134    /// Approve the wallet to spend tokens on the payment vault contract.
135    ///
136    /// This must be called once before any payments can be made.
137    /// Approves `U256::MAX` (unlimited) spending.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if the wallet is not set or the approval transaction fails.
142    pub async fn approve_token_spend(&self) -> Result<()> {
143        let wallet = self.require_wallet()?;
144        let evm_network = self.require_evm_network()?;
145
146        let vault_address = evm_network.payment_vault_address();
147        wallet
148            .approve_to_spend_tokens(*vault_address, ant_protocol::evm::U256::MAX)
149            .await
150            .map_err(|e| Error::Payment(format!("Token approval failed: {e}")))?;
151        info!("Token spend approved for payment vault contract");
152
153        Ok(())
154    }
155}
156
157/// Convert an ant-node `PeerId` to an `EncodedPeerId` for payment proofs.
158pub(crate) fn peer_id_to_encoded(peer_id: &PeerId) -> Result<EncodedPeerId> {
159    Ok(EncodedPeerId::new(*peer_id.as_bytes()))
160}