1use std::collections::BTreeMap;
7
8use serde_json::{Map, Value};
9
10use super::error::{ParalockError, ParalockErrorCode};
11use super::messages::{AcceptRequest, AdaptorPreSignatureRequest, AdaptorProposal, AdaptorProposalAccepted};
12use super::pricing::{usd_units_to_zat, zat_to_usd_units};
13use super::types::{EvmChain, Hex, Quote, SwapDirection, SwapMode};
14use crate::keys::PublicKey;
15use crate::zcash::{Network, Txid, is_transparent_address};
16
17pub const MAX_ZAT: u64 = 21_000_000 * 100_000_000;
19const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
21
22fn bad(message: impl Into<String>) -> ParalockError {
23 ParalockError::new(ParalockErrorCode::InvalidRequest, message)
24}
25
26fn reject(message: impl Into<String>) -> ParalockError {
27 ParalockError::new(ParalockErrorCode::QuoteRejected, message)
28}
29
30pub fn is_hex(value: &str, bytes: Option<usize>) -> bool {
32 let Some(digits) = value.strip_prefix("0x") else {
33 return false;
34 };
35 if digits.len() % 2 != 0 || !digits.bytes().all(|b| b.is_ascii_hexdigit()) {
36 return false;
37 }
38 bytes.is_none_or(|n| digits.len() == n * 2)
39}
40
41pub fn is_evm_address(value: &str) -> bool {
45 is_hex(value, Some(20)) && value.bytes().skip(2).any(|b| b != b'0')
46}
47
48pub fn is_evm_chain(value: &str) -> bool {
50 EvmChain::parse(value).is_some()
51}
52
53pub fn is_swap_mode(value: &str) -> bool {
55 SwapMode::parse(value).is_some()
56}
57
58pub fn is_quote_id(value: &str) -> bool {
60 let b = value.as_bytes();
61 b.len() == 36
62 && b.iter().enumerate().all(|(i, c)| match i {
63 8 | 13 | 18 | 23 => *c == b'-',
64 _ => c.is_ascii_hexdigit(),
65 })
66}
67
68fn safe_integer(value: &Value) -> Option<i64> {
70 let Value::Number(n) = value else { return None };
71 if let Some(u) = n.as_u64() {
72 return (u <= MAX_SAFE_INTEGER).then_some(u as i64);
73 }
74 if let Some(i) = n.as_i64() {
75 return (i.unsigned_abs() <= MAX_SAFE_INTEGER).then_some(i);
76 }
77 let f = n.as_f64()?;
78 (f.is_finite() && f.fract() == 0.0 && f.abs() <= MAX_SAFE_INTEGER as f64).then_some(f as i64)
79}
80
81pub fn parse_amount(value: &Value, what: &str, min: Option<u128>, max: Option<u128>) -> Result<u128, ParalockError> {
84 let n: i128 = match value {
85 Value::String(s) if (1..=30).contains(&s.len()) && s.bytes().all(|b| b.is_ascii_digit()) => {
86 s.parse::<i128>().map_err(|_| bad(format!("{what} must be a non-negative integer")))?
87 }
88 Value::Number(_) => {
89 safe_integer(value).map(i128::from).ok_or_else(|| bad(format!("{what} must be a non-negative integer")))?
90 }
91 _ => return Err(bad(format!("{what} must be a non-negative integer"))),
92 };
93 let min = min.unwrap_or(1);
94 if n < 0 || (n as u128) < min {
95 return Err(bad(format!("{what} must be at least {min}")));
96 }
97 if let Some(max) = max {
98 if n as u128 > max {
99 return Err(bad(format!("{what} must be at most {max}")));
100 }
101 }
102 Ok(n as u128)
103}
104
105pub fn parse_integer(value: &Value, what: &str, min: Option<u64>, max: Option<u64>) -> Result<u64, ParalockError> {
108 let n = safe_integer(value).ok_or_else(|| bad(format!("{what} must be an integer")))?;
109 let (min, max) = (min.unwrap_or(0), max.unwrap_or(MAX_SAFE_INTEGER));
110 if n < 0 || (n as u64) < min || (n as u64) > max {
111 return Err(bad(format!("{what} is out of range")));
112 }
113 Ok(n as u64)
114}
115
116fn parse_u32(value: &Value, what: &str, min: Option<u64>, max: u64) -> Result<u32, ParalockError> {
117 let n = parse_integer(value, what, min, Some(max))?;
118 u32::try_from(n).map_err(|_| bad(format!("{what} is out of range")))
119}
120
121fn object<'a>(value: &'a Value, what: &str) -> Result<&'a Map<String, Value>, ParalockError> {
122 value.as_object().ok_or_else(|| bad(format!("{what} must be an object")))
123}
124
125fn str_field<'a>(object: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
127 object.get(key).and_then(Value::as_str)
128}
129
130fn field<'a>(object: &'a Map<String, Value>, key: &str) -> &'a Value {
131 object.get(key).unwrap_or(&Value::Null)
132}
133
134fn string(object: &Map<String, Value>, key: &str, what: &str, max: usize) -> Result<String, ParalockError> {
136 match str_field(object, key) {
137 Some(s) if !s.is_empty() && s.encode_utf16().count() <= max => Ok(s.to_owned()),
138 _ => Err(bad(format!("{what} must be a string"))),
139 }
140}
141
142fn hex_field(object: &Map<String, Value>, key: &str) -> Option<Hex> {
143 str_field(object, key).and_then(|s| Hex::new(s).ok())
144}
145
146fn quote_id(object: &Map<String, Value>, key: &str, message: &str) -> Result<String, ParalockError> {
147 match str_field(object, key) {
148 Some(s) if is_quote_id(s) => Ok(s.to_owned()),
149 _ => Err(bad(message)),
150 }
151}
152
153fn txid(object: &Map<String, Value>, key: &str) -> Result<String, ParalockError> {
154 match str_field(object, key) {
155 Some(s) if Txid::is_txid(s) => Ok(s.to_ascii_lowercase()),
156 _ => Err(bad(format!("{key} must be a 64-character txid"))),
157 }
158}
159
160fn evm_address(object: &Map<String, Value>, key: &str) -> Result<Hex, ParalockError> {
161 match str_field(object, key) {
162 Some(s) if is_evm_address(s) => Hex::new(s).map_err(|_| bad(format!("{key} must be a non-zero 0x address"))),
163 _ => Err(bad(format!("{key} must be a non-zero 0x address"))),
164 }
165}
166
167fn pubkey_as_is(object: &Map<String, Value>, key: &str) -> Result<Hex, ParalockError> {
169 let invalid = || bad(format!("{key} must be a compressed secp256k1 public key"));
170 let s = str_field(object, key).ok_or_else(invalid)?;
171 if !is_hex(s, Some(33)) {
172 return Err(invalid());
173 }
174 let hex = Hex::new(s).map_err(|_| invalid())?;
175 if !PublicKey::is_valid_compressed(&hex.to_bytes()) {
176 return Err(invalid());
177 }
178 Ok(hex)
179}
180
181fn pubkey(object: &Map<String, Value>, key: &str) -> Result<Hex, ParalockError> {
183 let invalid = || bad(format!("{key} must be a compressed secp256k1 public key"));
184 let s = str_field(object, key).ok_or_else(invalid)?;
185 if !is_hex(s, Some(33)) {
186 return Err(invalid());
187 }
188 let hex = Hex::new(s.to_ascii_lowercase()).map_err(|_| invalid())?;
189 if !PublicKey::is_valid_compressed(&hex.to_bytes()) {
190 return Err(invalid());
191 }
192 Ok(hex)
193}
194
195fn optional_funding_expiry(object: &Map<String, Value>) -> Result<Option<u32>, ParalockError> {
198 match object.get("fundingExpiryHeight") {
199 None => Ok(None),
200 Some(value) => Ok(Some(parse_u32(value, "fundingExpiryHeight", Some(1), 499_999_999)?)),
201 }
202}
203
204pub fn parse_accept_request(body: &Value) -> Result<AcceptRequest, ParalockError> {
209 let b = object(body, "request")?;
210 let quote_id = quote_id(b, "quoteId", "quoteId must be a quote id")?;
211 let redeem_script = match str_field(b, "redeemScript") {
212 Some(s) if is_hex(s, None) && s.len() <= 2 + 2 * 520 => {
213 Hex::new(s).map_err(|_| bad("redeemScript must be hex"))?
214 }
215 _ => return Err(bad("redeemScript must be hex")),
216 };
217 let funding_txid = txid(b, "fundingTxid")?;
218 let taker_evm_address = evm_address(b, "takerEvmAddress")?;
219 let funding_vout = parse_u32(field(b, "fundingVout"), "fundingVout", None, 10_000)?;
220 let funding_expiry_height = optional_funding_expiry(b)?;
221 Ok(AcceptRequest { quote_id, redeem_script, funding_txid, funding_vout, taker_evm_address, funding_expiry_height })
222}
223
224pub fn parse_adaptor_proposal(body: &Value, network: Network) -> Result<AdaptorProposal, ParalockError> {
226 adaptor_proposal(body, Some(network))
227}
228
229pub(crate) fn adaptor_proposal(body: &Value, network: Option<Network>) -> Result<AdaptorProposal, ParalockError> {
232 let b = object(body, "proposal")?;
233 let quote_id = quote_id(b, "quoteId", "quoteId must be a quote id")?;
234 let taker_zcash_pubkey = pubkey(b, "takerZcashPubkey")?;
235 let adaptor_point = pubkey(b, "adaptorPoint")?;
236 let funding_txid = txid(b, "fundingTxid")?;
237 let refund_address = transparent_address(b, "refundAddress", network)?;
238 let taker_evm_recipient = evm_address(b, "takerEvmRecipient")?;
239 let funding_vout = parse_u32(field(b, "fundingVout"), "fundingVout", None, 10_000)?;
240 let amount = parse_amount(field(b, "amountZat"), "amountZat", None, Some(u128::from(MAX_ZAT)))?;
241 let refund_height = parse_u32(field(b, "refundHeight"), "refundHeight", Some(1), 499_999_999)?;
242 let funding_expiry_height = optional_funding_expiry(b)?;
243 Ok(AdaptorProposal {
244 quote_id,
245 taker_zcash_pubkey,
246 adaptor_point,
247 funding_txid,
248 funding_vout,
249 amount_zat: u64::try_from(amount).map_err(|_| bad("amountZat is out of range"))?,
250 refund_height,
251 refund_address,
252 taker_evm_recipient,
253 funding_expiry_height,
254 })
255}
256
257fn transparent_address(
258 object: &Map<String, Value>,
259 key: &str,
260 network: Option<Network>,
261) -> Result<String, ParalockError> {
262 match str_field(object, key) {
263 Some(s) if is_transparent_address(s, network) => Ok(s.to_owned()),
264 _ => Err(bad(match network {
265 Some(network) => format!("{key} must be a {network} transparent address"),
266 None => format!("{key} must be a transparent address"),
267 })),
268 }
269}
270
271pub fn parse_adaptor_proposal_accepted(
277 body: &Value,
278 network: Network,
279) -> Result<AdaptorProposalAccepted, ParalockError> {
280 adaptor_proposal_accepted(body, Some(network))
281}
282
283pub(crate) fn adaptor_proposal_accepted(
284 body: &Value,
285 network: Option<Network>,
286) -> Result<AdaptorProposalAccepted, ParalockError> {
287 let b = object(body, "answer")?;
288 let swap_id = quote_id(b, "swapId", "swapId must be a quote id")?;
289 let maker_refund_signature = match hex_field(b, "makerRefundSignature") {
290 Some(sig) if (9..=73).contains(&sig.byte_len()) => sig,
291 _ => return Err(bad("makerRefundSignature must be a DER signature followed by its sighash byte")),
292 };
293 let claim_to = transparent_address(b, "claimTo", network)?;
294 let branch_id = parse_u32(field(b, "branchId"), "branchId", Some(1), u64::from(u32::MAX))?;
295 let signer = evm_address(b, "signer")?;
296 let signer_pubkey = pubkey_as_is(b, "signerPubkey")?;
297 Ok(AdaptorProposalAccepted { swap_id, maker_refund_signature, claim_to, branch_id, signer, signer_pubkey })
298}
299
300pub fn parse_pre_signature_request(body: &Value) -> Result<AdaptorPreSignatureRequest, ParalockError> {
304 let b = object(body, "request")?;
305 let invalid = || bad("takerPreSignature must be a 162-byte hex pre-signature");
306 let pre = str_field(b, "takerPreSignature").ok_or_else(invalid)?;
307 let digits = pre.strip_prefix("0x").unwrap_or(pre);
308 if digits.len() != 324 || !digits.bytes().all(|c| c.is_ascii_hexdigit()) {
309 return Err(invalid());
310 }
311 Ok(AdaptorPreSignatureRequest { taker_pre_signature: digits.to_ascii_lowercase() })
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum QuoteAmount {
317 ExactIn(u64),
319 ExactOut(u128),
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326pub struct QuoteRequest {
327 pub amount: QuoteAmount,
329 pub chain: EvmChain,
331 pub mode: SwapMode,
333}
334
335pub fn parse_quote_request(
339 zat: Option<&str>,
340 usd: Option<&str>,
341 chain: Option<&str>,
342 mode: Option<&str>,
343) -> Result<QuoteRequest, ParalockError> {
344 let chain = EvmChain::parse(chain.unwrap_or("tempo")).ok_or_else(|| bad("unknown chain"))?;
345 let mode = SwapMode::parse(mode.unwrap_or("htlc")).ok_or_else(|| bad("mode must be htlc or adaptor"))?;
346 let raw = usd.or(zat).unwrap_or("");
347 if raw.is_empty() || raw.len() > 30 || !raw.bytes().all(|b| b.is_ascii_digit()) {
348 return Err(bad("amount must be a positive integer"));
349 }
350 let value = Value::String(raw.to_owned());
351 let amount = if usd.is_some() {
352 QuoteAmount::ExactOut(parse_amount(&value, "amount", None, Some(10u128.pow(30)))?)
353 } else {
354 let zat = parse_amount(&value, "amount", None, Some(u128::from(MAX_ZAT)))?;
355 QuoteAmount::ExactIn(u64::try_from(zat).map_err(|_| bad("amount is out of range"))?)
356 };
357 Ok(QuoteRequest { amount, chain, mode })
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub struct MakerLimits {
363 pub min_zec_amount_zat: u64,
365 pub max_zec_amount_zat: u64,
367 pub max_concurrent_swaps: u32,
369 pub max_open_quotes: u32,
371 pub quote_ttl_seconds: u64,
373 pub max_funding_expiry_blocks: u32,
375 pub min_evm_lock_seconds_left: u64,
378 pub zcash_fee_zat: u64,
380 pub check_inventory: bool,
382}
383
384pub const DEFAULT_MAKER_LIMITS: MakerLimits = MakerLimits {
386 min_zec_amount_zat: 1_000_000,
387 max_zec_amount_zat: 1_000_000_000,
388 max_concurrent_swaps: 10,
389 max_open_quotes: 1_000,
390 quote_ttl_seconds: 60,
391 max_funding_expiry_blocks: super::policy::MAX_FUNDING_EXPIRY_BLOCKS,
392 min_evm_lock_seconds_left: 30,
393 zcash_fee_zat: 10_000,
394 check_inventory: true,
395};
396
397impl Default for MakerLimits {
398 fn default() -> Self {
399 DEFAULT_MAKER_LIMITS
400 }
401}
402
403pub fn assert_quote_amount(zat: u64, limits: &MakerLimits) -> Result<(), ParalockError> {
405 if zat < limits.min_zec_amount_zat {
406 return Err(ParalockError::new(
407 ParalockErrorCode::LimitExceeded,
408 format!("minimum swap is {} zat", limits.min_zec_amount_zat),
409 ));
410 }
411 if zat > limits.max_zec_amount_zat {
412 return Err(ParalockError::new(
413 ParalockErrorCode::LimitExceeded,
414 format!("maximum swap is {} zat", limits.max_zec_amount_zat),
415 ));
416 }
417 Ok(())
418}
419
420pub fn parse_quote(body: &Value) -> Result<Quote, ParalockError> {
426 let q = object(body, "quote")?;
427 let id = quote_id(q, "id", "quote.id must be a quote id")?;
428 if str_field(q, "direction") != Some("ZEC_TO_USD") {
429 return Err(bad("quote.direction must be ZEC_TO_USD"));
430 }
431 let mode =
432 str_field(q, "mode").and_then(SwapMode::parse).ok_or_else(|| bad("quote.mode must be htlc or adaptor"))?;
433 let chain =
434 str_field(q, "chain").and_then(EvmChain::parse).ok_or_else(|| bad("quote.chain is not a known chain"))?;
435 let token =
436 hex_field(q, "token").filter(|h| h.byte_len() == 20).ok_or_else(|| bad("quote.token must be an address"))?;
437 let maker_zcash_pubkey = hex_field(q, "makerZcashPubkey")
438 .filter(|h| h.byte_len() == 33)
439 .ok_or_else(|| bad("quote.makerZcashPubkey must be 33 bytes of hex"))?;
440 let maker_evm_address = hex_field(q, "makerEvmAddress")
441 .filter(|h| h.byte_len() == 20)
442 .ok_or_else(|| bad("quote.makerEvmAddress must be an address"))?;
443 let rate = match q.get("rate").and_then(Value::as_f64) {
444 Some(r) if r.is_finite() && r > 0.0 => r,
445 _ => return Err(bad("quote.rate must be a positive number")),
446 };
447 let zec_amount_zat =
448 parse_amount(field(q, "zecAmountZat"), "quote.zecAmountZat", None, Some(u128::from(MAX_ZAT)))? as u64;
449 let usd_amount = parse_amount(field(q, "usdAmount"), "quote.usdAmount", None, Some(10u128.pow(30)))?;
450 let token_symbol = string(q, "tokenSymbol", "quote.tokenSymbol", 32)?;
451 let zcash_lock_blocks = parse_u32(field(q, "zcashLockBlocks"), "quote.zcashLockBlocks", Some(1), 100_000)?;
452 let evm_lock_seconds = parse_u32(field(q, "evmLockSeconds"), "quote.evmLockSeconds", Some(1), 10_000_000)?;
453 let expires_at = parse_integer(field(q, "expiresAt"), "quote.expiresAt", None, None)?;
454 let chain_id = match q.get("chainId") {
455 None => None,
456 Some(v) => Some(parse_integer(v, "quote.chainId", Some(1), None)?),
457 };
458 let exact_out = q.get("exactOut") == Some(&Value::Bool(true));
459 Ok(Quote {
460 id,
461 direction: SwapDirection::ZecToUsd,
462 mode,
463 chain,
464 chain_id,
465 zec_amount_zat,
466 usd_amount,
467 token,
468 token_symbol,
469 rate,
470 maker_zcash_pubkey,
471 maker_evm_address,
472 zcash_lock_blocks,
473 evm_lock_seconds,
474 expires_at,
475 exact_out,
476 })
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub struct QuoteLimits {
482 pub max_zcash_lock_blocks: u32,
484 pub min_zcash_lock_blocks: u32,
486 pub min_evm_lock_seconds: u32,
488 pub max_evm_lock_seconds: u32,
490 pub max_zec_amount_zat: u64,
492 pub expiry_margin_seconds: u64,
494 pub max_slippage_bps: u32,
496}
497
498pub const DEFAULT_QUOTE_LIMITS: QuoteLimits = QuoteLimits {
500 max_zcash_lock_blocks: 288,
501 min_zcash_lock_blocks: 10,
502 min_evm_lock_seconds: 30,
503 max_evm_lock_seconds: 86_400,
504 max_zec_amount_zat: 1_000 * 100_000_000,
505 expiry_margin_seconds: 5,
506 max_slippage_bps: 200,
507};
508
509impl Default for QuoteLimits {
510 fn default() -> Self {
511 DEFAULT_QUOTE_LIMITS
512 }
513}
514
515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
519pub struct ClaimLimits {
520 min_claim_window_seconds: u64,
521 min_evm_confirmations: u64,
522}
523
524pub const MIN_CLAIM_WINDOW_SECONDS: u64 = 30;
527pub const MIN_EVM_CONFIRMATIONS: u64 = 1;
530
531pub const DEFAULT_CLAIM_LIMITS: ClaimLimits =
533 ClaimLimits { min_claim_window_seconds: MIN_CLAIM_WINDOW_SECONDS, min_evm_confirmations: MIN_EVM_CONFIRMATIONS };
534
535impl ClaimLimits {
536 pub fn new(min_claim_window_seconds: u64, min_evm_confirmations: u64) -> Result<Self, ParalockError> {
539 if min_claim_window_seconds < MIN_CLAIM_WINDOW_SECONDS {
540 return Err(bad(format!(
541 "minClaimWindowSeconds of {min_claim_window_seconds}s is below the {MIN_CLAIM_WINDOW_SECONDS}s floor"
542 )));
543 }
544 if min_evm_confirmations < MIN_EVM_CONFIRMATIONS {
545 return Err(bad(format!(
546 "minEvmConfirmations of {min_evm_confirmations} is below the floor of {MIN_EVM_CONFIRMATIONS}"
547 )));
548 }
549 Ok(Self { min_claim_window_seconds, min_evm_confirmations })
550 }
551
552 pub const fn new_below_floor(min_claim_window_seconds: u64, min_evm_confirmations: u64) -> Self {
556 Self { min_claim_window_seconds, min_evm_confirmations }
557 }
558
559 pub const fn min_claim_window_seconds(&self) -> u64 {
561 self.min_claim_window_seconds
562 }
563
564 pub const fn min_evm_confirmations(&self) -> u64 {
566 self.min_evm_confirmations
567 }
568}
569
570impl Default for ClaimLimits {
571 fn default() -> Self {
572 DEFAULT_CLAIM_LIMITS
573 }
574}
575
576#[derive(Debug, Clone, PartialEq, Eq)]
578pub struct ChainConfig {
579 pub chain_id: u64,
581 pub tokens: Vec<String>,
583 pub adaptor: bool,
585 pub decimals: Option<u32>,
587}
588
589#[derive(Debug, Clone, Copy, PartialEq)]
591pub enum PriceCheck {
592 Reference(f64),
596 ReferenceWithFee {
601 price: f64,
603 fixed_fee_zat: u64,
605 },
606 Disabled,
610}
611
612#[derive(Debug, Clone, PartialEq)]
614pub struct QuoteContext {
615 pub limits: QuoteLimits,
617 pub chains: BTreeMap<EvmChain, ChainConfig>,
619 pub price: PriceCheck,
621 pub now: u64,
623}
624
625pub fn validate_quote(quote: &Quote, request: &QuoteRequest, ctx: &QuoteContext) -> Result<(), ParalockError> {
632 let same_amount = match request.amount {
633 QuoteAmount::ExactIn(zat) => !quote.exact_out && quote.zec_amount_zat == zat,
634 QuoteAmount::ExactOut(usd) => quote.exact_out && quote.usd_amount == usd,
635 };
636 if !same_amount || quote.chain != request.chain || quote.mode != request.mode {
637 return Err(reject("the maker quoted different terms than requested"));
638 }
639 let limits = &ctx.limits;
640 let chain = ctx
641 .chains
642 .get("e.chain)
643 .ok_or_else(|| reject(format!("you have no {} account configured", quote.chain)))?;
644 if let Some(id) = quote.chain_id {
645 if id != chain.chain_id {
646 return Err(reject(format!(
647 "quote is for chain id {id}, your {} client is on {}",
648 quote.chain, chain.chain_id
649 )));
650 }
651 }
652 if quote.mode == SwapMode::Adaptor && !chain.adaptor {
653 return Err(reject(format!("private mode is not configured on {}", quote.chain)));
654 }
655 let token = quote.token.as_str().to_ascii_lowercase();
656 if !chain.tokens.iter().any(|t| t.to_ascii_lowercase() == token) {
657 return Err(reject(format!(
658 "quote pays {} at {}, which is not an accepted stablecoin on {}",
659 quote.token_symbol, quote.token, quote.chain
660 )));
661 }
662 if !is_hex(quote.maker_zcash_pubkey.as_str(), Some(33))
663 || !PublicKey::is_valid_compressed("e.maker_zcash_pubkey.to_bytes())
664 {
665 return Err(reject("maker's Zcash public key is not a valid curve point"));
666 }
667 if !is_evm_address(quote.maker_evm_address.as_str()) {
668 return Err(reject("maker's EVM address is invalid"));
669 }
670 if quote.zec_amount_zat == 0 || quote.usd_amount == 0 {
671 return Err(reject("quote amounts must be positive"));
672 }
673 if quote.zec_amount_zat > limits.max_zec_amount_zat {
674 return Err(reject(format!("quote exceeds your maximum of {} zat", limits.max_zec_amount_zat)));
675 }
676 if quote.zcash_lock_blocks < limits.min_zcash_lock_blocks || quote.zcash_lock_blocks > limits.max_zcash_lock_blocks
677 {
678 return Err(reject(format!(
679 "Zcash lock of {} blocks is outside {}\u{2013}{}",
680 quote.zcash_lock_blocks, limits.min_zcash_lock_blocks, limits.max_zcash_lock_blocks
681 )));
682 }
683 if quote.evm_lock_seconds < limits.min_evm_lock_seconds || quote.evm_lock_seconds > limits.max_evm_lock_seconds {
684 return Err(reject(format!(
685 "EVM lock of {}s is outside {}\u{2013}{}s",
686 quote.evm_lock_seconds, limits.min_evm_lock_seconds, limits.max_evm_lock_seconds
687 )));
688 }
689 if u128::from(quote.expires_at) < u128::from(ctx.now) + u128::from(limits.expiry_margin_seconds) {
690 return Err(reject("quote has expired"));
691 }
692 let (price, fee) = match ctx.price {
693 PriceCheck::Reference(price) => (Some(price), 0),
694 PriceCheck::ReferenceWithFee { price, fixed_fee_zat } => (Some(price), fixed_fee_zat),
695 PriceCheck::Disabled => (None, 0),
696 };
697 if let Some(price) = price {
698 let decimals = chain.decimals.unwrap_or(6);
699 let slip = u128::from(limits.max_slippage_bps);
700 if quote.exact_out {
701 let fair = usd_units_to_zat(quote.usd_amount, price, decimals)?.saturating_add(u128::from(fee));
702 if u128::from(quote.zec_amount_zat) * 10_000 > fair.saturating_mul(10_000 + slip) {
704 return Err(reject(format!("quote costs more than {} bps above your price", limits.max_slippage_bps)));
705 }
706 } else if slip < 10_000 {
707 let fair = zat_to_usd_units(quote.zec_amount_zat.saturating_sub(fee), price, decimals)?;
708 if quote.usd_amount.saturating_mul(10_000) < fair.saturating_mul(10_000 - slip) {
709 return Err(reject(format!("quote pays more than {} bps below your price", limits.max_slippage_bps)));
710 }
711 } else {
712 zat_to_usd_units(quote.zec_amount_zat, price, decimals)?;
714 }
715 }
716 Ok(())
717}
718
719impl TryFrom<Value> for Quote {
723 type Error = ParalockError;
724
725 fn try_from(value: Value) -> Result<Self, Self::Error> {
726 parse_quote(&value)
727 }
728}
729
730impl TryFrom<Value> for AcceptRequest {
731 type Error = ParalockError;
732
733 fn try_from(value: Value) -> Result<Self, Self::Error> {
734 parse_accept_request(&value)
735 }
736}
737
738impl TryFrom<Value> for AdaptorProposal {
739 type Error = ParalockError;
740
741 fn try_from(value: Value) -> Result<Self, Self::Error> {
745 adaptor_proposal(&value, None)
746 }
747}
748
749impl TryFrom<Value> for AdaptorProposalAccepted {
750 type Error = ParalockError;
751
752 fn try_from(value: Value) -> Result<Self, Self::Error> {
754 adaptor_proposal_accepted(&value, None)
755 }
756}