Skip to main content

alkahest_rs/
lib.rs

1use alloy::{
2    dyn_abi::SolType,
3    primitives::{Address, FixedBytes, Log},
4    providers::Provider,
5    rpc::types::{Filter, TransactionReceipt},
6    signers::local::PrivateKeySigner,
7    sol_types::SolEvent,
8};
9use extensions::{
10    AlkahestExtension, BaseExtensions, HasArbiters, HasAttestation, HasCommitReveal, HasErc20,
11    HasErc721, HasErc1155, HasStringObligation, HasTokenBundle,
12};
13use futures_util::StreamExt;
14use serde::{Deserialize, Serialize};
15use types::EscrowClaimed;
16use std::sync::Arc;
17use types::{SharedPublicProvider, SharedWalletProvider};
18
19use crate::clients::{
20    arbiters::ArbitersAddresses, attestation::AttestationAddresses,
21    commit_reveal_obligation::CommitRevealObligationAddresses, erc20::Erc20Addresses,
22    erc721::Erc721Addresses, erc1155::Erc1155Addresses, native_token::NativeTokenAddresses,
23    string_obligation::StringObligationAddresses, token_bundle::TokenBundleAddresses,
24};
25
26/// Type alias for the default AlkahestClient with BaseExtensions
27pub type DefaultAlkahestClient = AlkahestClient<BaseExtensions>;
28
29pub mod addresses;
30pub mod clients;
31pub mod contracts;
32pub mod extensions;
33pub mod fixtures;
34
35pub mod types;
36pub mod utils;
37
38// Re-export contract types from client modules
39pub use clients::arbiters::ArbitersContract;
40pub use clients::attestation::AttestationContract;
41pub use clients::erc20::Erc20Contract;
42pub use clients::erc721::Erc721Contract;
43pub use clients::erc1155::Erc1155Contract;
44pub use clients::native_token::NativeTokenContract;
45pub use clients::commit_reveal_obligation::CommitRevealObligationContract;
46pub use clients::string_obligation::StringObligationContract;
47pub use clients::token_bundle::TokenBundleContract;
48pub use extensions::ContractModule;
49
50/// Configuration struct containing all contract addresses for Alkahest protocol extensions.
51///
52/// This struct holds the addresses for all the smart contracts used by different
53/// protocol modules. Each field represents a different module's addresses.
54///
55/// # Default Behavior
56///
57/// When using `Default::default()` or passing `None` to client constructors,
58/// the Base Sepolia network addresses are used by default.
59///
60/// # Example
61///
62/// ```rust,ignore
63/// use alkahest_rs::{DefaultExtensionConfig, addresses::BASE_SEPOLIA_ADDRESSES};
64///
65/// // Use default (Base Sepolia) configuration
66/// let default_config = DefaultExtensionConfig::default();
67///
68/// // Use a predefined configuration
69/// let base_config = BASE_SEPOLIA_ADDRESSES;
70///
71/// // Create a custom configuration
72/// let custom_config = DefaultExtensionConfig {
73///     arbiters_addresses: my_custom_arbiters,
74///     erc20_addresses: my_custom_erc20,
75///     // ... other fields
76///     ..BASE_SEPOLIA_ADDRESSES
77/// };
78/// ```
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct DefaultExtensionConfig {
81    /// Addresses for arbiter contracts that handle obligation verification
82    pub arbiters_addresses: ArbitersAddresses,
83    /// Addresses for ERC20-related contracts
84    pub erc20_addresses: Erc20Addresses,
85    /// Addresses for ERC721-related contracts
86    pub erc721_addresses: Erc721Addresses,
87    /// Addresses for ERC1155-related contracts
88    pub erc1155_addresses: Erc1155Addresses,
89    /// Addresses for native token contracts
90    pub native_token_addresses: NativeTokenAddresses,
91    /// Addresses for token bundle contracts that handle multiple token types
92    pub token_bundle_addresses: TokenBundleAddresses,
93    /// Addresses for attestation-related contracts
94    pub attestation_addresses: AttestationAddresses,
95    /// Addresses for string obligation contracts
96    pub string_obligation_addresses: StringObligationAddresses,
97    /// Addresses for commit-reveal obligation contracts
98    pub commit_reveal_obligation_addresses: CommitRevealObligationAddresses,
99}
100
101impl Default for DefaultExtensionConfig {
102    /// Returns the default configuration using Base Sepolia network addresses.
103    ///
104    /// This is equivalent to using `BASE_SEPOLIA_ADDRESSES` directly.
105    fn default() -> Self {
106        // Use Base Sepolia as the default network
107        crate::addresses::BASE_SEPOLIA_ADDRESSES
108    }
109}
110
111#[derive(Clone)]
112pub struct AlkahestClient<Extensions: AlkahestExtension = extensions::NoExtension> {
113    pub wallet_provider: SharedWalletProvider,
114    pub public_provider: SharedPublicProvider,
115    pub address: Address,
116    pub extensions: Extensions,
117    private_key: PrivateKeySigner,
118    rpc_url: String,
119}
120
121impl AlkahestClient<extensions::NoExtension> {
122    /// Create a new client with no extensions
123    pub async fn new(
124        private_key: PrivateKeySigner,
125        rpc_url: impl ToString + Clone + Send,
126    ) -> eyre::Result<Self> {
127        let wallet_provider =
128            Arc::new(utils::get_wallet_provider(private_key.clone(), rpc_url.clone()).await?);
129        let public_provider = Arc::new(utils::get_public_provider(rpc_url.clone()).await?);
130
131        Ok(AlkahestClient {
132            wallet_provider,
133            public_provider,
134            address: private_key.address(),
135            extensions: extensions::NoExtension,
136            private_key,
137            rpc_url: rpc_url.to_string(),
138        })
139    }
140}
141
142impl AlkahestClient<BaseExtensions> {
143    /// Create a client with all base extensions using DefaultExtensionConfig
144    /// This is a convenience method for the common case
145    pub async fn with_base_extensions(
146        private_key: PrivateKeySigner,
147        rpc_url: impl ToString + Clone + Send,
148        config: Option<DefaultExtensionConfig>,
149    ) -> eyre::Result<Self> {
150        let wallet_provider =
151            Arc::new(utils::get_wallet_provider(private_key.clone(), rpc_url.clone()).await?);
152        let public_provider = Arc::new(utils::get_public_provider(rpc_url.clone()).await?);
153
154        let providers = crate::types::ProviderContext {
155            wallet: wallet_provider.clone(),
156            public: public_provider.clone(),
157            signer: private_key.clone(),
158        };
159        let extensions = BaseExtensions::init(private_key.clone(), providers, config).await?;
160
161        Ok(AlkahestClient {
162            wallet_provider,
163            public_provider,
164            address: private_key.address(),
165            extensions,
166            private_key,
167            rpc_url: rpc_url.to_string(),
168        })
169    }
170}
171
172impl<Extensions: AlkahestExtension> AlkahestClient<Extensions> {
173    /// Add an extension with a specific configuration
174    pub async fn extend<NewExt: AlkahestExtension>(
175        self,
176        config: Option<NewExt::Config>,
177    ) -> eyre::Result<AlkahestClient<extensions::JoinExtension<Extensions, NewExt>>> {
178        let providers = crate::types::ProviderContext {
179            wallet: self.wallet_provider.clone(),
180            public: self.public_provider.clone(),
181            signer: self.private_key.clone(),
182        };
183        let new_extension = NewExt::init(self.private_key.clone(), providers, config).await?;
184
185        let joined_extensions = extensions::JoinExtension {
186            left: self.extensions,
187            right: new_extension,
188        };
189
190        Ok(AlkahestClient {
191            wallet_provider: self.wallet_provider,
192            public_provider: self.public_provider,
193            address: self.address,
194            extensions: joined_extensions,
195            private_key: self.private_key,
196            rpc_url: self.rpc_url,
197        })
198    }
199
200    /// Add an extension using its default configuration
201    pub async fn extend_default<NewExt: AlkahestExtension>(
202        self,
203    ) -> eyre::Result<AlkahestClient<extensions::JoinExtension<Extensions, NewExt>>>
204    where
205        NewExt::Config: Default,
206    {
207        self.extend::<NewExt>(Some(NewExt::Config::default())).await
208    }
209
210    /// Get the address of a specific ERC20 contract
211    ///
212    /// # Example
213    /// ```rust,ignore
214    /// use alkahest_rs::Erc20Contract;
215    ///
216    /// let escrow_addr = client.erc20_address(Erc20Contract::EscrowObligation);
217    /// ```
218    pub fn erc20_address(&self, contract: Erc20Contract) -> Address
219    where
220        Extensions: extensions::HasErc20,
221    {
222        match contract {
223            Erc20Contract::Eas => self.erc20().addresses.eas,
224            Erc20Contract::BarterUtils => self.erc20().addresses.barter_utils,
225            Erc20Contract::EscrowObligation => self.erc20().addresses.escrow_obligation_nontierable,
226            Erc20Contract::PaymentObligation => self.erc20().addresses.payment_obligation,
227        }
228    }
229
230    /// Get the address of a specific ERC721 contract
231    pub fn erc721_address(&self, contract: Erc721Contract) -> Address
232    where
233        Extensions: extensions::HasErc721,
234    {
235        match contract {
236            Erc721Contract::Eas => self.erc721().addresses.eas,
237            Erc721Contract::BarterUtils => self.erc721().addresses.barter_utils,
238            Erc721Contract::EscrowObligation => self.erc721().addresses.escrow_obligation_nontierable,
239            Erc721Contract::PaymentObligation => self.erc721().addresses.payment_obligation,
240        }
241    }
242
243    /// Get the address of a specific ERC1155 contract
244    pub fn erc1155_address(&self, contract: Erc1155Contract) -> Address
245    where
246        Extensions: extensions::HasErc1155,
247    {
248        match contract {
249            Erc1155Contract::Eas => self.erc1155().addresses.eas,
250            Erc1155Contract::BarterUtils => self.erc1155().addresses.barter_utils,
251            Erc1155Contract::EscrowObligation => self.erc1155().addresses.escrow_obligation_nontierable,
252            Erc1155Contract::PaymentObligation => self.erc1155().addresses.payment_obligation,
253        }
254    }
255
256    /// Get the address of a specific TokenBundle contract
257    pub fn token_bundle_address(&self, contract: TokenBundleContract) -> Address
258    where
259        Extensions: extensions::HasTokenBundle,
260    {
261        match contract {
262            TokenBundleContract::Eas => self.token_bundle().addresses.eas,
263            TokenBundleContract::BarterUtils => self.token_bundle().addresses.barter_utils,
264            TokenBundleContract::EscrowObligation => {
265                self.token_bundle().addresses.escrow_obligation_nontierable
266            }
267            TokenBundleContract::PaymentObligation => {
268                self.token_bundle().addresses.payment_obligation
269            }
270        }
271    }
272
273    /// Get the address of a specific Attestation contract
274    pub fn attestation_address(&self, contract: AttestationContract) -> Address
275    where
276        Extensions: extensions::HasAttestation,
277    {
278        match contract {
279            AttestationContract::Eas => self.attestation().addresses.eas,
280            AttestationContract::EasSchemaRegistry => {
281                self.attestation().addresses.eas_schema_registry
282            }
283            AttestationContract::BarterUtils => self.attestation().addresses.barter_utils,
284            AttestationContract::EscrowObligation => self.attestation().addresses.escrow_obligation_nontierable,
285            AttestationContract::EscrowObligation2 => {
286                self.attestation().addresses.escrow_obligation_2_nontierable
287            }
288        }
289    }
290
291    /// Get the address of a specific StringObligation contract
292    pub fn string_obligation_address(&self, contract: StringObligationContract) -> Address
293    where
294        Extensions: extensions::HasStringObligation,
295    {
296        match contract {
297            StringObligationContract::Eas => self.string_obligation().addresses.eas,
298            StringObligationContract::Obligation => self.string_obligation().addresses.obligation,
299        }
300    }
301
302    /// Get the address of a specific CommitRevealObligation contract
303    pub fn commit_reveal_obligation_address(
304        &self,
305        contract: CommitRevealObligationContract,
306    ) -> Address
307    where
308        Extensions: extensions::HasCommitReveal,
309    {
310        match contract {
311            CommitRevealObligationContract::Eas => self.commit_reveal().addresses.eas,
312            CommitRevealObligationContract::Obligation => {
313                self.commit_reveal().addresses.obligation
314            }
315        }
316    }
317
318    /// Get the address of a specific Arbiters contract
319    pub fn arbiters_address(&self, contract: ArbitersContract) -> Address
320    where
321        Extensions: extensions::HasArbiters,
322    {
323        match contract {
324            ArbitersContract::Eas => self.arbiters().addresses.eas,
325            ArbitersContract::TrivialArbiter => self.arbiters().addresses.trivial_arbiter,
326            ArbitersContract::TrustedOracleArbiter => {
327                self.arbiters().addresses.trusted_oracle_arbiter
328            }
329            ArbitersContract::IntrinsicsArbiter => self.arbiters().addresses.intrinsics_arbiter,
330            ArbitersContract::IntrinsicsArbiter2 => self.arbiters().addresses.intrinsics_arbiter_2,
331            ArbitersContract::ERC8004Arbiter => self.arbiters().addresses.erc8004_arbiter,
332            ArbitersContract::AnyArbiter => self.arbiters().addresses.any_arbiter,
333            ArbitersContract::AllArbiter => self.arbiters().addresses.all_arbiter,
334            ArbitersContract::RecipientArbiter => self.arbiters().addresses.recipient_arbiter,
335            ArbitersContract::UidArbiter => self.arbiters().addresses.uid_arbiter,
336        }
337    }
338
339    /// Extracts an Attested event from a transaction receipt.
340    ///
341    /// # Arguments
342    /// * `receipt` - The transaction receipt to extract the event from
343    ///
344    /// # Returns
345    /// * `Result<Log<Attested>>` - The decoded Attested event log
346    pub fn get_attested_event(
347        receipt: TransactionReceipt,
348    ) -> eyre::Result<Log<contracts::IEAS::Attested>> {
349        let attested_event = receipt
350            .inner
351            .logs()
352            .iter()
353            .filter(|log| log.topic0() == Some(&contracts::IEAS::Attested::SIGNATURE_HASH))
354            .collect::<Vec<_>>()
355            .first()
356            .map(|log| log.log_decode::<contracts::IEAS::Attested>())
357            .ok_or_else(|| eyre::eyre!("No Attested event found"))??;
358
359        Ok(attested_event.inner)
360    }
361
362    /// Waits for a fulfillment event for a specific escrow arrangement.
363    ///
364    /// This function will:
365    /// 1. Check for existing fulfillment events from the specified block
366    /// 2. If none found, subscribe to new events and wait for fulfillment
367    ///
368    /// # Arguments
369    /// * `contract_address` - The address of the contract to monitor
370    /// * `buy_attestation` - The attestation UID of the buy order
371    /// * `from_block` - Optional block number to start searching from
372    ///
373    /// # Returns
374    /// * `Result<Log<EscrowClaimed>>` - The fulfillment event log when found
375    pub async fn wait_for_fulfillment(
376        &self,
377        contract_address: Address,
378        buy_attestation: FixedBytes<32>,
379        from_block: Option<u64>,
380    ) -> eyre::Result<Log<EscrowClaimed>> {
381        let filter = Filter::new()
382            .from_block(from_block.unwrap_or(0))
383            .address(contract_address)
384            .event_signature(EscrowClaimed::SIGNATURE_HASH)
385            .topic1(buy_attestation);
386
387        let logs = self.public_provider.get_logs(&filter).await?;
388        println!("initial logs: {:?}", logs);
389        if let Some(log) = logs
390            .iter()
391            .collect::<Vec<_>>()
392            .first()
393            .map(|log| log.log_decode::<EscrowClaimed>())
394        {
395            return Ok(log?.inner);
396        }
397
398        let sub = self.public_provider.subscribe_logs(&filter).await?;
399        let mut stream = sub.into_stream();
400
401        if let Some(log) = stream.next().await {
402            let log = log.log_decode::<EscrowClaimed>()?;
403            return Ok(log.inner);
404        }
405
406        Err(eyre::eyre!("No EscrowClaimed event found"))
407    }
408
409    /// Extract obligation data from a fulfillment attestation
410    ///
411    /// # Example
412    /// ```rust,ignore
413    /// use alkahest_rs::contracts::StringObligation;
414    ///
415    /// let obligation = client.extract_obligation_data::<StringObligation::ObligationData>(&attestation)?;
416    /// ```
417    pub fn extract_obligation_data<ObligationData: SolType>(
418        &self,
419        attestation: &contracts::IEAS::Attestation,
420    ) -> eyre::Result<ObligationData::RustType> {
421        ObligationData::abi_decode(&attestation.data).map_err(Into::into)
422    }
423
424    /// Get the escrow attestation that this fulfillment references via refUID
425    ///
426    /// # Example
427    /// ```rust,ignore
428    /// let escrow_attestation = client.get_escrow_attestation(&fulfillment_attestation).await?;
429    /// ```
430    pub async fn get_escrow_attestation(
431        &self,
432        fulfillment: &contracts::IEAS::Attestation,
433    ) -> eyre::Result<contracts::IEAS::Attestation>
434    where
435        Extensions: extensions::HasAttestation,
436    {
437        let eas = contracts::IEAS::new(self.attestation().addresses.eas, &self.wallet_provider);
438        let escrow = eas.getAttestation(fulfillment.refUID).call().await?;
439        Ok(escrow)
440    }
441
442    /// Extract demand data from an escrow attestation
443    ///
444    /// # Example
445    /// ```rust,ignore
446    /// use alkahest_rs::clients::arbiters::TrustedOracleArbiter;
447    ///
448    /// let demand = client.extract_demand_data::<TrustedOracleArbiter::DemandData>(&escrow_attestation)?;
449    /// ```
450    pub fn extract_demand_data<DemandData: SolType>(
451        &self,
452        escrow_attestation: &contracts::IEAS::Attestation,
453    ) -> eyre::Result<DemandData::RustType> {
454        use alloy::sol;
455        sol! {
456            struct ArbiterDemand {
457                address oracle;
458                bytes demand;
459            }
460        }
461        let arbiter_demand = ArbiterDemand::abi_decode(&escrow_attestation.data)?;
462        DemandData::abi_decode(&arbiter_demand.demand).map_err(Into::into)
463    }
464
465    /// Get escrow attestation and extract demand data in one call
466    ///
467    /// # Example
468    /// ```rust,ignore
469    /// use alkahest_rs::clients::arbiters::TrustedOracleArbiter;
470    ///
471    /// let (escrow, demand) = client.get_escrow_and_demand::<TrustedOracleArbiter::DemandData>(&fulfillment).await?;
472    /// ```
473    pub async fn get_escrow_and_demand<DemandData: SolType>(
474        &self,
475        fulfillment: &contracts::IEAS::Attestation,
476    ) -> eyre::Result<(contracts::IEAS::Attestation, DemandData::RustType)>
477    where
478        Extensions: extensions::HasAttestation,
479    {
480        let escrow = self.get_escrow_attestation(fulfillment).await?;
481        let demand = self.extract_demand_data::<DemandData>(&escrow)?;
482        Ok((escrow, demand))
483    }
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    use crate::addresses::{BASE_SEPOLIA_ADDRESSES, FILECOIN_CALIBRATION_ADDRESSES};
490
491    #[test]
492    fn test_default_extension_config_uses_base_sepolia() {
493        let default_config = DefaultExtensionConfig::default();
494
495        // Verify that default configuration matches BASE_SEPOLIA_ADDRESSES
496        assert_eq!(
497            default_config.arbiters_addresses.eas,
498            BASE_SEPOLIA_ADDRESSES.arbiters_addresses.eas
499        );
500        assert_eq!(
501            default_config.erc20_addresses.barter_utils,
502            BASE_SEPOLIA_ADDRESSES.erc20_addresses.barter_utils
503        );
504        assert_eq!(
505            default_config.attestation_addresses.eas,
506            BASE_SEPOLIA_ADDRESSES.attestation_addresses.eas
507        );
508    }
509
510    #[test]
511    fn test_config_clone() {
512        let config = DefaultExtensionConfig::default();
513        let cloned = config.clone();
514
515        assert_eq!(config.arbiters_addresses.eas, cloned.arbiters_addresses.eas);
516        assert_eq!(
517            config.erc20_addresses.barter_utils,
518            cloned.erc20_addresses.barter_utils
519        );
520    }
521
522    #[test]
523    fn test_custom_config_with_struct_update_syntax() {
524        let custom_config = DefaultExtensionConfig {
525            arbiters_addresses: FILECOIN_CALIBRATION_ADDRESSES.arbiters_addresses,
526            ..BASE_SEPOLIA_ADDRESSES
527        };
528
529        // Verify arbiter addresses are from Filecoin
530        assert_eq!(
531            custom_config.arbiters_addresses.eas,
532            FILECOIN_CALIBRATION_ADDRESSES.arbiters_addresses.eas
533        );
534
535        // Verify other addresses are still from Base Sepolia
536        assert_eq!(
537            custom_config.erc20_addresses.eas,
538            BASE_SEPOLIA_ADDRESSES.erc20_addresses.eas
539        );
540    }
541
542    #[test]
543    fn test_all_address_fields_populated() {
544        let config = DefaultExtensionConfig::default();
545
546        // Test that no address is zero (all fields should be populated)
547        assert_ne!(config.arbiters_addresses.eas, Address::ZERO);
548        assert_ne!(config.erc20_addresses.eas, Address::ZERO);
549        assert_ne!(config.erc721_addresses.eas, Address::ZERO);
550        assert_ne!(config.erc1155_addresses.eas, Address::ZERO);
551        assert_ne!(config.token_bundle_addresses.eas, Address::ZERO);
552        assert_ne!(config.attestation_addresses.eas, Address::ZERO);
553
554        // Test specific contract addresses
555        assert_ne!(config.erc20_addresses.barter_utils, Address::ZERO);
556        assert_ne!(config.erc20_addresses.escrow_obligation_nontierable, Address::ZERO);
557        assert_ne!(config.erc20_addresses.payment_obligation, Address::ZERO);
558    }
559
560    #[test]
561    fn test_serialize_deserialize_default_extension_config() {
562        let original_config = DefaultExtensionConfig::default();
563
564        // Serialize to JSON
565        let json = serde_json::to_string(&original_config).expect("Failed to serialize");
566
567        // Deserialize from JSON
568        let deserialized_config: DefaultExtensionConfig =
569            serde_json::from_str(&json).expect("Failed to deserialize");
570
571        // Verify all fields match
572        assert_eq!(
573            original_config.arbiters_addresses.eas,
574            deserialized_config.arbiters_addresses.eas
575        );
576        assert_eq!(
577            original_config.arbiters_addresses.recipient_arbiter,
578            deserialized_config.arbiters_addresses.recipient_arbiter
579        );
580        assert_eq!(
581            original_config.erc20_addresses.eas,
582            deserialized_config.erc20_addresses.eas
583        );
584        assert_eq!(
585            original_config.erc20_addresses.barter_utils,
586            deserialized_config.erc20_addresses.barter_utils
587        );
588        assert_eq!(
589            original_config.erc721_addresses.eas,
590            deserialized_config.erc721_addresses.eas
591        );
592        assert_eq!(
593            original_config.erc1155_addresses.eas,
594            deserialized_config.erc1155_addresses.eas
595        );
596        assert_eq!(
597            original_config.token_bundle_addresses.eas,
598            deserialized_config.token_bundle_addresses.eas
599        );
600        assert_eq!(
601            original_config.attestation_addresses.eas,
602            deserialized_config.attestation_addresses.eas
603        );
604        assert_eq!(
605            original_config.string_obligation_addresses.eas,
606            deserialized_config.string_obligation_addresses.eas
607        );
608    }
609
610    #[test]
611    fn test_serialize_custom_config() {
612        // Create a custom config mixing addresses from different networks
613        let custom_config = DefaultExtensionConfig {
614            arbiters_addresses: FILECOIN_CALIBRATION_ADDRESSES.arbiters_addresses,
615            erc20_addresses: BASE_SEPOLIA_ADDRESSES.erc20_addresses,
616            ..FILECOIN_CALIBRATION_ADDRESSES
617        };
618
619        // Serialize to JSON
620        let json = serde_json::to_string(&custom_config).expect("Failed to serialize");
621
622        // Deserialize from JSON
623        let deserialized_config: DefaultExtensionConfig =
624            serde_json::from_str(&json).expect("Failed to deserialize");
625
626        // Verify mixed addresses are preserved
627        assert_eq!(
628            custom_config.arbiters_addresses.eas,
629            deserialized_config.arbiters_addresses.eas
630        );
631        assert_eq!(
632            FILECOIN_CALIBRATION_ADDRESSES.arbiters_addresses.eas,
633            deserialized_config.arbiters_addresses.eas
634        );
635        assert_eq!(
636            custom_config.erc20_addresses.eas,
637            deserialized_config.erc20_addresses.eas
638        );
639        assert_eq!(
640            BASE_SEPOLIA_ADDRESSES.erc20_addresses.eas,
641            deserialized_config.erc20_addresses.eas
642        );
643    }
644
645    #[test]
646    fn test_json_roundtrip_preserves_all_fields() {
647        let config = BASE_SEPOLIA_ADDRESSES;
648
649        // Convert to JSON and back
650        let json = serde_json::to_value(&config).expect("Failed to serialize to value");
651        let roundtrip_config: DefaultExtensionConfig =
652            serde_json::from_value(json).expect("Failed to deserialize from value");
653
654        // Comprehensive field checks
655        // Arbiters addresses
656        assert_eq!(
657            config.arbiters_addresses.eas,
658            roundtrip_config.arbiters_addresses.eas
659        );
660        assert_eq!(
661            config.arbiters_addresses.recipient_arbiter,
662            roundtrip_config.arbiters_addresses.recipient_arbiter
663        );
664        assert_eq!(
665            config.arbiters_addresses.trivial_arbiter,
666            roundtrip_config.arbiters_addresses.trivial_arbiter
667        );
668
669        // ERC20 addresses
670        assert_eq!(
671            config.erc20_addresses.eas,
672            roundtrip_config.erc20_addresses.eas
673        );
674        assert_eq!(
675            config.erc20_addresses.barter_utils,
676            roundtrip_config.erc20_addresses.barter_utils
677        );
678        assert_eq!(
679            config.erc20_addresses.escrow_obligation_nontierable,
680            roundtrip_config.erc20_addresses.escrow_obligation_nontierable
681        );
682        assert_eq!(
683            config.erc20_addresses.payment_obligation,
684            roundtrip_config.erc20_addresses.payment_obligation
685        );
686
687        // ERC721 addresses
688        assert_eq!(
689            config.erc721_addresses.eas,
690            roundtrip_config.erc721_addresses.eas
691        );
692        assert_eq!(
693            config.erc721_addresses.barter_utils,
694            roundtrip_config.erc721_addresses.barter_utils
695        );
696
697        // ERC1155 addresses
698        assert_eq!(
699            config.erc1155_addresses.eas,
700            roundtrip_config.erc1155_addresses.eas
701        );
702        assert_eq!(
703            config.erc1155_addresses.barter_utils,
704            roundtrip_config.erc1155_addresses.barter_utils
705        );
706
707        // Token bundle addresses
708        assert_eq!(
709            config.token_bundle_addresses.eas,
710            roundtrip_config.token_bundle_addresses.eas
711        );
712        assert_eq!(
713            config.token_bundle_addresses.barter_utils,
714            roundtrip_config.token_bundle_addresses.barter_utils
715        );
716
717        // Attestation addresses
718        assert_eq!(
719            config.attestation_addresses.eas,
720            roundtrip_config.attestation_addresses.eas
721        );
722        assert_eq!(
723            config.attestation_addresses.eas_schema_registry,
724            roundtrip_config.attestation_addresses.eas_schema_registry
725        );
726
727        // String obligation addresses
728        assert_eq!(
729            config.string_obligation_addresses.eas,
730            roundtrip_config.string_obligation_addresses.eas
731        );
732        assert_eq!(
733            config.string_obligation_addresses.obligation,
734            roundtrip_config.string_obligation_addresses.obligation
735        );
736    }
737}