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
24#[cfg(feature = "json")]
26pub const ESCROW_PARAMS_PATH: &str =
27 concat!(env!("CARGO_MANIFEST_DIR"), "/../deploy/escrow_params.json");
28
29#[cfg(feature = "json")]
31pub const ESCROW_METADATA_PATH: &str = concat!(
32 env!("CARGO_MANIFEST_DIR"),
33 "/../deploy/escrow_metadata.json"
34);
35
36#[cfg(feature = "json")]
38pub const ESCROW_CONDITIONS_PATH: &str = concat!(
39 env!("CARGO_MANIFEST_DIR"),
40 "/../deploy/escrow_conditions.json"
41);
42
43#[cfg(feature = "json")]
60#[must_use]
61pub fn expand_env_vars(input: &str) -> Cow<'_, str> {
62 expand_with(input, |name| std::env::var(name).ok())
63}
64
65#[cfg(feature = "json")]
70fn expand_with<F>(input: &str, mut lookup: F) -> Cow<'_, str>
71where
72 F: FnMut(&str) -> Option<String>,
73{
74 if !input.contains("${") {
75 return Cow::Borrowed(input);
76 }
77
78 let mut result = String::with_capacity(input.len());
79 let mut remaining = input;
80
81 while let Some(start) = remaining.find("${") {
82 result.push_str(&remaining[..start]);
83
84 let after_start = &remaining[start + 2..];
85 match after_start.find('}') {
86 Some(end) => {
87 let var_name = &after_start[..end];
88 if let Some(value) = lookup(var_name) {
89 result.push_str(&value);
90 }
91 remaining = &after_start[end + 1..];
92 }
93 None => {
94 result.push_str(&remaining[start..]);
95 remaining = "";
96 }
97 }
98 }
99
100 result.push_str(remaining);
101 Cow::Owned(result)
102}
103
104#[cfg(feature = "json")]
112fn expand_env_checked(input: &str) -> anyhow::Result<String> {
113 let mut missing = Vec::new();
114 let expanded = expand_with(input, |name| match std::env::var(name) {
115 Ok(value) => Some(value),
116 Err(_) => {
117 missing.push(name.to_string());
118 None
119 }
120 })
121 .into_owned();
122
123 if missing.is_empty() {
124 Ok(expanded)
125 } else {
126 anyhow::bail!(
127 "unset environment variable(s) referenced in configuration: {}",
128 missing.join(", ")
129 )
130 }
131}
132
133#[cfg(feature = "json")]
155pub fn load_escrow_data<P, T>(path: P) -> anyhow::Result<T>
156where
157 P: AsRef<Path>,
158 T: DeserializeOwned,
159{
160 let path = path.as_ref();
161 let content =
162 std::fs::read_to_string(path).with_context(|| format!("loading escrow data: {path:?}"))?;
163 let expanded = expand_env_checked(&content)?;
164 serde_json::from_str(&expanded).with_context(|| format!("parsing JSON from {path:?}"))
165}
166
167#[cfg(feature = "json")]
186pub fn save_escrow_data<P, T>(path: P, data: &T) -> anyhow::Result<()>
187where
188 P: AsRef<Path>,
189 T: Serialize,
190{
191 let path = path.as_ref();
192 let file = File::create(path).with_context(|| format!("creating file {path:?}"))?;
193 serde_json::to_writer_pretty(file, data)
194 .with_context(|| format!("serializing to JSON to {path:?}"))
195}
196
197#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
199#[derive(Debug, Clone, Copy, Encode, Decode, PartialEq, Eq)]
200pub enum ExecutionState {
201 Initialized,
203
204 Funded,
206
207 ConditionsMet,
210}
211
212#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
214#[derive(Debug, Clone, Encode, Decode)]
215pub struct EscrowMetadata {
216 pub params: EscrowParams,
218 pub state: ExecutionState,
220 pub escrow_id: Option<u64>,
222}
223
224#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
226#[derive(Debug, Clone, Encode, Decode)]
227pub struct EscrowParams {
228 pub chain_config: ChainConfig,
230
231 pub asset: Asset,
233
234 pub sender: Party,
236
237 pub recipient: Party,
239
240 pub finish_after: Option<u64>,
243
244 pub cancel_after: Option<u64>,
247
248 pub has_conditions: bool,
250}
251
252#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
254#[derive(Debug, Clone, Encode, Decode)]
255pub struct ChainConfig {
256 pub chain: Chain,
258 pub rpc_url: String,
260 pub sender_private_id: String,
265 pub agent_id: String,
267}
268
269#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
271#[cfg_attr(feature = "json", serde(rename_all = "lowercase"))]
272#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode)]
273pub enum Chain {
274 Ethereum,
276 Solana,
278}
279
280impl AsRef<str> for Chain {
281 fn as_ref(&self) -> &str {
282 match self {
283 Chain::Ethereum => "ethereum",
284 Chain::Solana => "solana",
285 }
286 }
287}
288
289impl std::str::FromStr for Chain {
290 type Err = EscrowError;
291
292 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
298 match s.to_lowercase().as_str() {
299 "ethereum" | "eth" => Ok(Self::Ethereum),
300 "solana" | "sol" => Ok(Self::Solana),
301 _ => Err(EscrowError::UnsupportedChain),
302 }
303 }
304}
305
306#[cfg(all(test, feature = "json"))]
307mod tests {
308 use std::str::FromStr;
309
310 use super::*;
311
312 #[test]
313 fn chain_from_str_ethereum() {
314 assert!(matches!(Chain::from_str("ethereum"), Ok(Chain::Ethereum)));
315 assert!(matches!(Chain::from_str("ETHEREUM"), Ok(Chain::Ethereum)));
316 assert!(matches!(Chain::from_str("eth"), Ok(Chain::Ethereum)));
317 assert!(matches!(Chain::from_str("ETH"), Ok(Chain::Ethereum)));
318 }
319
320 #[test]
321 fn chain_from_str_solana() {
322 assert!(matches!(Chain::from_str("solana"), Ok(Chain::Solana)));
323 assert!(matches!(Chain::from_str("SOLANA"), Ok(Chain::Solana)));
324 assert!(matches!(Chain::from_str("sol"), Ok(Chain::Solana)));
325 assert!(matches!(Chain::from_str("SOL"), Ok(Chain::Solana)));
326 }
327
328 #[test]
329 fn chain_from_str_unsupported() {
330 assert!(matches!(
331 Chain::from_str("bitcoin"),
332 Err(EscrowError::UnsupportedChain)
333 ));
334 assert!(matches!(
335 Chain::from_str(""),
336 Err(EscrowError::UnsupportedChain)
337 ));
338 }
339
340 #[test]
341 fn chain_as_ref() {
342 assert_eq!(Chain::Ethereum.as_ref(), "ethereum");
343 assert_eq!(Chain::Solana.as_ref(), "solana");
344 }
345
346 #[test]
347 fn expand_env_vars_no_vars() {
348 let result = expand_env_vars("no variables here");
349 assert_eq!(result, "no variables here");
350 assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
352 }
353
354 #[test]
355 fn expand_with_single_var() {
356 let result = expand_with("prefix-${VAR}-suffix", |name| {
357 (name == "VAR").then(|| "hello".to_string())
358 });
359 assert_eq!(result, "prefix-hello-suffix");
360 }
361
362 #[test]
363 fn expand_with_multiple_vars() {
364 let result = expand_with("${A} and ${B}", |name| match name {
365 "A" => Some("alpha".to_string()),
366 "B" => Some("beta".to_string()),
367 _ => None,
368 });
369 assert_eq!(result, "alpha and beta");
370 }
371
372 #[test]
373 fn expand_with_unset_becomes_empty() {
374 let result = expand_with("before-${UNSET}-after", |_| None);
375 assert_eq!(result, "before--after");
376 }
377
378 #[test]
379 fn expand_with_unclosed_brace() {
380 let result = expand_with("prefix-${UNCLOSED", |_| Some("value".to_string()));
381 assert_eq!(result, "prefix-${UNCLOSED");
382 }
383
384 #[test]
385 fn expand_env_checked_passthrough_without_vars() {
386 let result = expand_env_checked("no variables here").unwrap();
387 assert_eq!(result, "no variables here");
388 }
389
390 #[test]
391 fn expand_env_checked_errors_on_unset() {
392 let err = expand_env_checked("key=${ZESCROW_TEST_DEFINITELY_UNSET_VAR_XYZ}").unwrap_err();
393 assert!(
394 err.to_string()
395 .contains("ZESCROW_TEST_DEFINITELY_UNSET_VAR_XYZ")
396 );
397 }
398}