1#[cfg(feature = "json")]
8use std::borrow::Cow;
9#[cfg(feature = "json")]
10use std::fs::File;
11#[cfg(feature = "json")]
12use std::path::Path;
13
14#[cfg(feature = "json")]
15use anyhow::Context;
16use bincode::{Decode, Encode};
17#[cfg(feature = "json")]
18use serde::de::DeserializeOwned;
19#[cfg(feature = "json")]
20use serde::{Deserialize, Serialize};
21
22use crate::{Asset, EscrowError, Party};
23
24pub const ESCROW_PARAMS_PATH: &str =
26 concat!(env!("CARGO_MANIFEST_DIR"), "/../deploy/escrow_params.json");
27
28pub const ESCROW_METADATA_PATH: &str = concat!(
30 env!("CARGO_MANIFEST_DIR"),
31 "/../deploy/escrow_metadata.json"
32);
33
34pub const ESCROW_CONDITIONS_PATH: &str = concat!(
36 env!("CARGO_MANIFEST_DIR"),
37 "/../deploy/escrow_conditions.json"
38);
39
40#[cfg(feature = "json")]
58#[must_use]
59pub fn expand_env_vars(input: &str) -> Cow<'_, str> {
60 if !input.contains("${") {
61 return Cow::Borrowed(input);
62 }
63
64 let mut result = String::with_capacity(input.len());
65 let mut remaining = input;
66
67 while let Some(start) = remaining.find("${") {
68 result.push_str(&remaining[..start]);
69
70 let after_start = &remaining[start + 2..];
71 match after_start.find('}') {
72 Some(end) => {
73 let var_name = &after_start[..end];
74 if let Ok(value) = std::env::var(var_name) {
75 result.push_str(&value);
76 }
77 remaining = &after_start[end + 1..];
78 }
79 None => {
80 result.push_str(&remaining[start..]);
81 remaining = "";
82 }
83 }
84 }
85
86 result.push_str(remaining);
87 Cow::Owned(result)
88}
89
90#[cfg(feature = "json")]
112pub fn load_escrow_data<P, T>(path: P) -> anyhow::Result<T>
113where
114 P: AsRef<Path>,
115 T: DeserializeOwned,
116{
117 let path = path.as_ref();
118 let content =
119 std::fs::read_to_string(path).with_context(|| format!("loading escrow data: {path:?}"))?;
120 let expanded = expand_env_vars(&content);
121 serde_json::from_str(&expanded).with_context(|| format!("parsing JSON from {path:?}"))
122}
123
124#[cfg(feature = "json")]
143pub fn save_escrow_data<P, T>(path: P, data: &T) -> anyhow::Result<()>
144where
145 P: AsRef<Path>,
146 T: Serialize,
147{
148 let path = path.as_ref();
149 let file = File::create(path).with_context(|| format!("creating file {path:?}"))?;
150 serde_json::to_writer_pretty(file, data)
151 .with_context(|| format!("serializing to JSON to {path:?}"))
152}
153
154#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
156#[derive(Debug, Clone, Copy, Encode, Decode, PartialEq, Eq)]
157pub enum ExecutionState {
158 Initialized,
160
161 Funded,
163
164 ConditionsMet,
167}
168
169#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
171#[derive(Debug, Clone, Encode, Decode)]
172pub enum ExecutionResult {
173 Ok(ExecutionState),
175 Err(String),
177}
178
179#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
181#[derive(Debug, Clone, Encode, Decode)]
182pub struct EscrowMetadata {
183 pub params: EscrowParams,
185 pub state: ExecutionState,
187 pub escrow_id: Option<u64>,
189}
190
191#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
193#[derive(Debug, Clone, Encode, Decode)]
194pub struct EscrowParams {
195 pub chain_config: ChainConfig,
197
198 pub asset: Asset,
200
201 pub sender: Party,
203
204 pub recipient: Party,
206
207 pub finish_after: Option<u64>,
210
211 pub cancel_after: Option<u64>,
214
215 pub has_conditions: bool,
217}
218
219#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
221#[derive(Debug, Clone, Encode, Decode)]
222pub struct ChainConfig {
223 pub chain: Chain,
225 pub rpc_url: String,
227 pub sender_private_id: String,
232 pub agent_id: String,
234}
235
236#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
238#[cfg_attr(feature = "json", serde(rename_all = "lowercase"))]
239#[derive(Debug, Copy, Clone, Encode, Decode)]
240pub enum Chain {
241 Ethereum,
243 Solana,
245}
246
247impl AsRef<str> for Chain {
248 fn as_ref(&self) -> &str {
249 match self {
250 Chain::Ethereum => "ethereum",
251 Chain::Solana => "solana",
252 }
253 }
254}
255
256impl std::str::FromStr for Chain {
257 type Err = EscrowError;
258
259 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
265 match s.to_lowercase().as_str() {
266 "ethereum" | "eth" => Ok(Self::Ethereum),
267 "solana" | "sol" => Ok(Self::Solana),
268 _ => Err(EscrowError::UnsupportedChain),
269 }
270 }
271}
272
273#[cfg(all(test, feature = "json"))]
274mod tests {
275 use std::str::FromStr;
276
277 use super::*;
278
279 #[test]
280 fn chain_from_str_ethereum() {
281 assert!(matches!(Chain::from_str("ethereum"), Ok(Chain::Ethereum)));
282 assert!(matches!(Chain::from_str("ETHEREUM"), Ok(Chain::Ethereum)));
283 assert!(matches!(Chain::from_str("eth"), Ok(Chain::Ethereum)));
284 assert!(matches!(Chain::from_str("ETH"), Ok(Chain::Ethereum)));
285 }
286
287 #[test]
288 fn chain_from_str_solana() {
289 assert!(matches!(Chain::from_str("solana"), Ok(Chain::Solana)));
290 assert!(matches!(Chain::from_str("SOLANA"), Ok(Chain::Solana)));
291 assert!(matches!(Chain::from_str("sol"), Ok(Chain::Solana)));
292 assert!(matches!(Chain::from_str("SOL"), Ok(Chain::Solana)));
293 }
294
295 #[test]
296 fn chain_from_str_unsupported() {
297 assert!(matches!(
298 Chain::from_str("bitcoin"),
299 Err(EscrowError::UnsupportedChain)
300 ));
301 assert!(matches!(
302 Chain::from_str(""),
303 Err(EscrowError::UnsupportedChain)
304 ));
305 }
306
307 #[test]
308 fn chain_as_ref() {
309 assert_eq!(Chain::Ethereum.as_ref(), "ethereum");
310 assert_eq!(Chain::Solana.as_ref(), "solana");
311 }
312
313 #[test]
314 fn expand_env_vars_no_vars() {
315 let input = "no variables here";
316 let result = expand_env_vars(input);
317 assert_eq!(result, "no variables here");
318 assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
320 }
321
322 #[test]
323 fn expand_env_vars_single_var() {
324 std::env::set_var("TEST_VAR_SINGLE", "hello");
325 let result = expand_env_vars("prefix-${TEST_VAR_SINGLE}-suffix");
326 assert_eq!(result, "prefix-hello-suffix");
327 std::env::remove_var("TEST_VAR_SINGLE");
328 }
329
330 #[test]
331 fn expand_env_vars_multiple_vars() {
332 std::env::set_var("TEST_VAR_A", "alpha");
333 std::env::set_var("TEST_VAR_B", "beta");
334 let result = expand_env_vars("${TEST_VAR_A} and ${TEST_VAR_B}");
335 assert_eq!(result, "alpha and beta");
336 std::env::remove_var("TEST_VAR_A");
337 std::env::remove_var("TEST_VAR_B");
338 }
339
340 #[test]
341 fn expand_env_vars_unset_becomes_empty() {
342 std::env::remove_var("TEST_VAR_UNSET_XYZ");
343 let result = expand_env_vars("before-${TEST_VAR_UNSET_XYZ}-after");
344 assert_eq!(result, "before--after");
345 }
346
347 #[test]
348 fn expand_env_vars_unclosed_brace() {
349 let result = expand_env_vars("prefix-${UNCLOSED");
350 assert_eq!(result, "prefix-${UNCLOSED");
351 }
352}