1use 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
27pub const DEFAULT_VALIDITY_SECONDS: u64 = 120;
31
32#[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#[derive(Debug, Clone)]
56pub struct FeeForwarderParams {
57 pub fee_token: String,
59 pub fee_amount: i128,
61 pub max_fee_amount: i128,
63 pub expiration_ledger: u32,
65 pub target_contract: String,
67 pub target_fn: String,
69 pub target_args: Vec<ScVal>,
71 pub user: String,
73 pub relayer: String,
75}
76
77pub struct FeeForwarderService<P>
79where
80 P: StellarProviderTrait + Send + Sync,
81{
82 fee_forwarder_address: String,
84 provider: Arc<P>,
86}
87
88impl<P> FeeForwarderService<P>
89where
90 P: StellarProviderTrait + Send + Sync,
91{
92 pub fn new(fee_forwarder_address: String, provider: Arc<P>) -> Self {
99 Self {
100 fee_forwarder_address,
101 provider,
102 }
103 }
104
105 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(¶ms.fee_token)?;
116 let target_contract_addr = Self::parse_contract_address(¶ms.target_contract)?;
117 let user_addr = Self::parse_account_address(¶ms.user)?;
118 let _relayer_addr = Self::parse_account_address(¶ms.relayer)?;
119
120 let mut sub_invocations = Vec::new();
122
123 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 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(¶ms.target_fn)?,
157 args: target_args.into(),
158 }),
159 sub_invocations: VecM::default(),
160 });
161 }
162
163 let user_auth_args = Self::build_user_auth_args_standalone(params)?;
167
168 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 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 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 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(¶ms.relayer)?;
228
229 let forward_args = Self::build_forward_args_standalone(fee_forwarder_address, params)?;
231
232 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 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 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 fn build_user_auth_args_standalone(
275 params: &FeeForwarderParams,
276 ) -> Result<ScVec, FeeForwarderError> {
277 let fee_token_addr = Self::parse_contract_address(¶ms.fee_token)?;
278 let target_contract_addr = Self::parse_contract_address(¶ms.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 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(¶ms.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 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(¶ms.fee_token)?;
310 let target_contract_addr = Self::parse_contract_address(¶ms.target_contract)?;
311 let user_addr = Self::parse_account_address(¶ms.user)?;
312 let relayer_addr = Self::parse_account_address(¶ms.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 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(¶ms.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 pub fn fee_forwarder_address(&self) -> &str {
338 &self.fee_forwarder_address
339 }
340
341 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 pub fn parse_contract_address_public(address: &str) -> Result<ScAddress, FeeForwarderError> {
351 Self::parse_contract_address(address)
352 }
353
354 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 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 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 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 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 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(¶ms.fee_token)?;
432 let target_contract_addr = Self::parse_contract_address(¶ms.target_contract)?;
433 let user_addr = Self::parse_account_address(¶ms.user)?;
434 let _relayer_addr = Self::parse_account_address(¶ms.relayer)?;
436
437 let mut sub_invocations = Vec::new();
439
440 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 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(¶ms.target_fn)?,
474 args: target_args.into(),
475 }),
476 sub_invocations: VecM::default(),
477 });
478 }
479
480 let user_auth_args = Self::build_user_auth_args_standalone(params)?;
484
485 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 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 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 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 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 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 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 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 #[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 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 #[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 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 #[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 #[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 #[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 ¶ms,
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 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 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 ¶ms,
841 true,
842 );
843 assert!(result.is_ok());
844
845 let auth_entry = result.unwrap();
846 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", ¶ms, 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 ¶ms,
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 ¶ms,
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 ¶ms,
898 false,
899 );
900 assert!(result.is_err());
901 }
902
903 #[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 ¶ms,
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 }
920 _ => panic!("Expected Address credentials"),
921 }
922
923 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", ¶ms,
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 ¶ms,
943 );
944 assert!(result.is_err());
945 }
946
947 #[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 ¶ms,
956 );
957 assert!(result.is_ok());
958
959 let args = result.unwrap();
960 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 ¶ms,
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 ¶ms,
984 );
985 assert!(result.is_err());
986 }
987
988 #[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 ¶ms,
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 #[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 ¶ms,
1036 false,
1037 )
1038 .unwrap();
1039 let relayer_auth =
1040 FeeForwarderService::<StellarProvider>::build_relayer_auth_entry_standalone(
1041 VALID_CONTRACT_ADDR,
1042 ¶ms,
1043 )
1044 .unwrap();
1045
1046 let result = FeeForwarderService::<StellarProvider>::build_invoke_operation_standalone(
1047 VALID_CONTRACT_ADDR,
1048 ¶ms,
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 ¶ms,
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 ¶ms,
1080 vec![],
1081 );
1082 assert!(result.is_ok());
1083 }
1084
1085 #[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 ¶ms,
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 ¶ms,
1127 true,
1128 );
1129 assert!(result.is_ok());
1130 }
1131
1132 #[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 #[test]
1173 fn test_default_validity_seconds() {
1174 assert_eq!(DEFAULT_VALIDITY_SECONDS, 120); }
1176
1177 #[test]
1178 fn test_ledger_time_seconds() {
1179 assert_eq!(STELLAR_LEDGER_TIME_SECONDS, 5);
1180 }
1181}