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("es_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}