use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use alloy::primitives::{Address, B256, U256};
use alloy::sol_types::{SolCall, SolEvent};
use serde::{Deserialize, Serialize};
use crate::artifacts::ParalockContract;
use crate::bindings::ParalockAdaptorEscrow;
use crate::config::EvmConfig;
use crate::connection::{EventLog, EvmConnection, LogQuery, TxRequest, TxResult};
use crate::error::{EvmError, RevertSource};
use crate::escrow::{ScanConfig, check_new_lock, poll_until};
use crate::swap::{AdaptorOutcome, AdaptorSwap, ClaimSignature, EscrowStatus, LockInfo, Outcome, claim_digest};
#[derive(Clone, Debug)]
pub struct AdaptorEscrowClient {
conn: EvmConnection,
contract: Address,
scan: ScanConfig,
window: Arc<AtomicU64>,
}
const SOURCE: Option<RevertSource> = Some(RevertSource::AdaptorEscrow);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdaptorLockReceipt {
pub approval: Option<TxResult>,
pub lock: TxResult,
pub id: B256,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct AdaptorLock {
pub tx: TxResult,
pub id: B256,
}
impl AdaptorEscrowClient {
pub fn new(conn: EvmConnection, contract: Address, scan: ScanConfig) -> Self {
let window = Arc::new(AtomicU64::new(scan.max_block_range.max(1)));
Self { conn, contract, scan, window }
}
pub async fn connect(config: EvmConfig, contract: Address, scan: ScanConfig) -> Result<Self, EvmError> {
Ok(Self::new(EvmConnection::connect(config).await?, contract, scan))
}
pub fn connection(&self) -> &EvmConnection {
&self.conn
}
pub fn contract(&self) -> Address {
self.contract
}
pub fn chain_id(&self) -> u64 {
self.conn.chain_id()
}
pub fn address(&self) -> Option<Address> {
self.conn.address()
}
pub fn scan_config(&self) -> ScanConfig {
self.scan
}
pub fn scan_window(&self) -> u64 {
self.window.load(Ordering::Relaxed)
}
pub async fn verify_escrow_code(&self) -> Result<(), EvmError> {
self.conn.verify_escrow_code(self.contract, ParalockContract::AdaptorEscrow).await
}
pub async fn now(&self) -> Result<u64, EvmError> {
self.conn.now().await
}
pub async fn block_number(&self) -> Result<u64, EvmError> {
self.conn.block_number().await
}
pub async fn rpc_chain_id(&self) -> Result<u64, EvmError> {
self.conn.rpc_chain_id().await
}
pub async fn token_balance(&self, token: Address, owner: Option<Address>) -> Result<U256, EvmError> {
let owner = match owner {
Some(owner) => owner,
None => self.conn.require_signer()?,
};
self.conn.token_balance(token, owner).await
}
pub fn id_of(&self, swap: &AdaptorSwap) -> B256 {
swap.id()
}
pub fn claim_digest(&self, id: B256) -> B256 {
claim_digest(self.chain_id(), self.contract, id)
}
pub async fn status(&self, id: B256, block: Option<u64>) -> Result<EscrowStatus, EvmError> {
let raw = self.conn.read(self.contract, &ParalockAdaptorEscrow::statusOfCall { id }, block, SOURCE).await?;
EscrowStatus::from_enum(raw)
}
pub async fn get_lock(&self, id: B256) -> Result<Option<LockInfo<AdaptorSwap>>, EvmError> {
self.conn.verify_chain_id().await?;
let tip = self.conn.block_number().await?;
let status = self.status(id, Some(tip)).await?;
if status == EscrowStatus::Empty {
return Ok(None);
}
let logs = self
.scan(id, tip, |logs| logs.iter().any(|l| is(l, ParalockAdaptorEscrow::Locked::SIGNATURE_HASH)))
.await?;
Ok(Some(lock_from_logs(id, &logs, status, self.scan.from_block)?))
}
pub async fn get_locked_swap(&self, id: B256) -> Result<Option<AdaptorSwap>, EvmError> {
Ok(self.get_lock(id).await?.map(|lock| lock.swap))
}
pub async fn outcome(&self, id: B256) -> Result<AdaptorOutcome, EvmError> {
let tip = self.conn.block_number().await?;
let status = self.status(id, Some(tip)).await?;
match status {
EscrowStatus::Empty => return Ok(Outcome::Empty),
EscrowStatus::Active => return Ok(Outcome::Active),
EscrowStatus::Settled => {}
}
let logs = self
.scan(id, tip, |logs| {
logs.iter().any(is_settlement)
&& logs.iter().any(|l| is(l, ParalockAdaptorEscrow::Locked::SIGNATURE_HASH))
})
.await?;
settlement_from_logs(id, &logs, status, self.chain_id(), self.contract, self.scan.from_block)?
.ok_or(EvmError::SettlementNotIndexed { key: id })
}
pub async fn wait_for_lock(&self, id: B256, timeout: Option<Duration>) -> Result<LockInfo<AdaptorSwap>, EvmError> {
let what = format!("a lock under {id}");
poll_until(self.conn.polling_interval(), timeout, &what, || self.get_lock(id)).await
}
pub async fn wait_for_settlement(&self, id: B256, timeout: Option<Duration>) -> Result<AdaptorOutcome, EvmError> {
let what = format!("the settlement of {id}");
poll_until(self.conn.polling_interval(), timeout, &what, || async {
Ok(match self.outcome(id).await? {
outcome @ (Outcome::Claimed { .. } | Outcome::Refunded) => Some(outcome),
Outcome::Empty | Outcome::Active => None,
})
})
.await
}
pub async fn approve_if_needed(&self, token: Address, amount: U256) -> Result<Option<TxResult>, EvmError> {
self.conn.approve_if_needed(token, self.contract, amount).await
}
pub async fn lock(&self, swap: &AdaptorSwap) -> Result<AdaptorLock, EvmError> {
self.check_lock(swap).await?;
let id = swap.id();
let call = ParalockAdaptorEscrow::lockCall { s: (*swap).into() };
let tx = TxRequest::call(self.contract, &call, SOURCE);
let output = self.conn.simulate(&tx).await?;
let simulated = ParalockAdaptorEscrow::lockCall::abi_decode_returns_validate(&output)
.map_err(|e| EvmError::InvalidResponse(format!("cannot decode lock's return value: {e}")))?;
if simulated != id {
return Err(EvmError::InvalidResponse(format!("contract computes swap id {simulated}, expected {id}")));
}
let tx = self.conn.send_and_confirm(tx).await?;
Ok(AdaptorLock { tx, id })
}
pub async fn approve_and_lock(&self, swap: &AdaptorSwap) -> Result<AdaptorLockReceipt, EvmError> {
let account = self.conn.require_signer()?;
self.check_lock(swap).await?; let guard = self.conn.approval_lock(account, swap.token, self.contract);
let _held = guard.lock().await;
let approval = self.approve_if_needed(swap.token, swap.amount).await?;
let AdaptorLock { tx, id } = self.lock(swap).await?;
Ok(AdaptorLockReceipt { approval, lock: tx, id })
}
async fn check_lock(&self, swap: &AdaptorSwap) -> Result<(), EvmError> {
let account = self.conn.require_signer()?;
if swap.signer == Address::ZERO {
return Err(EvmError::InvalidInput("signer is the zero address".to_owned()));
}
check_new_lock(&self.conn, account, swap.sender, swap.recipient, swap.amount, swap.expiry).await
}
pub async fn claim(&self, swap: &AdaptorSwap, signature: &ClaimSignature) -> Result<TxResult, EvmError> {
self.conn.send_and_confirm(self.claim_request(swap, signature)?).await
}
pub fn claim_request(&self, swap: &AdaptorSwap, signature: &ClaimSignature) -> Result<TxRequest, EvmError> {
let sig = signature.normalized()?;
let id = swap.id();
let recovered = sig.recover(&self.claim_digest(id))?;
if recovered != swap.signer {
return Err(EvmError::WrongSigner { expected: swap.signer, recovered });
}
let call = ParalockAdaptorEscrow::claimCall { s: (*swap).into(), v: sig.v, r: sig.r, sigS: sig.s };
Ok(TxRequest::call(self.contract, &call, SOURCE))
}
pub async fn refund(&self, swap: &AdaptorSwap) -> Result<TxResult, EvmError> {
self.conn.send_and_confirm(self.refund_request(swap)).await
}
pub fn refund_request(&self, swap: &AdaptorSwap) -> TxRequest {
TxRequest::call(self.contract, &ParalockAdaptorEscrow::refundCall { s: (*swap).into() }, SOURCE)
}
async fn scan(&self, id: B256, tip: u64, done: impl FnMut(&[EventLog]) -> bool) -> Result<Vec<EventLog>, EvmError> {
let events = [
ParalockAdaptorEscrow::Locked::SIGNATURE_HASH,
ParalockAdaptorEscrow::Claimed::SIGNATURE_HASH,
ParalockAdaptorEscrow::Refunded::SIGNATURE_HASH,
];
let query = LogQuery {
contract: self.contract,
events: &events,
key: id,
from_block: self.scan.from_block,
to_block: tip,
window: &self.window,
};
self.conn.scan_logs(&query, done).await
}
}
fn is(log: &EventLog, signature: B256) -> bool {
log.topics.first() == Some(&signature)
}
fn is_settlement(log: &EventLog) -> bool {
is(log, ParalockAdaptorEscrow::Claimed::SIGNATURE_HASH) || is(log, ParalockAdaptorEscrow::Refunded::SIGNATURE_HASH)
}
pub(crate) fn lock_from_logs(
id: B256,
logs: &[EventLog],
status: EscrowStatus,
from_block: u64,
) -> Result<LockInfo<AdaptorSwap>, EvmError> {
let mut locked = logs.iter().filter(|l| is(l, ParalockAdaptorEscrow::Locked::SIGNATURE_HASH));
let log = locked.next().ok_or(EvmError::LockEventMissing { key: id, status, from_block })?;
if locked.next().is_some() {
return Err(EvmError::InvalidResponse(format!("more than one Locked event for {id}")));
}
let event = ParalockAdaptorEscrow::Locked::decode_raw_log_validate(log.topics.iter().copied(), &log.data)
.map_err(|e| EvmError::InvalidResponse(format!("malformed Locked event: {e}")))?;
let swap = AdaptorSwap {
token: event.token,
sender: event.sender,
recipient: event.recipient,
signer: event.signer,
amount: event.amount,
expiry: event.expiry,
salt: event.salt,
};
if event.id != id || swap.id() != id {
return Err(EvmError::InvalidResponse(format!("Locked event for {id} does not match its id")));
}
Ok(LockInfo { swap, block_number: log.block_number, block_hash: log.block_hash, tx_hash: log.tx_hash, status })
}
pub(crate) fn settlement_from_logs(
id: B256,
logs: &[EventLog],
status: EscrowStatus,
chain_id: u64,
contract: Address,
from_block: u64,
) -> Result<Option<AdaptorOutcome>, EvmError> {
let mut outcome = None;
for log in logs.iter().filter(|l| is_settlement(l)) {
let this = if is(log, ParalockAdaptorEscrow::Claimed::SIGNATURE_HASH) {
let event = ParalockAdaptorEscrow::Claimed::decode_raw_log_validate(log.topics.iter().copied(), &log.data)
.map_err(|e| EvmError::InvalidResponse(format!("malformed Claimed event: {e}")))?;
let signature = ClaimSignature { v: event.v, r: event.r, s: event.s };
if event.id != id || !signature.is_canonical() {
return Err(EvmError::InvalidResponse(format!(
"Claimed event for {id} is not a canonical claim of it"
)));
}
Outcome::Claimed { revealed: signature }
} else {
let event = ParalockAdaptorEscrow::Refunded::decode_raw_log_validate(log.topics.iter().copied(), &log.data)
.map_err(|e| EvmError::InvalidResponse(format!("malformed Refunded event: {e}")))?;
if event.id != id {
return Err(EvmError::InvalidResponse(format!("Refunded event for {} returned for {id}", event.id)));
}
Outcome::Refunded
};
if outcome.is_some_and(|o| o != this) {
return Err(EvmError::InvalidResponse(format!("conflicting settlement events for {id}")));
}
outcome = Some(this);
}
if let Some(Outcome::Claimed { revealed }) = &outcome {
let lock = lock_from_logs(id, logs, status, from_block)?;
let recovered = revealed.recover(&claim_digest(chain_id, contract, id))?;
if recovered != lock.swap.signer {
return Err(EvmError::InvalidResponse(format!(
"Claimed event for {id} carries a signature by {recovered}, not the swap's signer {}",
lock.swap.signer
)));
}
}
Ok(outcome)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use crate::htlc::tests::log_of;
use alloy::signers::SignerSync;
use alloy::signers::local::PrivateKeySigner;
const CHAIN: u64 = 31337;
const CONTRACT: Address = Address::repeat_byte(0xe5);
fn swap(signer: Address) -> AdaptorSwap {
AdaptorSwap {
token: Address::repeat_byte(0x20),
sender: Address::repeat_byte(1),
recipient: Address::repeat_byte(2),
signer,
amount: U256::from(25_000_000u64),
expiry: 1_800_003_600,
salt: B256::repeat_byte(5),
}
}
fn locked(s: &AdaptorSwap) -> ParalockAdaptorEscrow::Locked {
ParalockAdaptorEscrow::Locked {
id: s.id(),
sender: s.sender,
recipient: s.recipient,
token: s.token,
signer: s.signer,
amount: s.amount,
expiry: s.expiry,
salt: s.salt,
}
}
fn claimed(id: B256, sig: &ClaimSignature) -> ParalockAdaptorEscrow::Claimed {
ParalockAdaptorEscrow::Claimed { id, v: sig.v, r: sig.r, s: sig.s }
}
fn settle(id: B256, logs: &[EventLog]) -> Result<Option<AdaptorOutcome>, EvmError> {
settlement_from_logs(id, logs, EscrowStatus::Settled, CHAIN, CONTRACT, 0)
}
#[test]
fn rebuilds_the_lock_and_checks_its_id() {
let s = swap(Address::repeat_byte(9));
let lock = lock_from_logs(s.id(), &[log_of(&locked(&s), 3)], EscrowStatus::Active, 0).unwrap();
assert_eq!(lock.swap, s);
let mut event = locked(&s);
event.amount += U256::from(1); let err = lock_from_logs(s.id(), &[log_of(&event, 3)], EscrowStatus::Active, 0).unwrap_err();
assert!(matches!(err, EvmError::InvalidResponse(_)), "{err}");
}
#[test]
fn verifies_the_published_signature() {
let key = PrivateKeySigner::random();
let s = swap(key.address());
let id = s.id();
let lock = log_of(&locked(&s), 3);
let digest = claim_digest(CHAIN, CONTRACT, id);
let sig = ClaimSignature::try_from(key.sign_hash_sync(&digest).unwrap()).unwrap();
assert_eq!(
settle(id, &[lock.clone(), log_of(&claimed(id, &sig), 4)]).unwrap(),
Some(Outcome::Claimed { revealed: sig })
);
assert_eq!(settle(id, std::slice::from_ref(&lock)).unwrap(), None);
let refunded = log_of(&ParalockAdaptorEscrow::Refunded { id }, 4);
assert_eq!(settle(id, &[lock.clone(), refunded]).unwrap(), Some(Outcome::Refunded));
let other = PrivateKeySigner::random();
let forged = ClaimSignature::try_from(other.sign_hash_sync(&digest).unwrap()).unwrap();
assert!(matches!(
settle(id, &[lock.clone(), log_of(&claimed(id, &forged), 4)]),
Err(EvmError::InvalidResponse(_))
));
let elsewhere = claim_digest(CHAIN + 1, CONTRACT, id);
let replayed = ClaimSignature::try_from(key.sign_hash_sync(&elsewhere).unwrap()).unwrap();
assert!(matches!(
settle(id, &[lock.clone(), log_of(&claimed(id, &replayed), 4)]),
Err(EvmError::InvalidResponse(_))
));
let high = ClaimSignature {
v: if sig.v == 27 { 28 } else { 27 },
r: sig.r,
s: B256::from((crate::SECP256K1_N - U256::from_be_bytes(sig.s.0)).to_be_bytes::<32>()),
};
assert!(matches!(
settle(id, &[lock.clone(), log_of(&claimed(id, &high), 4)]),
Err(EvmError::InvalidResponse(_))
));
let bad_v = ClaimSignature { v: 29, ..sig };
assert!(matches!(
settle(id, &[lock.clone(), log_of(&claimed(id, &bad_v), 4)]),
Err(EvmError::InvalidResponse(_))
));
assert!(matches!(settle(id, &[log_of(&claimed(id, &sig), 4)]), Err(EvmError::LockEventMissing { .. })));
}
}