Skip to main content

r402_aptos/chain/
provider.rs

1//! Facilitator-side Aptos provider: fee-payer keys + `aptos-sdk` 0.6.
2
3use std::fmt::{Debug, Formatter};
4use std::sync::Arc;
5
6use aptos_sdk::account::Ed25519Account;
7use aptos_sdk::crypto::Ed25519PrivateKey;
8use aptos_sdk::transaction::PartiallySigned;
9use aptos_sdk::transaction::types::FeePayerRawTransaction;
10use aptos_sdk::types::AccountAddress;
11use aptos_sdk::{Aptos, AptosConfig};
12use r402_core::chain::{ChainId, ChainProvider};
13
14use super::codec::{AptosCodecError, DecodedAptosPayment, aptos_config_for, signed_from_decoded};
15use super::types::AptosChainReference;
16
17/// A fee-payer account this facilitator controls.
18#[derive(Clone)]
19pub struct AptosFeePayer {
20    account: Ed25519Account,
21}
22
23impl Debug for AptosFeePayer {
24    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("AptosFeePayer")
26            .field("address", &self.account.address().to_long_string())
27            .finish_non_exhaustive()
28    }
29}
30
31impl AptosFeePayer {
32    /// Constructs a fee payer from a 32-byte Ed25519 private key hex.
33    ///
34    /// # Errors
35    ///
36    /// Returns [`AptosProviderError::Parse`] if the key is invalid.
37    pub fn from_private_key_hex(private_key: impl AsRef<str>) -> Result<Self, AptosProviderError> {
38        let account = Ed25519Account::from_private_key_hex(private_key.as_ref())
39            .map_err(|e| AptosProviderError::Parse(e.to_string()))?;
40        Ok(Self { account })
41    }
42
43    /// Constructs a fee payer from an SDK Ed25519 account.
44    #[must_use]
45    pub const fn new(account: Ed25519Account) -> Self {
46        Self { account }
47    }
48
49    /// Constructs a fee payer from raw private-key bytes.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`AptosProviderError::Parse`] if the bytes are not a valid key.
54    pub fn from_private_key_bytes(bytes: &[u8]) -> Result<Self, AptosProviderError> {
55        let key = Ed25519PrivateKey::from_bytes(bytes)
56            .map_err(|e| AptosProviderError::Parse(e.to_string()))?;
57        Ok(Self {
58            account: Ed25519Account::from_private_key(key),
59        })
60    }
61
62    /// Fee-payer long-form address.
63    #[must_use]
64    pub fn address_long(&self) -> String {
65        self.account.address().to_long_string()
66    }
67
68    /// SDK account.
69    #[must_use]
70    pub const fn account(&self) -> &Ed25519Account {
71        &self.account
72    }
73}
74
75/// Errors from the Aptos facilitator provider.
76#[derive(Debug, thiserror::Error)]
77pub enum AptosProviderError {
78    /// Identifier or key could not be parsed.
79    #[error("aptos provider parse error: {0}")]
80    Parse(String),
81    /// Fullnode / view / simulate request failed.
82    #[error("aptos rpc error: {0}")]
83    Rpc(String),
84    /// Settlement submit failed.
85    #[error("aptos submit error: {0}")]
86    Submit(String),
87    /// Payload could not be decoded.
88    #[error(transparent)]
89    Codec(#[from] AptosCodecError),
90}
91
92/// Result of a simulated payment transaction.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct AptosSimulationResult {
95    /// Whether the VM would succeed.
96    pub success: bool,
97    /// VM status string.
98    pub vm_status: String,
99}
100
101/// Provider for interacting with an Aptos network as a facilitator.
102#[derive(Clone)]
103pub struct AptosChainProvider {
104    chain: AptosChainReference,
105    fee_payers: Vec<AptosFeePayer>,
106    aptos: Arc<Aptos>,
107    sponsor_transactions: bool,
108}
109
110impl Debug for AptosChainProvider {
111    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
112        f.debug_struct("AptosChainProvider")
113            .field("chain", &self.chain)
114            .field(
115                "fee_payers",
116                &self
117                    .fee_payers
118                    .iter()
119                    .map(AptosFeePayer::address_long)
120                    .collect::<Vec<_>>(),
121            )
122            .field("sponsor_transactions", &self.sponsor_transactions)
123            .finish_non_exhaustive()
124    }
125}
126
127impl AptosChainProvider {
128    /// Creates a provider for `chain` using the given fee payers and optional RPC URL.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`AptosProviderError`] if the SDK client cannot be constructed.
133    pub fn new(
134        chain: AptosChainReference,
135        fee_payers: Vec<AptosFeePayer>,
136        rpc_url: Option<&str>,
137    ) -> Result<Self, AptosProviderError> {
138        let config = aptos_config_for(chain.chain_id(), rpc_url)
139            .map_err(|e| AptosProviderError::Parse(e.to_string()))?;
140        Self::from_config(chain, fee_payers, config)
141    }
142
143    /// Creates a provider from an explicit [`AptosConfig`].
144    ///
145    /// # Errors
146    ///
147    /// Returns [`AptosProviderError::Rpc`] if the HTTP client fails to build.
148    pub fn from_config(
149        chain: AptosChainReference,
150        fee_payers: Vec<AptosFeePayer>,
151        config: AptosConfig,
152    ) -> Result<Self, AptosProviderError> {
153        let aptos = Aptos::new(config).map_err(|e| AptosProviderError::Rpc(e.to_string()))?;
154        Ok(Self {
155            chain,
156            fee_payers,
157            aptos: Arc::new(aptos),
158            sponsor_transactions: true,
159        })
160    }
161
162    /// Disables or enables fee-payer sponsorship advertised on `/supported`.
163    #[must_use]
164    pub const fn with_sponsor_transactions(mut self, sponsor: bool) -> Self {
165        self.sponsor_transactions = sponsor;
166        self
167    }
168
169    /// Whether this facilitator advertises `extra.feePayer`.
170    #[must_use]
171    pub const fn sponsor_transactions(&self) -> bool {
172        self.sponsor_transactions
173    }
174
175    /// Fee-payer long-form addresses this provider will sign for.
176    #[must_use]
177    pub fn fee_payer_addresses(&self) -> Vec<String> {
178        self.fee_payers
179            .iter()
180            .map(AptosFeePayer::address_long)
181            .collect()
182    }
183
184    /// The Aptos network this provider is bound to.
185    #[must_use]
186    pub const fn chain_reference(&self) -> AptosChainReference {
187        self.chain
188    }
189
190    /// Shared SDK client.
191    #[must_use]
192    pub fn aptos(&self) -> &Aptos {
193        self.aptos.as_ref()
194    }
195
196    /// Looks up a managed fee payer by address.
197    #[must_use]
198    pub fn fee_payer_for(&self, address: AccountAddress) -> Option<&AptosFeePayer> {
199        self.fee_payers
200            .iter()
201            .find(|p| p.account.address() == address)
202    }
203
204    /// Current fungible-asset balance of `owner` for `asset` (FA metadata).
205    ///
206    /// # Errors
207    ///
208    /// Returns [`AptosProviderError`] if addresses or the view call fail.
209    pub async fn fungible_asset_balance(
210        &self,
211        owner: &str,
212        asset: &str,
213    ) -> Result<u64, AptosProviderError> {
214        let owner = AccountAddress::from_hex(owner)
215            .map_err(|e| AptosProviderError::Parse(e.to_string()))?;
216        let asset = AccountAddress::from_hex(asset)
217            .map_err(|e| AptosProviderError::Parse(e.to_string()))?;
218        let values = self
219            .aptos
220            .view(
221                "0x1::primary_fungible_store::balance",
222                vec!["0x1::fungible_asset::Metadata".to_owned()],
223                vec![
224                    serde_json::Value::String(owner.to_long_string()),
225                    serde_json::Value::String(asset.to_long_string()),
226                ],
227            )
228            .await
229            .map_err(|e| AptosProviderError::Rpc(e.to_string()))?;
230        parse_u64_view(&values)
231    }
232
233    /// Simulates the decoded payment via `aptos-sdk` 0.6.
234    ///
235    /// # Errors
236    ///
237    /// Returns [`AptosProviderError`] if decode or simulate fails.
238    pub async fn simulate_payment(
239        &self,
240        transaction_base64: &str,
241    ) -> Result<AptosSimulationResult, AptosProviderError> {
242        let decoded = DecodedAptosPayment::from_base64(transaction_base64)?;
243        let signed = signed_from_decoded(&decoded);
244        let result = self
245            .aptos
246            .simulate_signed(&signed)
247            .await
248            .map_err(|e| AptosProviderError::Rpc(e.to_string()))?;
249        Ok(AptosSimulationResult {
250            success: result.success(),
251            vm_status: result.vm_status().to_owned(),
252        })
253    }
254
255    /// Adds the fee-payer signature when sponsored and submits via the SDK.
256    ///
257    /// # Errors
258    ///
259    /// Returns [`AptosProviderError`] if decode, signing, or submit fails.
260    pub async fn sign_and_submit(
261        &self,
262        transaction_base64: &str,
263        fee_payer: Option<&str>,
264    ) -> Result<String, AptosProviderError> {
265        let decoded = DecodedAptosPayment::from_base64(transaction_base64)?;
266        let signed = if let Some(fee_payer) = fee_payer {
267            let fee_payer_addr = AccountAddress::from_hex(fee_payer)
268                .map_err(|e| AptosProviderError::Parse(e.to_string()))?;
269            let payer = self.fee_payer_for(fee_payer_addr).ok_or_else(|| {
270                AptosProviderError::Submit(format!(
271                    "fee payer {} is not managed by this facilitator",
272                    fee_payer_addr.to_long_string()
273                ))
274            })?;
275            let mut partial = PartiallySigned::new(FeePayerRawTransaction::new_simple(
276                decoded.transaction.raw_transaction.clone(),
277                fee_payer_addr,
278            ));
279            partial.sender_auth = Some(decoded.sender_authenticator.clone());
280            partial
281                .sign_as_fee_payer(payer.account())
282                .map_err(|e| AptosProviderError::Submit(e.to_string()))?;
283            partial
284                .finalize()
285                .map_err(|e| AptosProviderError::Submit(e.to_string()))?
286        } else {
287            signed_from_decoded(&decoded)
288        };
289
290        let result = self
291            .aptos
292            .submit_and_wait(&signed, None)
293            .await
294            .map_err(|e| AptosProviderError::Submit(e.to_string()))?;
295        if let Some(hash) = result.data.get("hash").and_then(serde_json::Value::as_str) {
296            return Ok(hash.to_owned());
297        }
298        signed
299            .hash()
300            .map(|h| h.to_string())
301            .map_err(|e| AptosProviderError::Submit(e.to_string()))
302    }
303}
304
305impl ChainProvider for AptosChainProvider {
306    fn signer_addresses(&self) -> Vec<String> {
307        self.fee_payer_addresses()
308    }
309
310    fn chain_id(&self) -> ChainId {
311        self.chain.into()
312    }
313}
314
315fn parse_u64_view(values: &[serde_json::Value]) -> Result<u64, AptosProviderError> {
316    let first = values.first().ok_or_else(|| {
317        AptosProviderError::Rpc("empty view response for fungible asset balance".to_owned())
318    })?;
319    if let Some(n) = first.as_u64() {
320        return Ok(n);
321    }
322    if let Some(s) = first.as_str() {
323        return s
324            .parse()
325            .map_err(|e| AptosProviderError::Rpc(format!("invalid balance view: {e}")));
326    }
327    Err(AptosProviderError::Rpc(format!(
328        "unexpected balance view: {first}"
329    )))
330}