use core::fmt;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use paralock_core::keys::{KeyPair, PrivateKey};
use paralock_core::protocol::{ClaimLimits, EvmChain, Hex, MAINNET_POLICY, MakerLimits, TimeoutPolicy};
use paralock_core::zcash::{Network, TransparentAddress};
use paralock_evm::alloy::primitives::Address;
use paralock_evm::{AdaptorEscrowClient, EvmConfig, EvmHtlcClient, ScanConfig, known_network};
use zeroize::Zeroizing;
use crate::api::MakerApi;
use crate::evm::{
AdaptorEvmClient, ChainClients, EvmClient, RelayerClients, TakerChain, VerifierClients, parse_address,
};
use crate::evm_adapter::{AdaptorEscrow, HtlcEscrow};
use crate::maker::{AfterClaim, Maker, MakerConfig, MakerUpdateHook};
use crate::price::PriceSource;
use crate::store::{FileStore, MemoryStore, SwapStore};
use crate::taker::{
FreshRecipientHook, MineHook, Taker, TakerConfig, TakerLimits, TakerPriceCheck, TakerTimeouts, UpdateHook,
};
use crate::zcash::{ZcashNode, ZcashRpc};
use crate::{Error, Result};
#[derive(Clone)]
pub enum ZcashBackend {
Rpc {
url: String,
auth: Option<(String, Zeroizing<String>)>,
},
#[cfg(feature = "lightwalletd")]
Lightwalletd {
url: String,
},
Node(Arc<dyn ZcashNode>),
}
impl fmt::Debug for ZcashBackend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Rpc { .. } => "ZcashBackend::Rpc",
#[cfg(feature = "lightwalletd")]
Self::Lightwalletd { .. } => "ZcashBackend::Lightwalletd",
Self::Node(_) => "ZcashBackend::Node",
})
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct ZcashOptions {
pub network: Network,
pub backend: ZcashBackend,
pub key: Zeroizing<String>,
}
impl ZcashOptions {
pub fn new(network: Network, backend: ZcashBackend, key: impl Into<String>) -> Self {
Self { network, backend, key: Zeroizing::new(key.into()) }
}
}
impl fmt::Debug for ZcashOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ZcashOptions")
.field("network", &self.network)
.field("backend", &self.backend)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct EvmChainOptions {
pub rpc_url: String,
pub chain_id: u64,
pub htlc: Hex,
pub adaptor_escrow: Option<Hex>,
pub account: Zeroizing<String>,
pub from_block: Option<u64>,
pub fee_token: Option<Hex>,
pub tempo: Option<bool>,
pub rpc_timeout: Option<Duration>,
pub rpc_retries: Option<u32>,
pub receipt_timeout: Option<Duration>,
pub verify_rpc_url: Option<String>,
}
impl fmt::Debug for EvmChainOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EvmChainOptions")
.field("chain_id", &self.chain_id)
.field("htlc", &self.htlc)
.field("adaptor_escrow", &self.adaptor_escrow)
.field("from_block", &self.from_block)
.field("receipt_timeout", &self.receipt_timeout)
.field("verifier", &self.verify_rpc_url.is_some())
.finish_non_exhaustive()
}
}
impl EvmChainOptions {
pub fn new(rpc_url: impl Into<String>, chain_id: u64, htlc: Hex, account: impl Into<String>) -> Self {
Self {
rpc_url: rpc_url.into(),
chain_id,
htlc,
adaptor_escrow: None,
account: Zeroizing::new(account.into()),
from_block: None,
fee_token: None,
tempo: None,
rpc_timeout: None,
rpc_retries: None,
receipt_timeout: None,
verify_rpc_url: None,
}
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct TakerEvmChainOptions {
pub evm: EvmChainOptions,
pub tokens: Vec<Hex>,
pub token_decimals: Option<u32>,
pub relayer: Option<Zeroizing<String>>,
pub claim_limits: Option<ClaimLimits>,
}
impl TakerEvmChainOptions {
pub fn new(evm: EvmChainOptions, tokens: Vec<Hex>) -> Self {
Self { evm, tokens, token_decimals: None, relayer: None, claim_limits: None }
}
}
impl fmt::Debug for TakerEvmChainOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TakerEvmChainOptions")
.field("evm", &self.evm)
.field("tokens", &self.tokens)
.field("token_decimals", &self.token_decimals)
.field("relayer", &self.relayer.as_ref().map(|_| "<redacted>"))
.field("claim_limits", &self.claim_limits)
.finish()
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct MakerEvmChainOptions {
pub evm: EvmChainOptions,
pub token: Hex,
pub token_symbol: String,
pub token_decimals: Option<u32>,
}
impl MakerEvmChainOptions {
pub fn new(evm: EvmChainOptions, token: Hex, token_symbol: impl Into<String>) -> Self {
Self { evm, token, token_symbol: token_symbol.into(), token_decimals: None }
}
}
#[derive(Clone)]
pub enum StoreOption {
Dir(PathBuf),
Store(Arc<dyn SwapStore>),
Memory,
}
impl fmt::Debug for StoreOption {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Dir(dir) => f.debug_tuple("StoreOption::Dir").field(dir).finish(),
Self::Store(_) => f.write_str("StoreOption::Store"),
Self::Memory => f.write_str("StoreOption::Memory"),
}
}
}
#[derive(Clone)]
pub enum MakerEndpoint {
#[cfg(feature = "client")]
Url(String),
Api(Arc<dyn MakerApi>),
}
impl fmt::Debug for MakerEndpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "client")]
Self::Url(url) => f.debug_tuple("MakerEndpoint::Url").field(url).finish(),
Self::Api(_) => f.write_str("MakerEndpoint::Api"),
}
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct CreateTakerOptions {
pub zcash: ZcashOptions,
pub return_address: Option<String>,
pub evm: Vec<(EvmChain, TakerEvmChainOptions)>,
pub maker: MakerEndpoint,
pub store: StoreOption,
pub limits: TakerLimits,
pub timeouts: TakerTimeouts,
pub price: TakerPriceCheck,
pub allow_unknown_upgrades: bool,
pub on_update: Option<UpdateHook>,
pub on_fresh_recipient: Option<FreshRecipientHook>,
pub mine_zcash: Option<MineHook>,
}
impl CreateTakerOptions {
pub fn new(zcash: ZcashOptions, maker: MakerEndpoint, store: StoreOption, price: TakerPriceCheck) -> Self {
Self {
zcash,
return_address: None,
evm: Vec::new(),
maker,
store,
limits: TakerLimits::default(),
timeouts: TakerTimeouts::default(),
price,
allow_unknown_upgrades: false,
on_update: None,
on_fresh_recipient: None,
mine_zcash: None,
}
}
#[must_use]
pub fn with_chain(mut self, chain: EvmChain, options: TakerEvmChainOptions) -> Self {
self.evm.push((chain, options));
self
}
}
impl fmt::Debug for CreateTakerOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CreateTakerOptions")
.field("zcash", &self.zcash)
.field("return_address", &self.return_address)
.field("evm", &self.evm)
.field("maker", &self.maker)
.field("store", &self.store)
.field("limits", &self.limits)
.field("timeouts", &self.timeouts)
.field("price", &self.price)
.field("allow_unknown_upgrades", &self.allow_unknown_upgrades)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct CreateMakerOptions {
pub zcash: ZcashOptions,
pub payout_address: Option<String>,
pub evm: Vec<(EvmChain, MakerEvmChainOptions)>,
pub price: Arc<dyn PriceSource>,
pub spread_bps: Option<f64>,
pub policy: Option<TimeoutPolicy>,
pub limits: Option<MakerLimits>,
pub store: StoreOption,
pub poll_interval: Option<Duration>,
pub after_claim: Option<AfterClaim>,
pub allow_unknown_upgrades: bool,
pub on_update: Option<MakerUpdateHook>,
}
impl CreateMakerOptions {
pub fn new(zcash: ZcashOptions, price: Arc<dyn PriceSource>, store: StoreOption) -> Self {
Self {
zcash,
payout_address: None,
evm: Vec::new(),
price,
spread_bps: None,
policy: None,
limits: None,
store,
poll_interval: None,
after_claim: None,
allow_unknown_upgrades: false,
on_update: None,
}
}
#[must_use]
pub fn with_chain(mut self, chain: EvmChain, options: MakerEvmChainOptions) -> Self {
self.evm.push((chain, options));
self
}
}
impl fmt::Debug for CreateMakerOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CreateMakerOptions")
.field("zcash", &self.zcash)
.field("payout_address", &self.payout_address)
.field("evm", &self.evm)
.field("store", &self.store)
.finish_non_exhaustive()
}
}
fn key_pair(key: &str) -> Result<KeyPair> {
PrivateKey::from_hex(key.strip_prefix("0x").unwrap_or(key))
.map(KeyPair::from_private)
.map_err(|_| Error::invalid_request("zcash.key must be a 32-byte secp256k1 private key"))
}
pub fn zcash_node(backend: &ZcashBackend) -> Result<Arc<dyn ZcashNode>> {
Ok(match backend {
ZcashBackend::Rpc { url, auth } => {
let rpc = ZcashRpc::new(url.clone())?;
Arc::new(match auth {
Some((user, pass)) => rpc.with_auth(user.clone(), pass.as_str().to_owned()),
None => rpc,
})
}
#[cfg(feature = "lightwalletd")]
ZcashBackend::Lightwalletd { url } => Arc::new(crate::lightwalletd::Lightwalletd::new(url)?),
ZcashBackend::Node(node) => node.clone(),
})
}
fn store(option: &StoreOption, network: Network, role: &str) -> Result<Arc<dyn SwapStore>> {
let store: Arc<dyn SwapStore> = match option {
StoreOption::Dir(dir) if network == Network::Mainnet => Arc::new(FileStore::open_private(dir.clone())?),
StoreOption::Dir(dir) => Arc::new(FileStore::open(dir.clone())?),
StoreOption::Store(store) => store.clone(),
StoreOption::Memory => Arc::new(MemoryStore::new()),
};
if network == Network::Mainnet && !store.durable() {
return Err(Error::invalid_request(format!(
"a {role} on mainnet needs a durable store (a directory or a SwapStore)"
)));
}
Ok(store)
}
fn evm_config(chain: EvmChain, o: &EvmChainOptions, rpc_url: &str, account: &str) -> Result<EvmConfig> {
if !(rpc_url.starts_with("http://") || rpc_url.starts_with("https://")) {
return Err(Error::invalid_request(format!("{chain}.rpcUrl must be an http(s) URL")));
}
if o.chain_id == 0 {
return Err(Error::invalid_request(format!("{chain}.chainId must be a positive integer")));
}
let mut config = EvmConfig::new(rpc_url.to_owned(), o.chain_id)
.with_private_key(account)
.map_err(|_| Error::invalid_request(format!("{chain}: the EVM account must be a 0x 32-byte private key")))?;
if let Some(token) = &o.fee_token {
config = config.with_fee_token(Address::from(parse_address(token, &format!("{chain}.feeToken"))?.0));
}
if let Some(tempo) = o.tempo {
config = config.with_tempo(tempo);
}
if o.rpc_timeout.is_some() || o.rpc_retries.is_some() {
config = config.with_rpc_policy(
o.rpc_timeout.unwrap_or(Duration::from_secs(10)),
o.rpc_retries.unwrap_or(3),
Duration::from_millis(250),
);
}
if let Some(timeout) = o.receipt_timeout {
config = config.with_receipt_timeout(timeout);
}
Ok(config)
}
pub async fn connect_evm_chain(
chain: EvmChain,
o: &EvmChainOptions,
account: &str,
) -> Result<(Arc<dyn EvmClient>, Option<Arc<dyn AdaptorEvmClient>>)> {
connect_escrows(chain, o, &o.rpc_url, account).await
}
async fn connect_escrows(
chain: EvmChain,
o: &EvmChainOptions,
rpc_url: &str,
account: &str,
) -> Result<(Arc<dyn EvmClient>, Option<Arc<dyn AdaptorEvmClient>>)> {
let config = evm_config(chain, o, rpc_url, account)?;
let scan = o.from_block.map(ScanConfig::from_block).unwrap_or_default();
let htlc_address = Address::from(parse_address(&o.htlc, &format!("{chain}.htlc"))?.0);
let htlc = EvmHtlcClient::connect(config.clone(), htlc_address, scan).await.map_err(evm_error)?;
htlc.verify_escrow_code().await.map_err(|e| code_error(chain, "htlc", e))?;
let htlc: Arc<dyn EvmClient> = Arc::new(HtlcEscrow::new(chain, htlc)?);
let adaptor = match &o.adaptor_escrow {
Some(escrow) => {
let address = Address::from(parse_address(escrow, &format!("{chain}.adaptorEscrow"))?.0);
let client = AdaptorEscrowClient::connect(config, address, scan).await.map_err(evm_error)?;
client.verify_escrow_code().await.map_err(|e| code_error(chain, "adaptorEscrow", e))?;
Some(Arc::new(AdaptorEscrow::new(chain, client)?) as Arc<dyn AdaptorEvmClient>)
}
None => None,
};
Ok((htlc, adaptor))
}
fn evm_error(e: paralock_evm::EvmError) -> Error {
Error::Evm(crate::evm_adapter::convert(e))
}
fn code_error(chain: EvmChain, what: &str, e: paralock_evm::EvmError) -> Error {
if matches!(e, paralock_evm::EvmError::CodeMismatch { .. }) {
return Error::invalid_request(format!(
"{chain}.{what}: the address doesn't hold the Paralock contract this SDK was built for ({e})"
));
}
evm_error(e)
}
pub async fn create_taker(options: CreateTakerOptions) -> Result<Taker> {
let network = options.zcash.network;
let key = key_pair(&options.zcash.key)?;
if let Some(address) = &options.return_address {
TransparentAddress::decode_for(address, network).map_err(|_| {
Error::invalid_request(format!("zcash.returnAddress must be a {network} transparent address"))
})?;
}
let node = zcash_node(&options.zcash.backend)?;
let maker: Arc<dyn MakerApi> = match &options.maker {
#[cfg(feature = "client")]
MakerEndpoint::Url(url) => Arc::new(crate::http::HttpMakerApi::new(url.clone(), network)?),
MakerEndpoint::Api(api) => api.clone(),
};
let mut config = TakerConfig::new(network, node, key, maker, options.price.clone());
config.return_address = options.return_address.clone();
config.store = store(&options.store, network, "taker")?;
config.limits = options.limits;
config.timeouts = options.timeouts;
for (chain, o) in &options.evm {
let tokens = if o.tokens.is_empty() {
known_network(o.evm.chain_id)
.map(|n| n.tokens.iter().map(|(_, a)| Hex::from_bytes(a.as_slice())).collect::<Vec<_>>())
.unwrap_or_default()
} else {
o.tokens.clone()
};
if tokens.is_empty() {
return Err(Error::invalid_request(format!(
"{chain}.tokens is required: no known stablecoins for chain id {}",
o.evm.chain_id
)));
}
let mut evm = o.evm.clone();
if evm.receipt_timeout.is_none() {
let window = o.claim_limits.unwrap_or(options.limits.claim).min_claim_window_seconds();
evm.receipt_timeout = Some(Duration::from_secs((window / 2).clamp(5, 60)));
}
let (htlc, adaptor) = connect_evm_chain(*chain, &evm, &evm.account).await?;
let mut taker_chain = TakerChain::new(htlc, tokens);
taker_chain.adaptor = adaptor;
taker_chain.token_decimals = o.token_decimals;
taker_chain.claim_limits = o.claim_limits;
if let Some(relayer) = &o.relayer {
let (htlc, adaptor) = connect_evm_chain(*chain, &evm, relayer).await?;
taker_chain.relayer = Some(RelayerClients { htlc, adaptor });
}
if let Some(url) = &evm.verify_rpc_url {
if url == &evm.rpc_url {
return Err(Error::invalid_request(format!(
"{chain}.verifyRpcUrl must be a different endpoint than {chain}.rpcUrl"
)));
}
let (htlc, adaptor) = connect_escrows(*chain, &evm, url, &evm.account).await?;
taker_chain.verifier = Some(VerifierClients { htlc, adaptor });
}
config.chains.insert(*chain, taker_chain);
}
config.allow_unknown_upgrades = options.allow_unknown_upgrades;
config.on_update = options.on_update.clone();
config.on_fresh_recipient = options.on_fresh_recipient.clone();
config.mine_zcash = options.mine_zcash.clone();
Taker::new(config)
}
pub async fn create_maker(options: CreateMakerOptions) -> Result<Maker> {
let network = options.zcash.network;
let key = key_pair(&options.zcash.key)?;
let node = zcash_node(&options.zcash.backend)?;
let payout = match &options.payout_address {
Some(address) => address.clone(),
None => TransparentAddress::p2pkh(&key.public_key(), network).encode(),
};
let mut chains = Vec::new();
for (chain, o) in &options.evm {
parse_address(&o.token, &format!("{chain}.token.address"))?;
if o.token_symbol.is_empty() {
return Err(Error::invalid_request(format!("{chain}.token.symbol is required")));
}
let (htlc, adaptor) = connect_evm_chain(*chain, &o.evm, &o.evm.account).await?;
chains.push(ChainClients {
chain: *chain,
chain_id: o.evm.chain_id,
token: o.token.clone(),
token_symbol: o.token_symbol.clone(),
token_decimals: o.token_decimals.unwrap_or(6),
htlc,
adaptor,
});
}
let mut config = MakerConfig::new(
network,
node,
key,
payout,
chains,
options.price.clone(),
store(&options.store, network, "maker")?,
);
config.spread_bps = options.spread_bps.unwrap_or(50.0);
config.policy = options.policy.unwrap_or(MAINNET_POLICY);
config.limits = options.limits.unwrap_or(paralock_core::protocol::DEFAULT_MAKER_LIMITS);
config.poll_interval = options.poll_interval;
config.after_claim = options.after_claim.clone();
config.allow_unknown_upgrades = options.allow_unknown_upgrades;
config.on_update = options.on_update.clone();
Maker::new(config)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
#[test]
fn options_never_print_keys() {
let key = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
let relayer = "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a";
let evm = EvmChainOptions::new(
"http://127.0.0.1:8545",
31337,
Hex::new(format!("0x{}", "11".repeat(20))).unwrap(),
key,
);
let mut chain = TakerEvmChainOptions::new(evm, vec![]);
chain.relayer = Some(relayer.to_owned().into());
let zcash = ZcashOptions::new(
Network::Regtest,
ZcashBackend::Rpc {
url: "http://u:p@127.0.0.1:1".into(),
auth: Some(("u".into(), Zeroizing::new("pw".into()))),
},
key,
);
let printed = format!("{chain:?} {zcash:?}");
assert!(
!printed.contains(&key[2..]) && !printed.contains(&relayer[2..]) && !printed.contains("pw"),
"{printed}"
);
}
}