1use std::io::{IsTerminal as _, Write as _};
4use std::path::Path;
5use std::sync::Arc;
6use std::time::Duration;
7
8use offline::OfflineEvm;
9use paralock::evm::{RelayerClients, VerifierClients};
10use paralock::http::{HttpMakerApi, HttpMakerApiOptions};
11use paralock::taker::FreshRecipient;
12use paralock::{
13 EvmChain, FileStore, Hex, Network, PrivacyOptions, Quote, ResumeOptions, Swap, SwapMode, SwapOptions, SwapStatus,
14 Taker, TakerConfig, TakerPriceCheck, TakerSwapRecord, ZcashNode,
15};
16use paralock_core::keys::{KeyPair, PrivateKey};
17use paralock_core::protocol::is_standard_amount;
18use paralock_core::zcash::TransparentAddress;
19use serde_json::{Value, json};
20
21use super::{connect, pick_chain, price_source, zcash_node};
22use crate::amount::{self, parse_usd, parse_zec, usd, zec};
23use crate::cli::{PayArgs, QuoteArgs, RefundArgs, ResumeArgs, SwapArgs, SwapCommon, SwapsArgs};
24use crate::config::Setup;
25use crate::devnet::{Faucet, Miner};
26use crate::error::{CliError, CliResult, Exit};
27use crate::output::{Output, status_str, warn};
28
29pub struct TakerHandle {
31 pub taker: Taker,
33 pub node: Arc<dyn ZcashNode>,
35 pub address: String,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum Connect {
42 Online,
44 ZcashOnly,
46}
47
48pub async fn build(
50 setup: &Setup,
51 out: Output,
52 chains: &[EvmChain],
53 relay: bool,
54 connect_mode: Connect,
55) -> CliResult<TakerHandle> {
56 let network = setup.network;
57 let key = setup.zcash_key.load()?;
58 let key = KeyPair::from_private(
59 PrivateKey::from_hex(&key).map_err(|_| CliError::usage("the Zcash key is not a private key"))?,
60 );
61 let address = TransparentAddress::p2pkh(&key.public_key(), network).encode();
62 let node = zcash_node(setup)?;
63 let maker_url = setup.maker_url.clone().ok_or_else(|| {
64 CliError::usage("no maker to trade with").hint("set maker_url in paralock.toml or pass --maker-url <url>")
65 })?;
66 let maker = HttpMakerApi::with_options(maker_url, network, HttpMakerApiOptions::default())?;
67 let price = TakerPriceCheck::Source(price_source(setup)?);
68 let mut cfg = TakerConfig::new(network, node.clone(), key, Arc::new(maker), price);
69 cfg.return_address = setup.return_address.clone();
70 cfg.store = Arc::new(FileStore::open(setup.taker_store())?);
71 cfg.limits = setup.taker_limits;
72 cfg.timeouts = setup.taker_timeouts;
73 cfg.on_update = Some(out.progress(false));
74 let recipients = setup.store_dir.join("recipients.jsonl");
75 cfg.on_fresh_recipient =
76 Some(Arc::new(move |fresh: &FreshRecipient, swap: &Swap| match save_recipient(&recipients, fresh, swap) {
77 Ok(()) => out.say(format!(
78 " fresh recipient {} (its key is saved in the swap store and {})",
79 fresh.address,
80 recipients.display()
81 )),
82 Err(e) => warn(&format!("could not save the fresh recipient's key to {}: {e}", recipients.display())),
83 }));
84 if let Some(devnet) = &setup.devnet {
85 let miner = Arc::new(Miner::new(&devnet.zcash.rpc)?);
86 cfg.mine_zcash = Some(Arc::new(move |_blocks| {
87 let miner = Arc::clone(&miner);
88 Box::pin(async move {
89 miner.tick().await;
90 })
91 }));
92 }
93 for chain in chains {
94 let c = setup.chain(*chain)?;
95 if relay && c.relayer.is_none() {
96 return Err(CliError::usage(format!("--relay needs a relayer on {chain}"))
97 .hint(format!("set relayer_key_env or relayer_key_file in [evm.{chain}]")));
98 }
99 let tokens = if c.tokens.is_empty() {
100 paralock_evm::known_network(c.chain_id)
101 .map(|n| n.tokens.iter().map(|(_, a)| Hex::from_bytes(a.as_slice())).collect::<Vec<_>>())
102 .unwrap_or_default()
103 } else {
104 c.tokens.clone()
105 };
106 if tokens.is_empty() {
107 return Err(CliError::usage(format!(
108 "[evm.{chain}]: list the stablecoins you accept in tokens (none are known for chain id {})",
109 c.chain_id
110 )));
111 }
112 let window = setup.taker_limits.claim.min_claim_window_seconds();
114 let receipt_timeout = Some(Duration::from_secs((window / 2).clamp(5, 60)));
115 let connected = match connect(c, &c.key, None, receipt_timeout).await {
116 Ok(connected) => connected,
117 Err(e) if connect_mode == Connect::ZcashOnly => {
118 warn(&format!("{}; refunding with Zcash only", e.message));
119 let account = Hex::from_bytes(
120 ¶lock_core::evm::EvmAddress::from_public_key(
121 &PrivateKey::from_hex(&c.key.load()?).map_err(|_| CliError::usage("bad EVM key"))?.public_key(),
122 )
123 .0,
124 );
125 (Arc::new(OfflineEvm::new(c, account)) as Arc<dyn paralock::EvmClient>, None)
126 }
127 Err(e) => return Err(e),
128 };
129 let mut taker_chain = paralock::TakerChain::new(connected.0, tokens);
130 taker_chain.adaptor = connected.1;
131 taker_chain.token_decimals = Some(c.token.as_ref().map_or(6, |t| t.decimals));
132 if let (Some(relayer), Connect::Online) = (&c.relayer, connect_mode) {
133 match connect(c, relayer, None, receipt_timeout).await {
135 Ok((htlc, adaptor)) => taker_chain.relayer = Some(RelayerClients { htlc, adaptor }),
136 Err(e) if relay => return Err(e.context("the relayer")),
137 Err(e) => {
138 warn(&format!("the {chain} relayer is unavailable ({}); claims use your own account", e.message))
139 }
140 }
141 }
142 if let (Some(url), Connect::Online) = (&c.verify_rpc, connect_mode) {
143 let (htlc, adaptor) = connect(c, &c.key, Some(url), receipt_timeout).await?;
145 taker_chain.verifier = Some(VerifierClients { htlc, adaptor });
146 }
147 cfg.chains.insert(*chain, taker_chain);
148 }
149 let taker = Taker::new(cfg)?;
150 Ok(TakerHandle { taker, node, address })
151}
152
153fn save_recipient(path: &Path, fresh: &FreshRecipient, swap: &Swap) -> std::io::Result<()> {
155 let mut options = std::fs::OpenOptions::new();
156 options.append(true).create(true);
157 #[cfg(unix)]
158 {
159 use std::os::unix::fs::OpenOptionsExt;
160 options.mode(0o600);
161 }
162 let line = json!({
163 "address": fresh.address,
164 "privateKey": fresh.private_key.expose(),
165 "swapId": swap.id,
166 "chain": swap.evm.chain,
167 });
168 let mut file = options.open(path)?;
169 writeln!(file, "{line}")?;
170 file.sync_all()
171}
172
173fn mode_of(private: bool) -> SwapMode {
174 if private { SwapMode::Adaptor } else { SwapMode::Htlc }
175}
176
177fn decimals(setup: &Setup, chain: EvmChain) -> u32 {
178 setup.chains.get(&chain).and_then(|c| c.token.as_ref()).map_or(6, |t| t.decimals)
179}
180
181fn mode_line(mode: SwapMode) -> &'static str {
182 match mode {
183 SwapMode::Adaptor => "private (adaptor signatures: no hash on either chain)",
184 SwapMode::Htlc => "htlc (a shared sha256 hashlock on both chains)",
185 }
186}
187
188fn print_quote(out: Output, setup: &Setup, quote: &Quote, pay: Option<(&str, Option<&str>)>) {
189 let d = decimals(setup, quote.chain);
190 let dollars = usd(quote.usd_amount, d);
191 match pay {
192 Some((to, label)) => {
193 let label = label.map(|l| format!(" ({l})")).unwrap_or_default();
194 out.say(format!("Pay {dollars} {} on {} to {to}{label}", quote.token_symbol, quote.chain));
195 out.say(format!(" costs {} ZEC @ ${:.2}/ZEC", zec(quote.zec_amount_zat), quote.rate));
196 }
197 None => out.say(format!(
198 "Quote: {} ZEC → {dollars} {} on {} @ ${:.2}/ZEC",
199 zec(quote.zec_amount_zat),
200 quote.token_symbol,
201 quote.chain,
202 quote.rate
203 )),
204 }
205 out.say(format!(" Mode {}", mode_line(quote.mode)));
206 out.say(format!(
207 " Zcash refund window {} blocks · {} lock {}s · quote {}",
208 quote.zcash_lock_blocks, quote.chain, quote.evm_lock_seconds, quote.id
209 ));
210}
211
212fn quote_json(quote: &Quote, decimals: u32) -> Value {
213 json!({
214 "quote": quote,
215 "zec": zec(quote.zec_amount_zat),
216 "usd": amount::exact(quote.usd_amount, decimals),
217 })
218}
219
220pub async fn quote(setup: &Setup, out: Output, args: QuoteArgs) -> CliResult<Exit> {
222 let chain = pick_chain(setup, args.chain)?;
223 let h = build(setup, out, &[chain], false, Connect::Online).await?;
224 let mode = mode_of(args.private);
225 let quote = match (&args.zec, &args.usd) {
226 (Some(z), _) => h.taker.quote(parse_zec(z)?, chain, mode).await?,
227 (None, Some(u)) => h.taker.quote_exact_out(parse_usd(u, decimals(setup, chain))?, chain, mode).await?,
228 (None, None) => return Err(CliError::usage("give --zec or --usd")),
229 };
230 print_quote(out, setup, "e, None);
231 out.say(" The quote passed every check (keys, chain id, token, windows, price). Nothing was locked.");
232 out.result(quote_json("e, decimals(setup, chain)));
233 Ok(Exit::Ok)
234}
235
236pub async fn swap(setup: &Setup, out: Output, args: SwapArgs) -> CliResult<Exit> {
238 let zat = parse_zec(&args.zec)?;
239 if args.standard_only && !is_standard_amount(zat) {
240 return Err(CliError::usage("--standard-only allows 0.1, 0.5, 1 or 5 ZEC"));
241 }
242 let chain = pick_chain(setup, args.common.chain)?;
243 let h = build(setup, out, &[chain], args.common.relay, Connect::Online).await?;
244 let quote = h.taker.quote(zat, chain, mode_of(args.common.private)).await?;
245 print_quote(out, setup, "e, None);
246 let privacy = PrivacyOptions {
247 fresh_recipient: args.fresh_recipient.then_some(true),
248 relay_claim: args.common.relay.then_some(true),
249 standard_amounts_only: args.standard_only.then_some(true),
250 jitter_ms: args.jitter,
251 };
252 let options = SwapOptions { no_claim: false, privacy, recipient: None, label: None };
253 run(setup, out, &h, "e, options, &args.common, args.refund_demo).await
254}
255
256pub async fn pay(setup: &Setup, out: Output, args: PayArgs) -> CliResult<Exit> {
258 let to = paralock_core::evm::EvmAddress::parse(args.to.trim()).map_err(|_| {
259 CliError::usage(format!("--to must be an EVM address (0x + 40 hex digits), got \"{}\"", args.to))
260 })?;
261 let to = Hex::from_bytes(&to.0);
262 let chain = pick_chain(setup, args.common.chain)?;
263 let units = parse_usd(&args.usd, decimals(setup, chain))?;
264 let h = build(setup, out, &[chain], args.common.relay, Connect::Online).await?;
265 let quote = h.taker.quote_exact_out(units, chain, mode_of(args.common.private)).await?;
266 print_quote(out, setup, "e, Some((to.as_str(), args.label.as_deref())));
267 let privacy = PrivacyOptions { relay_claim: args.common.relay.then_some(true), ..PrivacyOptions::default() };
268 let options = SwapOptions { no_claim: false, privacy, recipient: Some(to), label: args.label.clone() };
269 run(setup, out, &h, "e, options, &args.common, false).await
270}
271
272async fn confirm(setup: &Setup, out: Output, yes: bool) -> CliResult<()> {
274 if setup.network != Network::Mainnet || yes {
275 return Ok(());
276 }
277 if out.json || !std::io::stdin().is_terminal() {
278 return Err(
279 CliError::usage("a mainnet swap needs confirmation").hint("add --yes to confirm it non-interactively")
280 );
281 }
282 out.say("This locks real ZEC on mainnet. Paralock is experimental and not audited.");
283 let answer = tokio::task::spawn_blocking(|| {
284 let mut stdout = std::io::stdout();
285 let _ = write!(stdout, "Proceed? [y/N] ");
286 let _ = stdout.flush();
287 let mut line = String::new();
288 std::io::stdin().read_line(&mut line).map(|_| line)
289 })
290 .await
291 .map_err(|e| CliError::failed(e.to_string()))??;
292 if matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
293 Ok(())
294 } else {
295 Err(CliError::failed("cancelled; nothing was locked"))
296 }
297}
298
299async fn balance(h: &TakerHandle) -> CliResult<u64> {
301 let utxos = h.node.get_address_utxos(std::slice::from_ref(&h.address)).await.map_err(paralock::Error::from)?;
302 Ok(utxos.iter().map(|u| u.value).sum())
303}
304
305async fn ensure_funds(setup: &Setup, out: Output, h: &TakerHandle, zat: u64) -> CliResult<()> {
307 let have = balance(h).await?;
308 let need = zat.saturating_add(50_000);
309 if have >= need {
310 return Ok(());
311 }
312 let Some(devnet) = &setup.devnet else {
313 return Err(CliError::failed(format!(
314 "the swap address {} holds {} ZEC; this swap needs {} ZEC plus the network fee",
315 h.address,
316 zec(have),
317 zec(zat)
318 ))
319 .hint(format!("send ZEC to {} (a transparent address), then run the command again", h.address)));
320 };
321 let missing = need - have;
322 out.say(format!("Funding the swap address {} with {} ZEC from the devnet faucet…", h.address, zec(missing)));
323 Faucet::new(&devnet.zcash.dashboard)?.pay(&h.address, missing).await?;
324 let deadline = tokio::time::Instant::now() + Duration::from_secs(120);
325 while balance(h).await? < need {
326 if tokio::time::Instant::now() >= deadline {
327 return Err(CliError::failed("the faucet's ZEC did not arrive within 2 minutes"));
328 }
329 tokio::time::sleep(Duration::from_secs(1)).await;
330 }
331 Ok(())
332}
333
334fn summary_lines(out: Output, swap: &Swap) {
335 let rows = [
336 ("Zcash lock", Some(swap.zcash.htlc_address.clone())),
337 ("Zcash funding", swap.zcash.funding_txid.clone()),
338 ("EVM lock", swap.evm.lock_tx.as_ref().map(ToString::to_string)),
339 ("EVM claim", swap.evm.claim_tx.as_ref().map(ToString::to_string)),
340 ("Zcash claim", swap.zcash.claim_txid.clone()),
341 ("Zcash refund", swap.zcash.refund_txid.clone()),
342 ("EVM refund", swap.evm.refund_tx.as_ref().map(ToString::to_string)),
343 ];
344 for (what, value) in rows {
345 if let Some(value) = value {
346 out.say(format!(" {what:<14} {value}"));
347 }
348 }
349}
350
351async fn run(
352 setup: &Setup,
353 out: Output,
354 h: &TakerHandle,
355 quote: &Quote,
356 options: SwapOptions,
357 common: &SwapCommon,
358 refund_demo: bool,
359) -> CliResult<Exit> {
360 confirm(setup, out, common.yes).await?;
361 ensure_funds(setup, out, h, quote.zec_amount_zat).await?;
362 let d = decimals(setup, quote.chain);
363 if refund_demo {
364 let swap = h.taker.lock_only(quote, options.recipient.clone(), options.label.clone()).await?;
365 out.say(format!(
366 "Refund demo: the maker never locks. Waiting for the Zcash refund height {}…",
367 swap.zcash.refund_height
368 ));
369 let refunded = h.taker.refund(&swap.id, true).await?;
370 out.say(format!("Refunded {} ZEC to the taker. No funds were lost.", zec(refunded.zcash.amount_zat)));
371 summary_lines(out, &refunded);
372 out.result(json!({ "swap": refunded }));
373 return Ok(Exit::Ok);
374 }
375 out.say("Swapping:");
376 let pay = options.recipient.is_some();
377 match h.taker.swap(quote, options).await {
378 Ok(swap) => {
379 let what = format!("{} {} on {}", usd(swap.evm.amount, d), quote.token_symbol, quote.chain);
380 if pay {
381 out.say(format!("\nPaid: {} received {what}; status {}.", swap.evm.recipient, status_str(swap.status)));
382 } else {
383 out.say(format!(
384 "\nDone: received {what} at {}; status {}.",
385 swap.evm.recipient,
386 status_str(swap.status)
387 ));
388 }
389 summary_lines(out, &swap);
390 if swap.mode == SwapMode::Adaptor {
391 out.say("No hashlock appeared on either chain; the secret traveled inside the signatures.");
392 }
393 out.result(json!({ "swap": swap }));
394 Ok(Exit::Ok)
395 }
396 Err(e) => {
397 let Some(failed) = e.failed_swap().cloned() else { return Err(e.into()) };
398 let refund_hint = format!(
399 "refund it once block {} is reached: paralock refund {}",
400 failed.zcash.refund_height, failed.id
401 );
402 if common.no_refund {
403 return Err(CliError::from(e).hint(refund_hint));
404 }
405 out.say(format!(
406 "Swap failed after locking ZEC ({e}). Refunding once the Zcash refund height {} is reached…",
407 failed.zcash.refund_height
408 ));
409 out.say(format!(" (the swap is saved; `paralock refund {}` also works after a restart)", failed.id));
410 match h.taker.refund(&failed.id, true).await {
411 Ok(refunded) => {
412 out.say(format!("ZEC refunded to the taker ({} ZEC).", zec(refunded.zcash.amount_zat)));
413 summary_lines(out, &refunded);
414 if out.json {
415 let error = CliError::from(e);
416 crate::output::print_document(&json!({
417 "ok": false,
418 "refunded": true,
419 "error": { "code": error.code.map(|c| c.as_str()), "message": error.message },
420 "swap": refunded,
421 }));
422 }
423 Ok(Exit::SwapFailed)
424 }
425 Err(r) => {
426 let mut error = CliError::from(e);
427 error.message = format!("{}; the refund failed too: {r}", error.message);
428 error.exit = Exit::SwapFailed;
429 error.swap = Some(Box::new(failed));
430 Err(error.hint(refund_hint))
431 }
432 }
433 }
434 }
435}
436
437fn check_id(id: &str) -> CliResult<String> {
439 let id = id.trim().to_ascii_lowercase();
440 let hash = id.strip_prefix("0x").is_some_and(|d| d.len() == 64 && d.bytes().all(|b| b.is_ascii_hexdigit()));
441 if hash || paralock_core::protocol::is_quote_id(&id) {
442 Ok(id)
443 } else {
444 Err(CliError::usage(format!("\"{id}\" is not a swap id (0x + 64 hex digits, or a UUID)")))
445 }
446}
447
448pub fn read_records(dir: &Path) -> CliResult<Vec<TakerSwapRecord>> {
451 let Ok(entries) = std::fs::read_dir(dir) else { return Ok(Vec::new()) };
452 let mut records = Vec::new();
453 for entry in entries.flatten() {
454 let name = entry.file_name().to_string_lossy().into_owned();
455 if !(name.starts_with("taker~") && name.ends_with(".json")) {
456 continue;
457 }
458 let text = zeroize::Zeroizing::new(std::fs::read_to_string(entry.path())?);
459 match serde_json::from_str::<TakerSwapRecord>(&text) {
460 Ok(record) => records.push(record),
461 Err(e) => warn(&format!("skipping {}: {e}", entry.path().display())),
462 }
463 }
464 records.sort_by_key(|r| r.updated_at);
465 Ok(records)
466}
467
468fn read_record(setup: &Setup, id: &str) -> CliResult<TakerSwapRecord> {
469 let id = check_id(id)?;
470 let path = setup.taker_store().join(format!("taker~{id}.json"));
471 let text = zeroize::Zeroizing::new(std::fs::read_to_string(&path).map_err(|_| {
472 CliError::usage(format!("no swap {id} in {}", setup.taker_store().display()))
473 .hint("`paralock swaps --all` lists the saved swaps")
474 })?);
475 serde_json::from_str(&text).map_err(|e| CliError::failed(format!("{} is unreadable: {e}", path.display())))
476}
477
478fn stage_str(record: &TakerSwapRecord) -> String {
479 serde_json::to_value(record.stage).ok().and_then(|v| v.as_str().map(str::to_owned)).unwrap_or_default()
480}
481
482pub fn swaps(setup: &Setup, out: Output, args: SwapsArgs) -> CliResult<Exit> {
484 let records: Vec<TakerSwapRecord> =
485 read_records(&setup.taker_store())?.into_iter().filter(|r| args.all || !r.stage.is_done()).collect();
486 if records.is_empty() {
487 out.say(if args.all { "No saved swaps." } else { "No unfinished swaps." });
488 }
489 for r in &records {
490 let s = &r.swap;
491 out.say(format!(
492 "{} {:<7} {:<14} {:<10} {} ZEC → {}, refund at block {}",
493 s.id,
494 s.mode.as_str(),
495 status_str(s.status),
496 stage_str(r),
497 zec(s.zcash.amount_zat),
498 s.evm.chain,
499 s.zcash.refund_height
500 ));
501 }
502 let list: Vec<Value> = records
503 .iter()
504 .map(|r| json!({ "stage": stage_str(r), "updatedAt": r.updated_at, "error": r.error, "swap": r.swap }))
505 .collect();
506 out.result(json!({ "store": setup.taker_store(), "swaps": list }));
507 Ok(Exit::Ok)
508}
509
510pub async fn resume(setup: &Setup, out: Output, args: ResumeArgs) -> CliResult<Exit> {
512 let record = read_record(setup, &args.id)?;
513 let id = record.id.clone();
514 let h = build(setup, out, &[record.swap.quote.chain], false, Connect::Online).await?;
515 out.say(format!("Resuming swap {id} ({}, {})…", record.swap.mode.as_str(), stage_str(&record)));
516 let options = ResumeOptions { claim: Some(true), relay_claim: args.no_relay.then_some(false) };
517 let swap = h.taker.resume(&id, options).await?;
518 out.say(format!("Swap {}: {}", swap.id, status_str(swap.status)));
519 summary_lines(out, &swap);
520 out.result(json!({ "swap": swap }));
521 Ok(if swap.status == SwapStatus::Failed { Exit::Failed } else { Exit::Ok })
522}
523
524pub async fn refund(setup: &Setup, out: Output, args: RefundArgs) -> CliResult<Exit> {
526 let record = read_record(setup, &args.id)?;
527 let id = record.id.clone();
528 let h = build(setup, out, &[record.swap.quote.chain], false, Connect::ZcashOnly).await?;
529 out.say(format!(
530 "Refunding swap {id}: {} ZEC from {} once block {} is reached…",
531 zec(record.swap.zcash.amount_zat),
532 record.swap.zcash.htlc_address,
533 record.swap.zcash.refund_height
534 ));
535 let swap = h.taker.refund(&id, !args.no_wait).await?;
536 out.say(format!(
537 "Refunded {} ZEC in {}.",
538 zec(swap.zcash.amount_zat),
539 swap.zcash.refund_txid.as_deref().unwrap_or("(already refunded)")
540 ));
541 out.result(json!({ "swap": swap }));
542 Ok(Exit::Ok)
543}
544
545mod offline {
547 use paralock::evm::{EvmLock, HtlcOutcome, TxHash};
548 use paralock::{ChainInfo, EvmChain, EvmClient, EvmError, Hex};
549 use paralock_core::evm::{EvmAddress, HtlcSwap, U256};
550 use paralock_core::keys::Secret;
551
552 use crate::config::Chain;
553
554 pub struct OfflineEvm {
555 chain: EvmChain,
556 chain_id: u64,
557 contract: Hex,
558 account: Hex,
559 }
560
561 impl OfflineEvm {
562 pub fn new(chain: &Chain, account: Hex) -> Self {
563 Self { chain: chain.chain, chain_id: chain.chain_id, contract: chain.htlc.clone(), account }
564 }
565 }
566
567 fn offline() -> EvmError {
568 EvmError::transient("the EVM RPC is unreachable")
569 }
570
571 #[async_trait::async_trait]
572 impl ChainInfo for OfflineEvm {
573 fn chain(&self) -> EvmChain {
574 self.chain
575 }
576 fn chain_id(&self) -> u64 {
577 self.chain_id
578 }
579 fn contract(&self) -> Hex {
580 self.contract.clone()
581 }
582 fn address(&self) -> Hex {
583 self.account.clone()
584 }
585 async fn now(&self) -> Result<u64, EvmError> {
586 Err(offline())
587 }
588 async fn block_number(&self) -> Result<u64, EvmError> {
589 Err(offline())
590 }
591 async fn rpc_chain_id(&self) -> Result<u64, EvmError> {
592 Err(offline())
593 }
594 async fn token_balance(&self, _: &EvmAddress, _: Option<&EvmAddress>) -> Result<U256, EvmError> {
595 Err(offline())
596 }
597 }
598
599 #[async_trait::async_trait]
600 impl EvmClient for OfflineEvm {
601 async fn lock(&self, _: &HtlcSwap) -> Result<TxHash, EvmError> {
602 Err(offline())
603 }
604 async fn claim(&self, _: &HtlcSwap, _: &Secret) -> Result<TxHash, EvmError> {
605 Err(offline())
606 }
607 async fn simulate_claim(&self, _: &HtlcSwap, _: &Secret) -> Result<(), EvmError> {
608 Err(offline())
609 }
610 async fn refund(&self, _: &HtlcSwap) -> Result<TxHash, EvmError> {
611 Err(offline())
612 }
613 async fn get_lock(&self, _: &[u8; 32]) -> Result<Option<EvmLock<HtlcSwap>>, EvmError> {
614 Err(offline())
615 }
616 async fn state(&self, _: &[u8; 32]) -> Result<[u8; 32], EvmError> {
617 Err(offline())
618 }
619 async fn outcome(&self, _: &[u8; 32]) -> Result<HtlcOutcome, EvmError> {
620 Err(offline())
621 }
622 }
623}