use super::error::{ParalockError, ParalockErrorCode};
use super::messages::{AcceptRequest, AdaptorPreSignatureRequest, AdaptorProposal, AdaptorProposalAccepted};
use super::policy::{
TimeoutPolicy, assert_optional_funding_expiry, assert_refund_window, assert_required_funding_expiry,
assert_safe_windows,
};
use super::spends::{AdaptorSpends, AdaptorSpendsRequest, adaptor_spends};
use super::types::{Hex, Quote, SwapMode};
use super::upgrade::{UpgradeCheck, require_upgrade_check};
use super::validation::ClaimLimits;
use crate::adaptor::{self, AdaptorSecret, PreSignature, VerifiedPreSignature};
use crate::evm::{
self, AdaptorSwap, ClaimSignature, EMPTY_STATE, EscrowStatus, EvmAddress, HtlcSwap, SETTLED_STATE, U256,
};
use crate::keys::{KeyPair, PublicKey};
use crate::zcash::{
Htlc, MultisigSpend, Network, ParsedTx, SignedTx, TransparentAddress, Txid, finalize_multisig_spend,
parse_htlc_redeem_script, sign_multisig_spend, verify_multisig_signature,
};
fn rejected(message: impl Into<String>) -> ParalockError {
ParalockError::new(ParalockErrorCode::QuoteRejected, message)
}
fn mismatch(message: impl Into<String>) -> ParalockError {
ParalockError::new(ParalockErrorCode::LockMismatch, message)
}
fn bad(message: impl Into<String>) -> ParalockError {
ParalockError::invalid_request(message)
}
fn internal(message: impl Into<String>) -> ParalockError {
ParalockError::new(ParalockErrorCode::Internal, message)
}
fn same_address(hex: &Hex, address: &EvmAddress) -> bool {
EvmAddress::parse_any_case(hex.as_str()).is_ok_and(|a| a == *address)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedAcceptance {
pub spends: AdaptorSpends,
pub signer_pubkey: PublicKey,
pub signer: EvmAddress,
}
pub fn verify_proposal_accepted(
quote: &Quote,
proposal: &AdaptorProposal,
accepted: &AdaptorProposalAccepted,
node_branch_id: u32,
lock_address: &str,
network: Network,
upgrade: &UpgradeCheck,
) -> Result<VerifiedAcceptance, ParalockError> {
if accepted.swap_id != quote.id || proposal.quote_id != quote.id {
return Err(rejected("maker answered for a different swap"));
}
if quote.mode != SwapMode::Adaptor {
return Err(rejected("quote is not an adaptor quote"));
}
if proposal.amount_zat != quote.zec_amount_zat {
return Err(rejected("proposal amount differs from the quote"));
}
require_upgrade_check(upgrade, proposal.refund_height)?;
if accepted.branch_id != node_branch_id {
return Err(rejected("maker signed the refund for a different consensus branch; not funding"));
}
let claim_to = TransparentAddress::decode_for(&accepted.claim_to, network)
.map_err(|_| rejected("maker's claim address is invalid"))?;
let refund_address = TransparentAddress::decode_for(&proposal.refund_address, network)
.map_err(|_| rejected("refund address is not a transparent address of this network"))?;
let taker =
PublicKey::from_hex(proposal.taker_zcash_pubkey.as_str()).map_err(|_| rejected("taker key is invalid"))?;
let maker = PublicKey::from_hex(quote.maker_zcash_pubkey.as_str())
.map_err(|_| rejected("maker's Zcash public key is not a valid curve point"))?;
let funding_txid =
Txid::from_display_hex(&proposal.funding_txid).map_err(|_| rejected("funding txid is invalid"))?;
let spends = adaptor_spends(&AdaptorSpendsRequest {
network,
taker_pubkey: &taker,
maker_pubkey: &maker,
funding_txid,
funding_vout: proposal.funding_vout,
amount_zat: proposal.amount_zat,
refund_height: proposal.refund_height,
refund_address: &refund_address,
claim_to: &claim_to,
branch_id: accepted.branch_id,
})
.map_err(|e| rejected(e.to_string()))?;
if spends.refund.multisig().address() != lock_address {
return Err(rejected("maker built a different lock"));
}
if !verify_multisig_signature(&spends.refund, &maker, &accepted.maker_refund_signature.to_bytes()) {
return Err(rejected("maker's refund signature is invalid; not funding"));
}
let signer_pubkey = PublicKey::from_compressed(&accepted.signer_pubkey.to_bytes())
.map_err(|_| rejected("signer pubkey must be a 33-byte compressed secp256k1 key"))?;
let signer = EvmAddress::from_public_key(&signer_pubkey);
if !same_address(&accepted.signer, &signer) {
return Err(rejected("signer pubkey does not match the signer address"));
}
Ok(VerifiedAcceptance { spends, signer_pubkey, signer })
}
pub fn check_lock_terms(
recipient: &EvmAddress,
token: &EvmAddress,
amount: &U256,
expected_recipient: &Hex,
quote: &Quote,
) -> Result<(), ParalockError> {
if !same_address(expected_recipient, recipient) {
return Err(mismatch("lock pays someone else"));
}
if !same_address("e.token, token) {
return Err(mismatch("lock uses a different token"));
}
if *amount < U256::from_u128(quote.usd_amount) {
return Err(mismatch("lock is smaller than quoted"));
}
Ok(())
}
pub fn check_htlc_lock(
locked: &HtlcSwap,
hashlock: &[u8; 32],
expected_recipient: &Hex,
quote: &Quote,
) -> Result<(), ParalockError> {
if locked.hashlock != *hashlock {
return Err(mismatch("lock uses a different hashlock"));
}
check_lock_terms(&locked.recipient, &locked.token, &locked.amount, expected_recipient, quote)
}
pub fn check_adaptor_lock(
locked: &AdaptorSwap,
signer: &Hex,
expected_recipient: &Hex,
quote: &Quote,
) -> Result<(), ParalockError> {
check_lock_terms(&locked.recipient, &locked.token, &locked.amount, expected_recipient, quote)?;
if !same_address(signer, &locked.signer) {
return Err(mismatch("lock uses a different signer"));
}
Ok(())
}
pub fn check_evm_pre_signature(
pre_signature: &str,
signer_pubkey: &PublicKey,
chain_id: u64,
contract: &EvmAddress,
locked: &AdaptorSwap,
adaptor_point: &PublicKey,
) -> Result<VerifiedPreSignature, ParalockError> {
if EvmAddress::from_public_key(signer_pubkey) != locked.signer {
return Err(mismatch("lock uses a different signer"));
}
let digest = evm::claim_digest(chain_id, contract, &locked.id());
let pre = PreSignature::from_hex(pre_signature).map_err(|_| mismatch("maker's pre-signature is malformed"))?;
adaptor::pre_verify(signer_pubkey, &digest, adaptor_point, &pre)
.map_err(|_| mismatch("maker's pre-signature is invalid"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EvmLockObservation {
pub now: u64,
pub latest_block: u64,
pub lock_block: u64,
}
pub fn assert_claim_window(expiry: u64, now: u64, limits: &ClaimLimits) -> Result<(), ParalockError> {
if i128::from(expiry) - i128::from(now) < i128::from(limits.min_claim_window_seconds()) {
return Err(ParalockError::new(
ParalockErrorCode::Timeout,
"the maker's lock expires too soon to claim safely",
));
}
Ok(())
}
pub fn assert_lock_confirmed(lock_block: u64, latest_block: u64, limits: &ClaimLimits) -> Result<(), ParalockError> {
let confirmations = i128::from(latest_block) - i128::from(lock_block) + 1;
if confirmations < i128::from(limits.min_evm_confirmations()) {
return Err(ParalockError::new(
ParalockErrorCode::Timeout,
format!(
"the maker's lock has {} of {} confirmations",
confirmations.max(0),
limits.min_evm_confirmations()
),
));
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub struct HtlcLockCheck<'a> {
pub quote: &'a Quote,
pub request: &'a AcceptRequest,
pub hashlock: &'a [u8; 32],
pub locked: &'a HtlcSwap,
pub state: &'a [u8; 32],
pub observed: &'a EvmLockObservation,
pub limits: &'a ClaimLimits,
}
pub fn verify_htlc_lock(check: &HtlcLockCheck<'_>) -> Result<(), ParalockError> {
let HtlcLockCheck { quote, request, hashlock, locked, state, observed, limits } = *check;
if request.quote_id != quote.id || quote.mode != SwapMode::Htlc {
return Err(mismatch("the lock is checked against another swap"));
}
let script_hashlock = parse_htlc_redeem_script(&request.redeem_script.to_bytes()).map(|p| p.hashlock);
if script_hashlock.as_ref().ok() != Some(hashlock) {
return Err(mismatch("the lock is checked against another swap"));
}
if locked.hashlock != *hashlock {
return Err(mismatch("lock uses a different hashlock"));
}
if *state != locked.commitment() {
return Err(mismatch(match *state {
EMPTY_STATE => "there is no lock for this hashlock",
SETTLED_STATE => "the lock is already settled",
_ => "the lock does not match the on-chain commitment",
}));
}
check_lock_terms(&locked.recipient, &locked.token, &locked.amount, &request.taker_evm_address, quote)?;
assert_lock_confirmed(observed.lock_block, observed.latest_block, limits)?;
assert_claim_window(locked.expiry, observed.now, limits)
}
#[derive(Debug, Clone, Copy)]
pub struct AdaptorLockCheck<'a> {
pub quote: &'a Quote,
pub proposal: &'a AdaptorProposal,
pub accepted: &'a AdaptorProposalAccepted,
pub chain_id: u64,
pub contract: &'a EvmAddress,
pub escrow_id: &'a [u8; 32],
pub locked: &'a AdaptorSwap,
pub status: EscrowStatus,
pub pre_signature: &'a str,
pub observed: &'a EvmLockObservation,
pub limits: &'a ClaimLimits,
}
pub fn verify_adaptor_lock(check: &AdaptorLockCheck<'_>) -> Result<VerifiedPreSignature, ParalockError> {
let AdaptorLockCheck {
quote,
proposal,
accepted,
chain_id,
contract,
escrow_id,
locked,
status,
pre_signature,
observed,
limits,
} = *check;
if proposal.quote_id != quote.id || accepted.swap_id != quote.id || quote.mode != SwapMode::Adaptor {
return Err(mismatch("the lock is checked against another swap"));
}
if locked.id() != *escrow_id {
return Err(mismatch("the lock read on-chain is not the announced escrow"));
}
if status != EscrowStatus::Active {
return Err(mismatch("the lock is not active"));
}
check_adaptor_lock(locked, &accepted.signer, &proposal.taker_evm_recipient, quote)?;
let signer_pubkey = PublicKey::from_compressed(&accepted.signer_pubkey.to_bytes())
.map_err(|_| mismatch("signer pubkey must be a 33-byte compressed secp256k1 key"))?;
let adaptor_point =
PublicKey::from_hex(proposal.adaptor_point.as_str()).map_err(|_| mismatch("adaptor point is invalid"))?;
let verified = check_evm_pre_signature(pre_signature, &signer_pubkey, chain_id, contract, locked, &adaptor_point)?;
assert_lock_confirmed(observed.lock_block, observed.latest_block, limits)?;
assert_claim_window(locked.expiry, observed.now, limits)?;
Ok(verified)
}
pub fn complete_escrow_claim(
verified: &VerifiedPreSignature,
adaptor_secret: &AdaptorSecret,
) -> Result<ClaimSignature, ParalockError> {
let signature = adaptor::adapt(verified, adaptor_secret).map_err(|e| match e {
adaptor::Error::WrongAdaptorSecret => internal("the adaptor secret does not match the swap's adaptor point"),
other => mismatch(format!("could not complete the maker's pre-signature: {other}")),
})?;
let claim = evm::claim_signature(&signature, verified.public_key(), verified.digest())
.map_err(|_| mismatch("the completed signature does not recover to the signer"))?;
let recovered = evm::recover_signer(verified.digest(), &claim)
.map_err(|_| mismatch("the completed signature does not recover to the signer"))?;
if recovered != EvmAddress::from_public_key(verified.public_key()) {
return Err(mismatch("the completed signature does not recover to the signer"));
}
Ok(claim)
}
#[derive(Debug, Clone, Copy)]
pub struct AcceptRequestCheck<'a> {
pub request: &'a AcceptRequest,
pub quote: &'a Quote,
pub maker_key: &'a PublicKey,
pub network: Network,
pub tip: u32,
pub policy: &'a TimeoutPolicy,
pub max_funding_expiry_blocks: u32,
}
pub fn verify_accept_request(check: &AcceptRequestCheck<'_>) -> Result<Htlc, ParalockError> {
let AcceptRequestCheck { request, quote, maker_key, network, tip, policy, max_funding_expiry_blocks } = *check;
let htlc = Htlc::from_redeem_script(&request.redeem_script.to_bytes(), network)
.map_err(|_| bad("redeemScript is not a Paralock HTLC"))?;
if request.quote_id != quote.id {
return Err(bad("the request is for another quote"));
}
if quote.mode != SwapMode::Htlc {
return Err(bad(format!("quote is for {} mode", quote.mode.as_str())));
}
if htlc.params().claim_pubkey != *maker_key {
return Err(bad("HTLC does not pay the maker's key"));
}
assert_optional_funding_expiry(request.funding_expiry_height, tip, max_funding_expiry_blocks)?;
assert_safe_windows(policy, htlc.params().refund_height, tip, u64::from(quote.evm_lock_seconds))?;
assert_refund_window(htlc.params().refund_height, tip, policy)?;
Ok(htlc)
}
#[derive(Debug, Clone, Copy)]
pub struct AdaptorProposalCheck<'a> {
pub proposal: &'a AdaptorProposal,
pub quote: &'a Quote,
pub maker_key: &'a PublicKey,
pub network: Network,
pub tip: u32,
pub policy: &'a TimeoutPolicy,
pub max_funding_expiry_blocks: u32,
pub upgrade: &'a UpgradeCheck,
pub branch_id: u32,
pub claim_to: &'a TransparentAddress,
}
pub fn verify_adaptor_proposal(check: &AdaptorProposalCheck<'_>) -> Result<AdaptorSpends, ParalockError> {
let AdaptorProposalCheck {
proposal,
quote,
maker_key,
network,
tip,
policy,
max_funding_expiry_blocks,
upgrade,
branch_id,
claim_to,
} = *check;
if proposal.quote_id != quote.id {
return Err(bad("the proposal is for another quote"));
}
if quote.mode != SwapMode::Adaptor {
return Err(bad(format!("quote is for {} mode", quote.mode.as_str())));
}
if !PublicKey::from_hex(quote.maker_zcash_pubkey.as_str()).is_ok_and(|k| k == *maker_key) {
return Err(internal("the quote was made with another maker key"));
}
let refund_address = TransparentAddress::decode_for(&proposal.refund_address, network)
.map_err(|_| bad(format!("refundAddress must be a {network} transparent address")))?;
if proposal.amount_zat != quote.zec_amount_zat {
return Err(bad("lock amount must equal the quoted amount"));
}
let taker = PublicKey::from_hex(proposal.taker_zcash_pubkey.as_str())
.map_err(|_| bad("takerZcashPubkey must be a compressed secp256k1 public key"))?;
if taker == *maker_key {
return Err(bad("taker key must differ from the maker's"));
}
assert_required_funding_expiry(proposal.funding_expiry_height, tip, max_funding_expiry_blocks)?;
assert_safe_windows(policy, proposal.refund_height, tip, u64::from(quote.evm_lock_seconds))?;
assert_refund_window(proposal.refund_height, tip, policy)?;
require_upgrade_check(upgrade, proposal.refund_height)?;
let funding_txid =
Txid::from_display_hex(&proposal.funding_txid).map_err(|_| bad("fundingTxid must be a 64-character txid"))?;
claim_to.check_network(network).map_err(|e| internal(format!("the maker's payout address: {e}")))?;
adaptor_spends(&AdaptorSpendsRequest {
network,
taker_pubkey: &taker,
maker_pubkey: maker_key,
funding_txid,
funding_vout: proposal.funding_vout,
amount_zat: proposal.amount_zat,
refund_height: proposal.refund_height,
refund_address: &refund_address,
claim_to,
branch_id,
})
.map_err(|e| bad(e.to_string()))
}
pub fn verify_taker_pre_signature(
proposal: &AdaptorProposal,
spends: &AdaptorSpends,
request: &AdaptorPreSignatureRequest,
) -> Result<VerifiedPreSignature, ParalockError> {
let pre = PreSignature::from_hex(&request.taker_pre_signature).map_err(|_| bad("malformed pre-signature"))?;
let invalid = || bad("invalid pre-signature for the maker's claim");
let taker = PublicKey::from_hex(proposal.taker_zcash_pubkey.as_str()).map_err(|_| invalid())?;
let point = PublicKey::from_hex(proposal.adaptor_point.as_str()).map_err(|_| invalid())?;
if !spends.claim.multisig().contains(&taker) {
return Err(invalid());
}
adaptor::pre_verify(&taker, spends.claim.sighash(), &point, &pre).map_err(|_| invalid())
}
pub fn complete_maker_claim(
claim: &MultisigSpend,
taker_pre: &VerifiedPreSignature,
adaptor_secret: &AdaptorSecret,
maker: &KeyPair,
) -> Result<SignedTx, ParalockError> {
if taker_pre.digest() != claim.sighash() || !claim.multisig().contains(taker_pre.public_key()) {
return Err(internal("the taker's pre-signature is not for this claim"));
}
let taker_signature =
adaptor::adapt(taker_pre, adaptor_secret).map_err(|e| internal(format!("completing the claim: {e}")))?;
let mut taker_sig = taker_signature.to_der();
taker_sig.push(crate::zcash::SighashType::All.to_u8());
let maker_sig = sign_multisig_spend(claim, maker).map_err(|e| internal(format!("signing the claim: {e}")))?;
finalize_multisig_spend(claim, &[(*taker_pre.public_key(), taker_sig), (maker.public_key(), maker_sig)])
.map_err(|e| internal(format!("assembling the claim: {e}")))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FundingAmount {
Exact(u64),
AtLeast(u64),
}
pub fn check_funding_output(
tx: &ParsedTx,
vout: u32,
script_pubkey: &[u8],
amount: FundingAmount,
) -> Result<u64, ParalockError> {
let output = usize::try_from(vout)
.ok()
.and_then(|i| tx.tx.outputs.get(i))
.ok_or_else(|| mismatch(format!("the funding has no output {vout}")))?;
if output.script_pubkey != script_pubkey {
return Err(mismatch(format!("funding output {vout} does not pay the lock")));
}
let value =
u64::try_from(output.value).map_err(|_| mismatch(format!("funding output {vout} has a negative value")))?;
let (ok, expected) = match amount {
FundingAmount::Exact(expected) => (value == expected, expected),
FundingAmount::AtLeast(expected) => (value >= expected, expected),
};
if !ok {
return Err(mismatch(format!("the Zcash lock holds {value} zat, expected {expected}")));
}
Ok(value)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use crate::protocol::{DEFAULT_CLAIM_LIMITS, EvmChain, SwapDirection};
use crate::zcash::{TransparentTx, TxOut};
fn quote() -> Quote {
Quote {
id: "00000000-0000-4000-8000-000000000001".into(),
direction: SwapDirection::ZecToUsd,
mode: SwapMode::Adaptor,
chain: EvmChain::Tempo,
chain_id: Some(42431),
zec_amount_zat: 100_000_000,
usd_amount: 49_745_025,
token: Hex::new("0x20c0000000000000000000006a37DA5C996874BE").unwrap(),
token_symbol: "OUSD".into(),
rate: 49.75,
maker_zcash_pubkey: Hex::from_bytes(KeyPair::random(&mut crate::testutil::TestRng).public_key().as_bytes()),
maker_evm_address: Hex::new("0x6709a9c834a22dd371d447278641001f98811690").unwrap(),
zcash_lock_blocks: 144,
evm_lock_seconds: 3600,
expires_at: 1_800_000_060,
exact_out: false,
}
}
#[test]
fn lock_terms() {
let q = quote();
let recipient: EvmAddress = "0x36e33698492d087ee35db0830ba305faf075d087".parse().unwrap();
let expected = Hex::new("0x36E33698492d087ee35db0830ba305faf075d087").unwrap();
let token = EvmAddress::parse_any_case(q.token.as_str()).unwrap();
let amount = U256::from_u128(q.usd_amount);
assert!(check_lock_terms(&recipient, &token, &amount, &expected, &q).is_ok());
let other = EvmAddress([9; 20]);
assert_eq!(
check_lock_terms(&other, &token, &amount, &expected, &q).unwrap_err().message,
"lock pays someone else"
);
assert_eq!(
check_lock_terms(&recipient, &other, &amount, &expected, &q).unwrap_err().message,
"lock uses a different token"
);
let less = U256::from_u128(q.usd_amount - 1);
let err = check_lock_terms(&recipient, &token, &less, &expected, &q).unwrap_err();
assert_eq!((err.code, err.message.as_str()), (ParalockErrorCode::LockMismatch, "lock is smaller than quoted"));
let htlc = HtlcSwap { hashlock: [1; 32], token, sender: other, recipient, amount, expiry: 10 };
assert!(check_htlc_lock(&htlc, &[1; 32], &expected, &q).is_ok());
assert!(check_htlc_lock(&htlc, &[2; 32], &expected, &q).is_err());
let signer = Hex::from_bytes(&[7; 20]);
let lock = AdaptorSwap {
token,
sender: other,
recipient,
signer: EvmAddress([7; 20]),
amount,
expiry: 10,
salt: [0; 32],
};
assert!(check_adaptor_lock(&lock, &signer, &expected, &q).is_ok());
assert!(check_adaptor_lock(&lock, &Hex::from_bytes(&[8; 20]), &expected, &q).is_err());
}
#[test]
fn claim_window_confirmations_and_pre_signature() {
let limits = DEFAULT_CLAIM_LIMITS;
assert!(assert_claim_window(1_000, 970, &limits).is_ok());
assert_eq!(assert_claim_window(1_000, 971, &limits).unwrap_err().code, ParalockErrorCode::Timeout);
assert!(assert_claim_window(1_000, 2_000, &limits).is_err());
assert!(assert_lock_confirmed(10, 10, &limits).is_ok());
assert_eq!(
assert_lock_confirmed(11, 10, &limits).unwrap_err().message,
"the maker's lock has 0 of 1 confirmations"
);
let three = ClaimLimits::new(60, 3).unwrap();
assert!(assert_lock_confirmed(10, 11, &three).is_err());
assert!(assert_lock_confirmed(10, 12, &three).is_ok());
let signer = KeyPair::random(&mut crate::testutil::TestRng);
let (_, point) = adaptor::adaptor_key_pair(&mut crate::testutil::TestRng);
let lock = AdaptorSwap {
token: EvmAddress([1; 20]),
sender: EvmAddress([2; 20]),
recipient: EvmAddress([3; 20]),
signer: EvmAddress::from_public_key(&signer.public_key()),
amount: U256::from(5u64),
expiry: 10,
salt: [4; 32],
};
let contract = EvmAddress([5; 20]);
let digest = evm::claim_digest(42431, &contract, &lock.id());
let pre = adaptor::pre_sign(&mut crate::testutil::TestRng, signer.private_key(), &digest, &point).unwrap();
let ok = check_evm_pre_signature(&pre.to_hex(), &signer.public_key(), 42431, &contract, &lock, &point).unwrap();
assert_eq!(ok.digest(), &digest);
let err = check_evm_pre_signature(&pre.to_hex(), &signer.public_key(), 1, &contract, &lock, &point);
assert_eq!(err.unwrap_err().message, "maker's pre-signature is invalid");
let other_lock = AdaptorSwap { salt: [6; 32], ..lock };
assert!(
check_evm_pre_signature(&pre.to_hex(), &signer.public_key(), 42431, &contract, &other_lock, &point)
.is_err()
);
let stranger = KeyPair::random(&mut crate::testutil::TestRng).public_key();
let err = check_evm_pre_signature(&pre.to_hex(), &stranger, 42431, &contract, &lock, &point).unwrap_err();
assert_eq!(err.message, "lock uses a different signer");
assert_eq!(
check_evm_pre_signature("zz", &signer.public_key(), 42431, &contract, &lock, &point).unwrap_err().message,
"maker's pre-signature is malformed"
);
}
#[test]
fn funding_outputs() {
let tx = ParsedTx {
tx: TransparentTx {
branch_id: 1,
lock_time: 0,
expiry_height: 0,
inputs: vec![],
outputs: vec![
TxOut { value: 1_000, script_pubkey: vec![0xa9] },
TxOut { value: -1, script_pubkey: vec![1] },
],
},
shielded_raw: vec![0, 0, 0],
};
assert_eq!(check_funding_output(&tx, 0, &[0xa9], FundingAmount::Exact(1_000)).unwrap(), 1_000);
assert_eq!(check_funding_output(&tx, 0, &[0xa9], FundingAmount::AtLeast(999)).unwrap(), 1_000);
let err = check_funding_output(&tx, 0, &[0xa9], FundingAmount::Exact(999)).unwrap_err();
assert_eq!(
(err.code, err.message.as_str()),
(ParalockErrorCode::LockMismatch, "the Zcash lock holds 1000 zat, expected 999")
);
assert!(check_funding_output(&tx, 0, &[0xa9], FundingAmount::AtLeast(1_001)).is_err());
assert_eq!(
check_funding_output(&tx, 0, &[0xa8], FundingAmount::AtLeast(1)).unwrap_err().message,
"funding output 0 does not pay the lock"
);
assert_eq!(
check_funding_output(&tx, 2, &[0xa9], FundingAmount::AtLeast(1)).unwrap_err().message,
"the funding has no output 2"
);
assert!(check_funding_output(&tx, 1, &[1], FundingAmount::AtLeast(0)).is_err());
}
}