openzeppelin_relayer/services/stellar_fee_forwarder/
mod.rs

1//! FeeForwarder Service for Soroban Gas Abstraction
2//!
3//! This module provides functionality to build and manage transactions
4//! using the FeeForwarder contract for gas abstraction on Soroban.
5//!
6//! The FeeForwarder contract enables fee abstraction by allowing users to pay
7//! relayers in tokens instead of native XLM. It atomically:
8//! 1. Collects fee payment from user
9//! 2. Forwards the call to the target contract
10//!
11//! ## Authorization Flow
12//!
13//! User signs authorization for `fee_forwarder.forward()` with sub-invocations:
14//! - `fee_token.approve(user, fee_forwarder, max_fee_amount, expiration_ledger)`
15//! - `target_contract.target_fn(target_args)` (if target requires auth)
16
17use crate::constants::STELLAR_LEDGER_TIME_SECONDS;
18use crate::services::provider::StellarProviderTrait;
19use soroban_rs::xdr::{
20    ContractId, Hash, Int128Parts, InvokeContractArgs, Limits, Operation, OperationBody, ScAddress,
21    ScSymbol, ScVal, ScVec, SorobanAddressCredentials, SorobanAuthorizationEntry,
22    SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, VecM, WriteXdr,
23};
24use std::sync::Arc;
25use thiserror::Error;
26
27/// Default validity duration for gas abstraction authorizations (2 minutes).
28///
29/// Matches sponsored transaction validity so authorization expiration aligns with the submission window.
30pub const DEFAULT_VALIDITY_SECONDS: u64 = 120;
31
32/// Errors that can occur in FeeForwarder operations
33#[derive(Error, Debug)]
34pub enum FeeForwarderError {
35    #[error("Invalid contract address: {0}")]
36    InvalidContractAddress(String),
37
38    #[error("Invalid account address: {0}")]
39    InvalidAccountAddress(String),
40
41    #[error("Failed to build authorization: {0}")]
42    AuthorizationBuildError(String),
43
44    #[error("Provider error: {0}")]
45    ProviderError(String),
46
47    #[error("XDR serialization error: {0}")]
48    XdrError(String),
49
50    #[error("Invalid function name: {0}")]
51    InvalidFunctionName(String),
52}
53
54/// Parameters for building a FeeForwarder transaction
55#[derive(Debug, Clone)]
56pub struct FeeForwarderParams {
57    /// Soroban token contract address for fee payment
58    pub fee_token: String,
59    /// Actual fee amount to charge (determined at submission time)
60    pub fee_amount: i128,
61    /// Maximum fee amount user authorized
62    pub max_fee_amount: i128,
63    /// Authorization expiration ledger
64    pub expiration_ledger: u32,
65    /// Target contract address to call
66    pub target_contract: String,
67    /// Target function name
68    pub target_fn: String,
69    /// Target function arguments
70    pub target_args: Vec<ScVal>,
71    /// User's Stellar address
72    pub user: String,
73    /// Relayer's Stellar address (fee recipient)
74    pub relayer: String,
75}
76
77/// Service for building FeeForwarder transactions
78pub struct FeeForwarderService<P>
79where
80    P: StellarProviderTrait + Send + Sync,
81{
82    /// FeeForwarder contract address
83    fee_forwarder_address: String,
84    /// Stellar provider for network queries
85    provider: Arc<P>,
86}
87
88impl<P> FeeForwarderService<P>
89where
90    P: StellarProviderTrait + Send + Sync,
91{
92    /// Create a new FeeForwarderService
93    ///
94    /// # Arguments
95    ///
96    /// * `fee_forwarder_address` - The deployed FeeForwarder contract address
97    /// * `provider` - Stellar provider for network queries
98    pub fn new(fee_forwarder_address: String, provider: Arc<P>) -> Self {
99        Self {
100            fee_forwarder_address,
101            provider,
102        }
103    }
104
105    /// Build a user authorization entry without requiring a FeeForwarderService instance
106    ///
107    /// This static method is useful when you don't have an `Arc<P>` provider
108    /// but still need to build authorization entries for the FeeForwarder.
109    pub fn build_user_auth_entry_standalone(
110        fee_forwarder_address: &str,
111        params: &FeeForwarderParams,
112        requires_target_auth: bool,
113    ) -> Result<SorobanAuthorizationEntry, FeeForwarderError> {
114        let fee_forwarder_addr = Self::parse_contract_address(fee_forwarder_address)?;
115        let fee_token_addr = Self::parse_contract_address(&params.fee_token)?;
116        let target_contract_addr = Self::parse_contract_address(&params.target_contract)?;
117        let user_addr = Self::parse_account_address(&params.user)?;
118        let _relayer_addr = Self::parse_account_address(&params.relayer)?;
119
120        // Build sub-invocations
121        let mut sub_invocations = Vec::new();
122
123        // 1. fee_token.approve(user, fee_forwarder, max_fee_amount, expiration_ledger)
124        // Note: When called from another contract, approve requires 'from' address as first arg
125        let approve_args: ScVec = vec![
126            ScVal::Address(user_addr.clone()),
127            ScVal::Address(fee_forwarder_addr.clone()),
128            Self::i128_to_scval(params.max_fee_amount),
129            ScVal::U32(params.expiration_ledger),
130        ]
131        .try_into()
132        .map_err(|_| {
133            FeeForwarderError::AuthorizationBuildError("Failed to create approve args".to_string())
134        })?;
135
136        sub_invocations.push(SorobanAuthorizedInvocation {
137            function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
138                contract_address: fee_token_addr,
139                function_name: Self::create_symbol("approve")?,
140                args: approve_args.into(),
141            }),
142            sub_invocations: VecM::default(),
143        });
144
145        // 2. target_contract.target_fn(target_args) - if needed
146        if requires_target_auth {
147            let target_args: ScVec = params.target_args.clone().try_into().map_err(|_| {
148                FeeForwarderError::AuthorizationBuildError(
149                    "Failed to create target args".to_string(),
150                )
151            })?;
152
153            sub_invocations.push(SorobanAuthorizedInvocation {
154                function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
155                    contract_address: target_contract_addr,
156                    function_name: Self::create_symbol(&params.target_fn)?,
157                    args: target_args.into(),
158                }),
159                sub_invocations: VecM::default(),
160            });
161        }
162
163        // Build the forward() function arguments for USER (6 parameters)
164        // User signs: fee_token, max_fee_amount, expiration_ledger, target_contract, target_fn, target_args
165        // User does NOT sign: fee_amount, user, relayer
166        let user_auth_args = Self::build_user_auth_args_standalone(params)?;
167
168        // Build the root invocation for fee_forwarder.forward()
169        let root_invocation = SorobanAuthorizedInvocation {
170            function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
171                contract_address: fee_forwarder_addr,
172                function_name: Self::create_symbol("forward")?,
173                args: user_auth_args.into(),
174            }),
175            sub_invocations: sub_invocations.try_into().map_err(|_| {
176                FeeForwarderError::AuthorizationBuildError(
177                    "Failed to create sub-invocations".to_string(),
178                )
179            })?,
180        };
181
182        // Generate nonce using timestamp combined with randomness for uniqueness
183        let nonce = {
184            use rand::Rng;
185            use std::time::{SystemTime, UNIX_EPOCH};
186            let timestamp = SystemTime::now()
187                .duration_since(UNIX_EPOCH)
188                .map(|d| d.as_nanos() as i64)
189                .unwrap_or(0);
190            let random: i64 = rand::rng().random();
191            timestamp ^ random
192        };
193
194        // For simulation, signature must be an empty vector (not Void)
195        // Void causes Error(Value, UnexpectedType) during auth verification
196        let empty_signature = ScVal::Vec(Some(ScVec::default()));
197
198        Ok(SorobanAuthorizationEntry {
199            credentials: SorobanCredentials::Address(SorobanAddressCredentials {
200                address: user_addr,
201                nonce,
202                signature_expiration_ledger: params.expiration_ledger,
203                signature: empty_signature,
204            }),
205            root_invocation,
206        })
207    }
208
209    /// Build a relayer authorization entry without requiring a FeeForwarderService instance
210    ///
211    /// The FeeForwarder contract requires the relayer to authorize receiving the fee payment.
212    /// This creates an auth entry for the relayer's authorization of the `forward` call.
213    ///
214    /// # Arguments
215    ///
216    /// * `fee_forwarder_address` - The FeeForwarder contract address
217    /// * `params` - FeeForwarder transaction parameters
218    ///
219    /// # Returns
220    ///
221    /// A SorobanAuthorizationEntry for the relayer (with empty signature for simulation)
222    pub fn build_relayer_auth_entry_standalone(
223        fee_forwarder_address: &str,
224        params: &FeeForwarderParams,
225    ) -> Result<SorobanAuthorizationEntry, FeeForwarderError> {
226        let fee_forwarder_addr = Self::parse_contract_address(fee_forwarder_address)?;
227        let relayer_addr = Self::parse_account_address(&params.relayer)?;
228
229        // Build the forward() function arguments
230        let forward_args = Self::build_forward_args_standalone(fee_forwarder_address, params)?;
231
232        // Build the root invocation for fee_forwarder.forward()
233        // Relayer only needs to authorize the forward call itself (no sub-invocations)
234        let root_invocation = SorobanAuthorizedInvocation {
235            function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
236                contract_address: fee_forwarder_addr,
237                function_name: Self::create_symbol("forward")?,
238                args: forward_args.into(),
239            }),
240            sub_invocations: VecM::default(),
241        };
242
243        // Generate nonce using timestamp combined with randomness for uniqueness
244        let nonce = {
245            use rand::Rng;
246            use std::time::{SystemTime, UNIX_EPOCH};
247            let timestamp = SystemTime::now()
248                .duration_since(UNIX_EPOCH)
249                .map(|d| d.as_nanos() as i64)
250                .unwrap_or(0);
251            let random: i64 = rand::rng().random();
252            timestamp ^ random
253        };
254
255        // For simulation, signature must be an empty vector (not Void)
256        // Void causes Error(Value, UnexpectedType) during auth verification
257        let empty_signature = ScVal::Vec(Some(ScVec::default()));
258
259        Ok(SorobanAuthorizationEntry {
260            credentials: SorobanCredentials::Address(SorobanAddressCredentials {
261                address: relayer_addr,
262                nonce,
263                signature_expiration_ledger: params.expiration_ledger,
264                signature: empty_signature,
265            }),
266            root_invocation,
267        })
268    }
269
270    /// Build forward args for USER authorization (6 parameters)
271    ///
272    /// User signs: fee_token, max_fee_amount, expiration_ledger, target_contract, target_fn, target_args
273    /// User does NOT sign: fee_amount, user, relayer
274    fn build_user_auth_args_standalone(
275        params: &FeeForwarderParams,
276    ) -> Result<ScVec, FeeForwarderError> {
277        let fee_token_addr = Self::parse_contract_address(&params.fee_token)?;
278        let target_contract_addr = Self::parse_contract_address(&params.target_contract)?;
279
280        let target_args_vec: ScVec = params.target_args.clone().try_into().map_err(|_| {
281            FeeForwarderError::AuthorizationBuildError("Failed to create target args".to_string())
282        })?;
283
284        // User signs 6 parameters (excludes fee_amount, user, relayer)
285        let args: Vec<ScVal> = vec![
286            ScVal::Address(fee_token_addr),
287            Self::i128_to_scval(params.max_fee_amount),
288            ScVal::U32(params.expiration_ledger),
289            ScVal::Address(target_contract_addr),
290            ScVal::Symbol(Self::create_symbol(&params.target_fn)?),
291            ScVal::Vec(Some(target_args_vec)),
292        ];
293
294        args.try_into().map_err(|_| {
295            FeeForwarderError::AuthorizationBuildError(
296                "Failed to create user auth args".to_string(),
297            )
298        })
299    }
300
301    /// Build forward args for RELAYER authorization and invoke operation (9 parameters)
302    ///
303    /// Relayer signs ALL parameters in the exact order they appear in the function signature:
304    /// fee_token, fee_amount, max_fee_amount, expiration_ledger, target_contract, target_fn, target_args, user, relayer
305    fn build_forward_args_standalone(
306        _fee_forwarder_address: &str,
307        params: &FeeForwarderParams,
308    ) -> Result<ScVec, FeeForwarderError> {
309        let fee_token_addr = Self::parse_contract_address(&params.fee_token)?;
310        let target_contract_addr = Self::parse_contract_address(&params.target_contract)?;
311        let user_addr = Self::parse_account_address(&params.user)?;
312        let relayer_addr = Self::parse_account_address(&params.relayer)?;
313
314        let target_args_vec: ScVec = params.target_args.clone().try_into().map_err(|_| {
315            FeeForwarderError::AuthorizationBuildError("Failed to create target args".to_string())
316        })?;
317
318        // Relayer signs all 9 parameters
319        let args: Vec<ScVal> = vec![
320            ScVal::Address(fee_token_addr),
321            Self::i128_to_scval(params.fee_amount),
322            Self::i128_to_scval(params.max_fee_amount),
323            ScVal::U32(params.expiration_ledger),
324            ScVal::Address(target_contract_addr),
325            ScVal::Symbol(Self::create_symbol(&params.target_fn)?),
326            ScVal::Vec(Some(target_args_vec)),
327            ScVal::Address(user_addr),
328            ScVal::Address(relayer_addr),
329        ];
330
331        args.try_into().map_err(|_| {
332            FeeForwarderError::AuthorizationBuildError("Failed to create forward args".to_string())
333        })
334    }
335
336    /// Get the FeeForwarder contract address
337    pub fn fee_forwarder_address(&self) -> &str {
338        &self.fee_forwarder_address
339    }
340
341    /// Public wrapper for building forward args (for serializing InvokeContractArgs)
342    pub fn build_forward_args_for_invoke_contract_args(
343        fee_forwarder_address: &str,
344        params: &FeeForwarderParams,
345    ) -> Result<ScVec, FeeForwarderError> {
346        Self::build_forward_args_standalone(fee_forwarder_address, params)
347    }
348
349    /// Public wrapper for parsing a contract address
350    pub fn parse_contract_address_public(address: &str) -> Result<ScAddress, FeeForwarderError> {
351        Self::parse_contract_address(address)
352    }
353
354    /// Calculate the expiration ledger for authorization
355    ///
356    /// # Arguments
357    ///
358    /// * `validity_seconds` - How long the authorization should be valid
359    ///
360    /// # Returns
361    ///
362    /// The ledger number when the authorization expires
363    pub async fn get_expiration_ledger(
364        &self,
365        validity_seconds: u64,
366    ) -> Result<u32, FeeForwarderError> {
367        let current_ledger = self
368            .provider
369            .get_latest_ledger()
370            .await
371            .map_err(|e| FeeForwarderError::ProviderError(e.to_string()))?;
372
373        let ledgers_to_add = validity_seconds / STELLAR_LEDGER_TIME_SECONDS;
374        Ok(current_ledger.sequence + ledgers_to_add as u32)
375    }
376
377    /// Parse a Soroban contract address (C...) to ScAddress
378    fn parse_contract_address(address: &str) -> Result<ScAddress, FeeForwarderError> {
379        let contract = stellar_strkey::Contract::from_string(address).map_err(|e| {
380            FeeForwarderError::InvalidContractAddress(format!("Invalid contract '{address}': {e}"))
381        })?;
382
383        Ok(ScAddress::Contract(ContractId(Hash(contract.0))))
384    }
385
386    /// Parse a Stellar account address (G...) to ScAddress
387    fn parse_account_address(address: &str) -> Result<ScAddress, FeeForwarderError> {
388        let account = stellar_strkey::ed25519::PublicKey::from_string(address).map_err(|e| {
389            FeeForwarderError::InvalidAccountAddress(format!("Invalid account '{address}': {e}"))
390        })?;
391
392        Ok(ScAddress::Account(soroban_rs::xdr::AccountId(
393            soroban_rs::xdr::PublicKey::PublicKeyTypeEd25519(soroban_rs::xdr::Uint256(account.0)),
394        )))
395    }
396
397    /// Convert i128 to ScVal::I128
398    fn i128_to_scval(amount: i128) -> ScVal {
399        let hi = (amount >> 64) as i64;
400        let lo = amount as u64;
401        ScVal::I128(Int128Parts { hi, lo })
402    }
403
404    /// Create a ScSymbol from a function name
405    fn create_symbol(name: &str) -> Result<ScSymbol, FeeForwarderError> {
406        ScSymbol::try_from(name.as_bytes().to_vec())
407            .map_err(|_| FeeForwarderError::InvalidFunctionName(name.to_string()))
408    }
409
410    /// Build the user authorization entry for FeeForwarder.forward()
411    ///
412    /// This creates the authorization structure that the user needs to sign.
413    /// The authorization includes sub-invocations for:
414    /// - fee_token.approve(user, fee_forwarder, max_fee_amount, expiration_ledger)
415    /// - target_contract.target_fn(target_args) (if requires_target_auth is true)
416    ///
417    /// # Arguments
418    ///
419    /// * `params` - FeeForwarder transaction parameters
420    /// * `requires_target_auth` - Whether the target contract call requires user authorization
421    ///
422    /// # Returns
423    ///
424    /// A SorobanAuthorizationEntry that the user needs to sign
425    pub fn build_user_auth_entry(
426        &self,
427        params: &FeeForwarderParams,
428        requires_target_auth: bool,
429    ) -> Result<SorobanAuthorizationEntry, FeeForwarderError> {
430        let fee_forwarder_addr = Self::parse_contract_address(&self.fee_forwarder_address)?;
431        let fee_token_addr = Self::parse_contract_address(&params.fee_token)?;
432        let target_contract_addr = Self::parse_contract_address(&params.target_contract)?;
433        let user_addr = Self::parse_account_address(&params.user)?;
434        // Validate relayer address is valid (used later in build_forward_args)
435        let _relayer_addr = Self::parse_account_address(&params.relayer)?;
436
437        // Build sub-invocations
438        let mut sub_invocations = Vec::new();
439
440        // 1. fee_token.approve(user, fee_forwarder, max_fee_amount, expiration_ledger)
441        // Note: When called from another contract, approve requires 'from' address as first arg
442        let approve_args: ScVec = vec![
443            ScVal::Address(user_addr.clone()),
444            ScVal::Address(fee_forwarder_addr.clone()),
445            Self::i128_to_scval(params.max_fee_amount),
446            ScVal::U32(params.expiration_ledger),
447        ]
448        .try_into()
449        .map_err(|_| {
450            FeeForwarderError::AuthorizationBuildError("Failed to create approve args".to_string())
451        })?;
452
453        sub_invocations.push(SorobanAuthorizedInvocation {
454            function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
455                contract_address: fee_token_addr,
456                function_name: Self::create_symbol("approve")?,
457                args: approve_args.into(),
458            }),
459            sub_invocations: VecM::default(),
460        });
461
462        // 2. target_contract.target_fn(target_args) - if needed
463        if requires_target_auth {
464            let target_args: ScVec = params.target_args.clone().try_into().map_err(|_| {
465                FeeForwarderError::AuthorizationBuildError(
466                    "Failed to create target args".to_string(),
467                )
468            })?;
469
470            sub_invocations.push(SorobanAuthorizedInvocation {
471                function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
472                    contract_address: target_contract_addr.clone(),
473                    function_name: Self::create_symbol(&params.target_fn)?,
474                    args: target_args.into(),
475                }),
476                sub_invocations: VecM::default(),
477            });
478        }
479
480        // Build the forward() function arguments for USER (6 parameters)
481        // User signs: fee_token, max_fee_amount, expiration_ledger, target_contract, target_fn, target_args
482        // User does NOT sign: fee_amount, user, relayer
483        let user_auth_args = Self::build_user_auth_args_standalone(params)?;
484
485        // Build the root invocation for fee_forwarder.forward()
486        let root_invocation = SorobanAuthorizedInvocation {
487            function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
488                contract_address: fee_forwarder_addr,
489                function_name: Self::create_symbol("forward")?,
490                args: user_auth_args.into(),
491            }),
492            sub_invocations: sub_invocations.try_into().map_err(|_| {
493                FeeForwarderError::AuthorizationBuildError(
494                    "Failed to create sub-invocations".to_string(),
495                )
496            })?,
497        };
498
499        // Build the authorization entry
500        // For simulation, signature must be an empty vector (not Void)
501        // Void causes Error(Value, UnexpectedType) during auth verification
502        // User will replace this with their actual signature after signing
503        let empty_signature = ScVal::Vec(Some(ScVec::default()));
504
505        Ok(SorobanAuthorizationEntry {
506            credentials: SorobanCredentials::Address(SorobanAddressCredentials {
507                address: user_addr,
508                nonce: self.generate_nonce(),
509                signature_expiration_ledger: params.expiration_ledger,
510                signature: empty_signature,
511            }),
512            root_invocation,
513        })
514    }
515
516    /// Generate a nonce for authorization
517    ///
518    /// The nonce should be unique per authorization to prevent replay attacks.
519    /// Combines timestamp with randomness for improved uniqueness.
520    fn generate_nonce(&self) -> i64 {
521        use rand::Rng;
522        use std::time::{SystemTime, UNIX_EPOCH};
523        let timestamp = SystemTime::now()
524            .duration_since(UNIX_EPOCH)
525            .map(|d| d.as_nanos() as i64)
526            .unwrap_or(0);
527        let random: i64 = rand::rng().random();
528        timestamp ^ random
529    }
530
531    /// Serialize an authorization entry to base64 XDR
532    pub fn serialize_auth_entry(
533        auth: &SorobanAuthorizationEntry,
534    ) -> Result<String, FeeForwarderError> {
535        auth.to_xdr_base64(Limits::none())
536            .map_err(|e| FeeForwarderError::XdrError(format!("Failed to serialize auth: {e}")))
537    }
538
539    /// Deserialize an authorization entry from base64 XDR
540    pub fn deserialize_auth_entry(
541        xdr: &str,
542    ) -> Result<SorobanAuthorizationEntry, FeeForwarderError> {
543        use soroban_rs::xdr::ReadXdr;
544        SorobanAuthorizationEntry::from_xdr_base64(xdr, Limits::none())
545            .map_err(|e| FeeForwarderError::XdrError(format!("Failed to deserialize auth: {e}")))
546    }
547
548    /// Build the InvokeHostFunction operation for FeeForwarder.forward()
549    ///
550    /// This creates the operation that will be included in the transaction.
551    /// The authorization entries should include both user and relayer signatures.
552    ///
553    /// # Arguments
554    ///
555    /// * `params` - FeeForwarder transaction parameters
556    /// * `auth_entries` - Signed authorization entries (user + relayer)
557    pub fn build_invoke_operation(
558        &self,
559        params: &FeeForwarderParams,
560        auth_entries: Vec<SorobanAuthorizationEntry>,
561    ) -> Result<Operation, FeeForwarderError> {
562        Self::build_invoke_operation_standalone(&self.fee_forwarder_address, params, auth_entries)
563    }
564
565    /// Build the InvokeHostFunction operation without requiring a service instance.
566    ///
567    /// This static method is useful when you don't have an `Arc<P>` provider
568    /// but still need to build InvokeHostFunction operations for the FeeForwarder.
569    pub fn build_invoke_operation_standalone(
570        fee_forwarder_address: &str,
571        params: &FeeForwarderParams,
572        auth_entries: Vec<SorobanAuthorizationEntry>,
573    ) -> Result<Operation, FeeForwarderError> {
574        let fee_forwarder_addr = Self::parse_contract_address(fee_forwarder_address)?;
575        let forward_args = Self::build_forward_args_standalone(fee_forwarder_address, params)?;
576
577        let host_function = soroban_rs::xdr::HostFunction::InvokeContract(InvokeContractArgs {
578            contract_address: fee_forwarder_addr,
579            function_name: Self::create_symbol("forward")?,
580            args: forward_args.into(),
581        });
582
583        let invoke_op = soroban_rs::xdr::InvokeHostFunctionOp {
584            host_function,
585            auth: auth_entries.try_into().map_err(|_| {
586                FeeForwarderError::AuthorizationBuildError(
587                    "Failed to create auth entries vector".to_string(),
588                )
589            })?,
590        };
591
592        Ok(Operation {
593            source_account: None,
594            body: OperationBody::InvokeHostFunction(invoke_op),
595        })
596    }
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602    use crate::services::provider::StellarProvider;
603
604    // Test constants
605    const VALID_CONTRACT_ADDR: &str = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";
606    const VALID_ACCOUNT_ADDR: &str = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
607    const VALID_ACCOUNT_ADDR_2: &str = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
608
609    fn create_test_params() -> FeeForwarderParams {
610        FeeForwarderParams {
611            fee_token: VALID_CONTRACT_ADDR.to_string(),
612            fee_amount: 1_000_000,
613            max_fee_amount: 2_000_000,
614            expiration_ledger: 100000,
615            target_contract: VALID_CONTRACT_ADDR.to_string(),
616            target_fn: "transfer".to_string(),
617            target_args: vec![ScVal::U32(42)],
618            user: VALID_ACCOUNT_ADDR.to_string(),
619            relayer: VALID_ACCOUNT_ADDR_2.to_string(),
620        }
621    }
622
623    // ==================== parse_contract_address tests ====================
624
625    #[test]
626    fn test_parse_contract_address_valid() {
627        let result =
628            FeeForwarderService::<StellarProvider>::parse_contract_address(VALID_CONTRACT_ADDR);
629        assert!(result.is_ok());
630        match result.unwrap() {
631            ScAddress::Contract(_) => {}
632            _ => panic!("Expected Contract address"),
633        }
634    }
635
636    #[test]
637    fn test_parse_contract_address_invalid() {
638        let addr = "INVALID";
639        let result = FeeForwarderService::<StellarProvider>::parse_contract_address(addr);
640        assert!(result.is_err());
641        let err = result.unwrap_err();
642        assert!(matches!(err, FeeForwarderError::InvalidContractAddress(_)));
643    }
644
645    #[test]
646    fn test_parse_contract_address_with_account_address() {
647        // Account addresses (G...) should fail when parsed as contract addresses
648        let result =
649            FeeForwarderService::<StellarProvider>::parse_contract_address(VALID_ACCOUNT_ADDR);
650        assert!(result.is_err());
651    }
652
653    #[test]
654    fn test_parse_contract_address_empty() {
655        let result = FeeForwarderService::<StellarProvider>::parse_contract_address("");
656        assert!(result.is_err());
657    }
658
659    #[test]
660    fn test_parse_contract_address_public() {
661        let result = FeeForwarderService::<StellarProvider>::parse_contract_address_public(
662            VALID_CONTRACT_ADDR,
663        );
664        assert!(result.is_ok());
665    }
666
667    // ==================== parse_account_address tests ====================
668
669    #[test]
670    fn test_parse_account_address_valid() {
671        let result =
672            FeeForwarderService::<StellarProvider>::parse_account_address(VALID_ACCOUNT_ADDR);
673        assert!(result.is_ok());
674        match result.unwrap() {
675            ScAddress::Account(_) => {}
676            _ => panic!("Expected Account address"),
677        }
678    }
679
680    #[test]
681    fn test_parse_account_address_valid_2() {
682        let result =
683            FeeForwarderService::<StellarProvider>::parse_account_address(VALID_ACCOUNT_ADDR_2);
684        assert!(result.is_ok());
685    }
686
687    #[test]
688    fn test_parse_account_address_invalid_format() {
689        let addr = "GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGH";
690        let result = FeeForwarderService::<StellarProvider>::parse_account_address(addr);
691        assert!(result.is_err());
692        let err = result.unwrap_err();
693        assert!(matches!(err, FeeForwarderError::InvalidAccountAddress(_)));
694    }
695
696    #[test]
697    fn test_parse_account_address_with_contract_address() {
698        // Contract addresses (C...) should fail when parsed as account addresses
699        let result =
700            FeeForwarderService::<StellarProvider>::parse_account_address(VALID_CONTRACT_ADDR);
701        assert!(result.is_err());
702    }
703
704    #[test]
705    fn test_parse_account_address_empty() {
706        let result = FeeForwarderService::<StellarProvider>::parse_account_address("");
707        assert!(result.is_err());
708    }
709
710    // ==================== i128_to_scval tests ====================
711
712    #[test]
713    fn test_i128_to_scval() {
714        let amount: i128 = 1_000_000_000;
715        let scval = FeeForwarderService::<StellarProvider>::i128_to_scval(amount);
716        match scval {
717            ScVal::I128(parts) => {
718                let recovered = ((parts.hi as i128) << 64) | (parts.lo as i128);
719                assert_eq!(amount, recovered);
720            }
721            _ => panic!("Expected I128"),
722        }
723    }
724
725    #[test]
726    fn test_i128_to_scval_zero() {
727        let amount: i128 = 0;
728        let scval = FeeForwarderService::<StellarProvider>::i128_to_scval(amount);
729        match scval {
730            ScVal::I128(parts) => {
731                assert_eq!(parts.hi, 0);
732                assert_eq!(parts.lo, 0);
733            }
734            _ => panic!("Expected I128"),
735        }
736    }
737
738    #[test]
739    fn test_i128_to_scval_negative() {
740        let amount: i128 = -1_000_000;
741        let scval = FeeForwarderService::<StellarProvider>::i128_to_scval(amount);
742        match scval {
743            ScVal::I128(parts) => {
744                let recovered = ((parts.hi as i128) << 64) | (parts.lo as i128);
745                assert_eq!(amount, recovered);
746            }
747            _ => panic!("Expected I128"),
748        }
749    }
750
751    #[test]
752    fn test_i128_to_scval_max() {
753        let amount: i128 = i128::MAX;
754        let scval = FeeForwarderService::<StellarProvider>::i128_to_scval(amount);
755        match scval {
756            ScVal::I128(parts) => {
757                let recovered = ((parts.hi as i128) << 64) | (parts.lo as i128);
758                assert_eq!(amount, recovered);
759            }
760            _ => panic!("Expected I128"),
761        }
762    }
763
764    #[test]
765    fn test_i128_to_scval_min() {
766        let amount: i128 = i128::MIN;
767        let scval = FeeForwarderService::<StellarProvider>::i128_to_scval(amount);
768        match scval {
769            ScVal::I128(parts) => {
770                let recovered = ((parts.hi as i128) << 64) | (parts.lo as i128);
771                assert_eq!(amount, recovered);
772            }
773            _ => panic!("Expected I128"),
774        }
775    }
776
777    // ==================== create_symbol tests ====================
778
779    #[test]
780    fn test_create_symbol() {
781        let result = FeeForwarderService::<StellarProvider>::create_symbol("forward");
782        assert!(result.is_ok());
783        assert_eq!(result.unwrap().to_utf8_string_lossy(), "forward");
784    }
785
786    #[test]
787    fn test_create_symbol_approve() {
788        let result = FeeForwarderService::<StellarProvider>::create_symbol("approve");
789        assert!(result.is_ok());
790        assert_eq!(result.unwrap().to_utf8_string_lossy(), "approve");
791    }
792
793    #[test]
794    fn test_create_symbol_transfer() {
795        let result = FeeForwarderService::<StellarProvider>::create_symbol("transfer");
796        assert!(result.is_ok());
797        assert_eq!(result.unwrap().to_utf8_string_lossy(), "transfer");
798    }
799
800    #[test]
801    fn test_create_symbol_empty() {
802        let result = FeeForwarderService::<StellarProvider>::create_symbol("");
803        assert!(result.is_ok());
804    }
805
806    // ==================== build_user_auth_entry_standalone tests ====================
807
808    #[test]
809    fn test_build_user_auth_entry_standalone_without_target_auth() {
810        let params = create_test_params();
811        let result = FeeForwarderService::<StellarProvider>::build_user_auth_entry_standalone(
812            VALID_CONTRACT_ADDR,
813            &params,
814            false,
815        );
816        assert!(result.is_ok());
817
818        let auth_entry = result.unwrap();
819        match &auth_entry.credentials {
820            SorobanCredentials::Address(creds) => {
821                assert_eq!(creds.signature_expiration_ledger, params.expiration_ledger);
822                // Signature should be empty vec for simulation
823                match &creds.signature {
824                    ScVal::Vec(Some(v)) => assert!(v.is_empty()),
825                    _ => panic!("Expected empty Vec signature"),
826                }
827            }
828            _ => panic!("Expected Address credentials"),
829        }
830
831        // Should have 1 sub-invocation (approve only)
832        assert_eq!(auth_entry.root_invocation.sub_invocations.len(), 1);
833    }
834
835    #[test]
836    fn test_build_user_auth_entry_standalone_with_target_auth() {
837        let params = create_test_params();
838        let result = FeeForwarderService::<StellarProvider>::build_user_auth_entry_standalone(
839            VALID_CONTRACT_ADDR,
840            &params,
841            true,
842        );
843        assert!(result.is_ok());
844
845        let auth_entry = result.unwrap();
846        // Should have 2 sub-invocations (approve + target)
847        assert_eq!(auth_entry.root_invocation.sub_invocations.len(), 2);
848    }
849
850    #[test]
851    fn test_build_user_auth_entry_standalone_invalid_fee_forwarder() {
852        let params = create_test_params();
853        let result = FeeForwarderService::<StellarProvider>::build_user_auth_entry_standalone(
854            "INVALID", &params, false,
855        );
856        assert!(result.is_err());
857        assert!(matches!(
858            result.unwrap_err(),
859            FeeForwarderError::InvalidContractAddress(_)
860        ));
861    }
862
863    #[test]
864    fn test_build_user_auth_entry_standalone_invalid_fee_token() {
865        let mut params = create_test_params();
866        params.fee_token = "INVALID".to_string();
867        let result = FeeForwarderService::<StellarProvider>::build_user_auth_entry_standalone(
868            VALID_CONTRACT_ADDR,
869            &params,
870            false,
871        );
872        assert!(result.is_err());
873    }
874
875    #[test]
876    fn test_build_user_auth_entry_standalone_invalid_user() {
877        let mut params = create_test_params();
878        params.user = "INVALID".to_string();
879        let result = FeeForwarderService::<StellarProvider>::build_user_auth_entry_standalone(
880            VALID_CONTRACT_ADDR,
881            &params,
882            false,
883        );
884        assert!(result.is_err());
885        assert!(matches!(
886            result.unwrap_err(),
887            FeeForwarderError::InvalidAccountAddress(_)
888        ));
889    }
890
891    #[test]
892    fn test_build_user_auth_entry_standalone_invalid_relayer() {
893        let mut params = create_test_params();
894        params.relayer = "INVALID".to_string();
895        let result = FeeForwarderService::<StellarProvider>::build_user_auth_entry_standalone(
896            VALID_CONTRACT_ADDR,
897            &params,
898            false,
899        );
900        assert!(result.is_err());
901    }
902
903    // ==================== build_relayer_auth_entry_standalone tests ====================
904
905    #[test]
906    fn test_build_relayer_auth_entry_standalone() {
907        let params = create_test_params();
908        let result = FeeForwarderService::<StellarProvider>::build_relayer_auth_entry_standalone(
909            VALID_CONTRACT_ADDR,
910            &params,
911        );
912        assert!(result.is_ok());
913
914        let auth_entry = result.unwrap();
915        match &auth_entry.credentials {
916            SorobanCredentials::Address(creds) => {
917                assert_eq!(creds.signature_expiration_ledger, params.expiration_ledger);
918                // Relayer has no sub-invocations
919            }
920            _ => panic!("Expected Address credentials"),
921        }
922
923        // Relayer should have no sub-invocations
924        assert!(auth_entry.root_invocation.sub_invocations.is_empty());
925    }
926
927    #[test]
928    fn test_build_relayer_auth_entry_standalone_invalid_fee_forwarder() {
929        let params = create_test_params();
930        let result = FeeForwarderService::<StellarProvider>::build_relayer_auth_entry_standalone(
931            "INVALID", &params,
932        );
933        assert!(result.is_err());
934    }
935
936    #[test]
937    fn test_build_relayer_auth_entry_standalone_invalid_relayer() {
938        let mut params = create_test_params();
939        params.relayer = "INVALID".to_string();
940        let result = FeeForwarderService::<StellarProvider>::build_relayer_auth_entry_standalone(
941            VALID_CONTRACT_ADDR,
942            &params,
943        );
944        assert!(result.is_err());
945    }
946
947    // ==================== build_forward_args tests ====================
948
949    #[test]
950    fn test_build_forward_args_for_invoke_contract_args() {
951        let params = create_test_params();
952        let result =
953            FeeForwarderService::<StellarProvider>::build_forward_args_for_invoke_contract_args(
954                VALID_CONTRACT_ADDR,
955                &params,
956            );
957        assert!(result.is_ok());
958
959        let args = result.unwrap();
960        // Should have 9 arguments
961        assert_eq!(args.len(), 9);
962    }
963
964    #[test]
965    fn test_build_forward_args_standalone_invalid_fee_token() {
966        let mut params = create_test_params();
967        params.fee_token = "INVALID".to_string();
968        let result =
969            FeeForwarderService::<StellarProvider>::build_forward_args_for_invoke_contract_args(
970                VALID_CONTRACT_ADDR,
971                &params,
972            );
973        assert!(result.is_err());
974    }
975
976    #[test]
977    fn test_build_forward_args_standalone_invalid_target_contract() {
978        let mut params = create_test_params();
979        params.target_contract = "INVALID".to_string();
980        let result =
981            FeeForwarderService::<StellarProvider>::build_forward_args_for_invoke_contract_args(
982                VALID_CONTRACT_ADDR,
983                &params,
984            );
985        assert!(result.is_err());
986    }
987
988    // ==================== serialize/deserialize auth entry tests ====================
989
990    #[test]
991    fn test_serialize_and_deserialize_auth_entry() {
992        let params = create_test_params();
993        let auth_entry = FeeForwarderService::<StellarProvider>::build_user_auth_entry_standalone(
994            VALID_CONTRACT_ADDR,
995            &params,
996            false,
997        )
998        .unwrap();
999
1000        let serialized = FeeForwarderService::<StellarProvider>::serialize_auth_entry(&auth_entry);
1001        assert!(serialized.is_ok());
1002
1003        let xdr_string = serialized.unwrap();
1004        assert!(!xdr_string.is_empty());
1005
1006        let deserialized =
1007            FeeForwarderService::<StellarProvider>::deserialize_auth_entry(&xdr_string);
1008        assert!(deserialized.is_ok());
1009    }
1010
1011    #[test]
1012    fn test_deserialize_auth_entry_invalid_xdr() {
1013        let result =
1014            FeeForwarderService::<StellarProvider>::deserialize_auth_entry("not-valid-base64-xdr!");
1015        assert!(result.is_err());
1016        assert!(matches!(
1017            result.unwrap_err(),
1018            FeeForwarderError::XdrError(_)
1019        ));
1020    }
1021
1022    #[test]
1023    fn test_deserialize_auth_entry_empty() {
1024        let result = FeeForwarderService::<StellarProvider>::deserialize_auth_entry("");
1025        assert!(result.is_err());
1026    }
1027
1028    // ==================== build_invoke_operation_standalone tests ====================
1029
1030    #[test]
1031    fn test_build_invoke_operation_standalone() {
1032        let params = create_test_params();
1033        let user_auth = FeeForwarderService::<StellarProvider>::build_user_auth_entry_standalone(
1034            VALID_CONTRACT_ADDR,
1035            &params,
1036            false,
1037        )
1038        .unwrap();
1039        let relayer_auth =
1040            FeeForwarderService::<StellarProvider>::build_relayer_auth_entry_standalone(
1041                VALID_CONTRACT_ADDR,
1042                &params,
1043            )
1044            .unwrap();
1045
1046        let result = FeeForwarderService::<StellarProvider>::build_invoke_operation_standalone(
1047            VALID_CONTRACT_ADDR,
1048            &params,
1049            vec![user_auth, relayer_auth],
1050        );
1051        assert!(result.is_ok());
1052
1053        let operation = result.unwrap();
1054        assert!(operation.source_account.is_none());
1055        match operation.body {
1056            OperationBody::InvokeHostFunction(op) => {
1057                assert_eq!(op.auth.len(), 2);
1058            }
1059            _ => panic!("Expected InvokeHostFunction operation"),
1060        }
1061    }
1062
1063    #[test]
1064    fn test_build_invoke_operation_standalone_invalid_fee_forwarder() {
1065        let params = create_test_params();
1066        let result = FeeForwarderService::<StellarProvider>::build_invoke_operation_standalone(
1067            "INVALID",
1068            &params,
1069            vec![],
1070        );
1071        assert!(result.is_err());
1072    }
1073
1074    #[test]
1075    fn test_build_invoke_operation_standalone_empty_auth() {
1076        let params = create_test_params();
1077        let result = FeeForwarderService::<StellarProvider>::build_invoke_operation_standalone(
1078            VALID_CONTRACT_ADDR,
1079            &params,
1080            vec![],
1081        );
1082        assert!(result.is_ok());
1083    }
1084
1085    // ==================== FeeForwarderParams tests ====================
1086
1087    #[test]
1088    fn test_fee_forwarder_params_clone() {
1089        let params = create_test_params();
1090        let cloned = params.clone();
1091        assert_eq!(params.fee_token, cloned.fee_token);
1092        assert_eq!(params.fee_amount, cloned.fee_amount);
1093        assert_eq!(params.max_fee_amount, cloned.max_fee_amount);
1094        assert_eq!(params.expiration_ledger, cloned.expiration_ledger);
1095        assert_eq!(params.target_contract, cloned.target_contract);
1096        assert_eq!(params.target_fn, cloned.target_fn);
1097        assert_eq!(params.user, cloned.user);
1098        assert_eq!(params.relayer, cloned.relayer);
1099    }
1100
1101    #[test]
1102    fn test_fee_forwarder_params_with_empty_target_args() {
1103        let mut params = create_test_params();
1104        params.target_args = vec![];
1105
1106        let result = FeeForwarderService::<StellarProvider>::build_user_auth_entry_standalone(
1107            VALID_CONTRACT_ADDR,
1108            &params,
1109            true,
1110        );
1111        assert!(result.is_ok());
1112    }
1113
1114    #[test]
1115    fn test_fee_forwarder_params_with_multiple_target_args() {
1116        let mut params = create_test_params();
1117        params.target_args = vec![
1118            ScVal::U32(1),
1119            ScVal::U32(2),
1120            ScVal::U32(3),
1121            ScVal::Bool(true),
1122        ];
1123
1124        let result = FeeForwarderService::<StellarProvider>::build_user_auth_entry_standalone(
1125            VALID_CONTRACT_ADDR,
1126            &params,
1127            true,
1128        );
1129        assert!(result.is_ok());
1130    }
1131
1132    // ==================== Error display tests ====================
1133
1134    #[test]
1135    fn test_error_display_invalid_contract_address() {
1136        let err = FeeForwarderError::InvalidContractAddress("test".to_string());
1137        assert!(err.to_string().contains("Invalid contract address"));
1138    }
1139
1140    #[test]
1141    fn test_error_display_invalid_account_address() {
1142        let err = FeeForwarderError::InvalidAccountAddress("test".to_string());
1143        assert!(err.to_string().contains("Invalid account address"));
1144    }
1145
1146    #[test]
1147    fn test_error_display_authorization_build_error() {
1148        let err = FeeForwarderError::AuthorizationBuildError("test".to_string());
1149        assert!(err.to_string().contains("Failed to build authorization"));
1150    }
1151
1152    #[test]
1153    fn test_error_display_provider_error() {
1154        let err = FeeForwarderError::ProviderError("test".to_string());
1155        assert!(err.to_string().contains("Provider error"));
1156    }
1157
1158    #[test]
1159    fn test_error_display_xdr_error() {
1160        let err = FeeForwarderError::XdrError("test".to_string());
1161        assert!(err.to_string().contains("XDR serialization error"));
1162    }
1163
1164    #[test]
1165    fn test_error_display_invalid_function_name() {
1166        let err = FeeForwarderError::InvalidFunctionName("test".to_string());
1167        assert!(err.to_string().contains("Invalid function name"));
1168    }
1169
1170    // ==================== Constants tests ====================
1171
1172    #[test]
1173    fn test_default_validity_seconds() {
1174        assert_eq!(DEFAULT_VALIDITY_SECONDS, 120); // 2 minutes, matches sponsored tx validity
1175    }
1176
1177    #[test]
1178    fn test_ledger_time_seconds() {
1179        assert_eq!(STELLAR_LEDGER_TIME_SECONDS, 5);
1180    }
1181}