Skip to main content

hypercall/shared/
option_token_address.rs

1use alloy::primitives::{Address, B256};
2use anyhow::{anyhow, Result};
3use hypercall_types::{
4    derive_option_token_address as derive_option_token_address_with_deployment,
5    OptionTokenDeployment, WalletAddress,
6};
7use rust_decimal::Decimal;
8use std::str::FromStr;
9
10const OPTION_REGISTRY_ADDRESS_ENV: &str = "OPTION_REGISTRY_ADDRESS";
11const OPTION_TOKEN_BEACON_PROXY_INIT_CODE_HASH_ENV: &str =
12    "OPTION_TOKEN_BEACON_PROXY_INIT_CODE_HASH";
13
14pub fn derive_option_token_address(
15    underlying: &str,
16    expiry_yyyymmdd: u64,
17    strike: Decimal,
18    option_type: &str,
19) -> Result<WalletAddress> {
20    Ok(derive_option_token_address_with_deployment(
21        current_option_token_deployment()?,
22        underlying,
23        expiry_yyyymmdd,
24        strike,
25        option_type,
26    )?)
27}
28
29pub fn ensure_option_token_derivation_supported() -> Result<()> {
30    current_option_token_deployment().map(|_| ())
31}
32
33fn current_option_token_deployment() -> Result<OptionTokenDeployment> {
34    Ok(OptionTokenDeployment::new(
35        env_address(OPTION_REGISTRY_ADDRESS_ENV)?,
36        env_b256(OPTION_TOKEN_BEACON_PROXY_INIT_CODE_HASH_ENV)?,
37    ))
38}
39
40fn env_address(name: &'static str) -> Result<Address> {
41    Address::from_str(&required_env(name)?)
42        .map_err(|err| anyhow!("Option token derivation requires valid {}: {}", name, err))
43}
44
45fn env_b256(name: &'static str) -> Result<B256> {
46    B256::from_str(&required_env(name)?)
47        .map_err(|err| anyhow!("Option token derivation requires valid {}: {}", name, err))
48}
49
50fn required_env(name: &'static str) -> Result<String> {
51    let value = std::env::var(name).map_err(|_| missing_contract_env_error(name))?;
52    let trimmed = value.trim();
53    if trimmed.is_empty() {
54        return Err(missing_contract_env_error(name));
55    }
56    Ok(trimmed.to_string())
57}
58
59fn missing_contract_env_error(name: &'static str) -> anyhow::Error {
60    anyhow!(
61        "Option token derivation requires {} from backend contracts config before startup. {}",
62        name,
63        testnet_mode_hint()
64    )
65}
66
67fn testnet_mode_hint() -> String {
68    match std::env::var("TESTNET_MODE") {
69        Ok(value) => format!("TESTNET_MODE is set to '{}'", value),
70        Err(_) => "TESTNET_MODE is not set".to_string(),
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::test_contracts::{
78        TESTNET_OPTION_REGISTRY_ADDRESS, TESTNET_OPTION_TOKEN_BEACON_PROXY_INIT_CODE_HASH,
79    };
80    use alloy::primitives::{address, b256};
81    use serial_test::serial;
82
83    struct EnvGuard {
84        key: &'static str,
85        original: Option<String>,
86    }
87
88    impl EnvGuard {
89        fn set(key: &'static str, value: &str) -> Self {
90            let original = std::env::var(key).ok();
91            unsafe {
92                std::env::set_var(key, value);
93            }
94            Self { key, original }
95        }
96
97        fn remove(key: &'static str) -> Self {
98            let original = std::env::var(key).ok();
99            unsafe {
100                std::env::remove_var(key);
101            }
102            Self { key, original }
103        }
104    }
105
106    impl Drop for EnvGuard {
107        fn drop(&mut self) {
108            match &self.original {
109                Some(value) => unsafe { std::env::set_var(self.key, value) },
110                None => unsafe { std::env::remove_var(self.key) },
111            }
112        }
113    }
114
115    #[test]
116    #[serial]
117    fn test_option_token_deployment_is_loaded_from_env() {
118        let _option_registry =
119            EnvGuard::set(OPTION_REGISTRY_ADDRESS_ENV, TESTNET_OPTION_REGISTRY_ADDRESS);
120        let _init_code_hash = EnvGuard::set(
121            OPTION_TOKEN_BEACON_PROXY_INIT_CODE_HASH_ENV,
122            TESTNET_OPTION_TOKEN_BEACON_PROXY_INIT_CODE_HASH,
123        );
124
125        assert_eq!(
126            current_option_token_deployment().unwrap(),
127            OptionTokenDeployment::new(
128                address!("ed55460d83cc0b37964c377a058b8d6ce0da3034"),
129                b256!("4bff6a944492f2b349eaafcfac839d3e141f08391de02e808c5cfa97a16b5d64"),
130            )
131        );
132    }
133
134    #[test]
135    #[serial]
136    fn test_missing_contract_env_fails_closed() {
137        let _option_registry = EnvGuard::remove(OPTION_REGISTRY_ADDRESS_ENV);
138        let _init_code_hash = EnvGuard::remove(OPTION_TOKEN_BEACON_PROXY_INIT_CODE_HASH_ENV);
139        let _testnet_mode = EnvGuard::remove("TESTNET_MODE");
140
141        let err = current_option_token_deployment().unwrap_err().to_string();
142
143        assert!(
144            err.contains("OPTION_REGISTRY_ADDRESS"),
145            "unexpected error: {}",
146            err
147        );
148        assert!(
149            err.contains("backend contracts config"),
150            "unexpected error: {}",
151            err
152        );
153    }
154}