#[cfg(feature = "json")]
use std::borrow::Cow;
#[cfg(feature = "json")]
use std::fs::File;
#[cfg(feature = "json")]
use std::path::Path;
#[cfg(feature = "json")]
use anyhow::Context;
use bincode::{Decode, Encode};
#[cfg(feature = "json")]
use serde::de::DeserializeOwned;
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
use crate::{Asset, EscrowError, Party};
#[cfg(feature = "json")]
pub const ESCROW_PARAMS_PATH: &str =
concat!(env!("CARGO_MANIFEST_DIR"), "/../deploy/escrow_params.json");
#[cfg(feature = "json")]
pub const ESCROW_METADATA_PATH: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../deploy/escrow_metadata.json"
);
#[cfg(feature = "json")]
pub const ESCROW_CONDITIONS_PATH: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../deploy/escrow_conditions.json"
);
#[cfg(feature = "json")]
#[must_use]
pub fn expand_env_vars(input: &str) -> Cow<'_, str> {
expand_with(input, |name| std::env::var(name).ok())
}
#[cfg(feature = "json")]
fn expand_with<F>(input: &str, mut lookup: F) -> Cow<'_, str>
where
F: FnMut(&str) -> Option<String>,
{
if !input.contains("${") {
return Cow::Borrowed(input);
}
let mut result = String::with_capacity(input.len());
let mut remaining = input;
while let Some(start) = remaining.find("${") {
result.push_str(&remaining[..start]);
let after_start = &remaining[start + 2..];
match after_start.find('}') {
Some(end) => {
let var_name = &after_start[..end];
if let Some(value) = lookup(var_name) {
result.push_str(&value);
}
remaining = &after_start[end + 1..];
}
None => {
result.push_str(&remaining[start..]);
remaining = "";
}
}
}
result.push_str(remaining);
Cow::Owned(result)
}
#[cfg(feature = "json")]
fn expand_env_checked(input: &str) -> anyhow::Result<String> {
let mut missing = Vec::new();
let expanded = expand_with(input, |name| match std::env::var(name) {
Ok(value) => Some(value),
Err(_) => {
missing.push(name.to_string());
None
}
})
.into_owned();
if missing.is_empty() {
Ok(expanded)
} else {
anyhow::bail!(
"unset environment variable(s) referenced in configuration: {}",
missing.join(", ")
)
}
}
#[cfg(feature = "json")]
pub fn load_escrow_data<P, T>(path: P) -> anyhow::Result<T>
where
P: AsRef<Path>,
T: DeserializeOwned,
{
let path = path.as_ref();
let content =
std::fs::read_to_string(path).with_context(|| format!("loading escrow data: {path:?}"))?;
let expanded = expand_env_checked(&content)?;
serde_json::from_str(&expanded).with_context(|| format!("parsing JSON from {path:?}"))
}
#[cfg(feature = "json")]
pub fn save_escrow_data<P, T>(path: P, data: &T) -> anyhow::Result<()>
where
P: AsRef<Path>,
T: Serialize,
{
let path = path.as_ref();
let file = File::create(path).with_context(|| format!("creating file {path:?}"))?;
serde_json::to_writer_pretty(file, data)
.with_context(|| format!("serializing to JSON to {path:?}"))
}
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, Encode, Decode, PartialEq, Eq)]
pub enum ExecutionState {
Initialized,
Funded,
ConditionsMet,
}
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Encode, Decode)]
pub struct EscrowMetadata {
pub params: EscrowParams,
pub state: ExecutionState,
pub escrow_id: Option<u64>,
}
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Encode, Decode)]
pub struct EscrowParams {
pub chain_config: ChainConfig,
pub asset: Asset,
pub sender: Party,
pub recipient: Party,
pub finish_after: Option<u64>,
pub cancel_after: Option<u64>,
pub has_conditions: bool,
}
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Encode, Decode)]
pub struct ChainConfig {
pub chain: Chain,
pub rpc_url: String,
pub sender_private_id: String,
pub agent_id: String,
}
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "lowercase"))]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode)]
pub enum Chain {
Ethereum,
Solana,
}
impl AsRef<str> for Chain {
fn as_ref(&self) -> &str {
match self {
Chain::Ethereum => "ethereum",
Chain::Solana => "solana",
}
}
}
impl std::str::FromStr for Chain {
type Err = EscrowError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"ethereum" | "eth" => Ok(Self::Ethereum),
"solana" | "sol" => Ok(Self::Solana),
_ => Err(EscrowError::UnsupportedChain),
}
}
}
#[cfg(all(test, feature = "json"))]
mod tests {
use std::str::FromStr;
use super::*;
#[test]
fn chain_from_str_ethereum() {
assert!(matches!(Chain::from_str("ethereum"), Ok(Chain::Ethereum)));
assert!(matches!(Chain::from_str("ETHEREUM"), Ok(Chain::Ethereum)));
assert!(matches!(Chain::from_str("eth"), Ok(Chain::Ethereum)));
assert!(matches!(Chain::from_str("ETH"), Ok(Chain::Ethereum)));
}
#[test]
fn chain_from_str_solana() {
assert!(matches!(Chain::from_str("solana"), Ok(Chain::Solana)));
assert!(matches!(Chain::from_str("SOLANA"), Ok(Chain::Solana)));
assert!(matches!(Chain::from_str("sol"), Ok(Chain::Solana)));
assert!(matches!(Chain::from_str("SOL"), Ok(Chain::Solana)));
}
#[test]
fn chain_from_str_unsupported() {
assert!(matches!(
Chain::from_str("bitcoin"),
Err(EscrowError::UnsupportedChain)
));
assert!(matches!(
Chain::from_str(""),
Err(EscrowError::UnsupportedChain)
));
}
#[test]
fn chain_as_ref() {
assert_eq!(Chain::Ethereum.as_ref(), "ethereum");
assert_eq!(Chain::Solana.as_ref(), "solana");
}
#[test]
fn expand_env_vars_no_vars() {
let result = expand_env_vars("no variables here");
assert_eq!(result, "no variables here");
assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
}
#[test]
fn expand_with_single_var() {
let result = expand_with("prefix-${VAR}-suffix", |name| {
(name == "VAR").then(|| "hello".to_string())
});
assert_eq!(result, "prefix-hello-suffix");
}
#[test]
fn expand_with_multiple_vars() {
let result = expand_with("${A} and ${B}", |name| match name {
"A" => Some("alpha".to_string()),
"B" => Some("beta".to_string()),
_ => None,
});
assert_eq!(result, "alpha and beta");
}
#[test]
fn expand_with_unset_becomes_empty() {
let result = expand_with("before-${UNSET}-after", |_| None);
assert_eq!(result, "before--after");
}
#[test]
fn expand_with_unclosed_brace() {
let result = expand_with("prefix-${UNCLOSED", |_| Some("value".to_string()));
assert_eq!(result, "prefix-${UNCLOSED");
}
#[test]
fn expand_env_checked_passthrough_without_vars() {
let result = expand_env_checked("no variables here").unwrap();
assert_eq!(result, "no variables here");
}
#[test]
fn expand_env_checked_errors_on_unset() {
let err = expand_env_checked("key=${ZESCROW_TEST_DEFINITELY_UNSET_VAR_XYZ}").unwrap_err();
assert!(
err.to_string()
.contains("ZESCROW_TEST_DEFINITELY_UNSET_VAR_XYZ")
);
}
}