hypercall/shared/
option_token_address.rs1use 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}