use alloy::primitives::{Address, B256, U256};
use alloy::sol_types::SolEvent;
use super::{PaymentPool, to_pool_u64};
#[test]
fn pool_redeemed_signature_matches_contract() {
assert_eq!(
PaymentPool::PoolRedeemed::SIGNATURE,
"PoolRedeemed(bytes32,address,(address,uint64,uint64)[])"
);
}
#[test]
fn pool_redeemed_indexed_layout_matches_contract() {
let pool_id = B256::repeat_byte(0x11);
let provider = Address::repeat_byte(0x33);
let signer_a = Address::repeat_byte(0x22);
let signer_b = Address::repeat_byte(0x44);
let event = PaymentPool::PoolRedeemed {
poolId: pool_id,
provider,
lanes: vec![
PaymentPool::LaneSettled {
signer: signer_a,
newPaidCumulative: 3_000,
bytesPaid: 2_000,
},
PaymentPool::LaneSettled {
signer: signer_b,
newPaidCumulative: 7_000,
bytesPaid: 5_000,
},
],
};
let log = event.encode_log_data();
assert_eq!(log.topics().len(), 3);
assert_eq!(
log.topics().first(),
Some(&PaymentPool::PoolRedeemed::SIGNATURE_HASH)
);
assert_eq!(log.topics().get(1), Some(&pool_id));
assert_eq!(log.topics().get(2), Some(&provider.into_word()));
assert_eq!(log.data.len(), 32 * (2 + 3 * 2));
let decoded = PaymentPool::PoolRedeemed::decode_log_data(&log).unwrap();
assert_eq!(decoded.poolId, pool_id);
assert_eq!(decoded.provider, provider);
assert_eq!(decoded.lanes.len(), 2);
assert_eq!(decoded.lanes[0].signer, signer_a);
assert_eq!(decoded.lanes[0].newPaidCumulative, 3_000);
assert_eq!(decoded.lanes[0].bytesPaid, 2_000);
assert_eq!(decoded.lanes[1].signer, signer_b);
assert_eq!(decoded.lanes[1].newPaidCumulative, 7_000);
}
fn mocked(
calls: Vec<Option<alloy::primitives::Bytes>>,
) -> PaymentPool::PaymentPoolInstance<impl alloy::providers::Provider + Clone + 'static> {
use alloy::providers::ProviderBuilder;
use alloy::providers::mock::Asserter;
let asserter = Asserter::new();
for call in calls {
match call {
Some(response) => asserter.push_success(&response),
None => asserter.push_failure_msg("transient rpc fault"),
}
}
PaymentPool::new(
Address::ZERO,
ProviderBuilder::new().connect_mocked_client(asserter),
)
}
fn pool(status: PaymentPool::Status, deposit: u64, redeemed: u64) -> PaymentPool::Pool {
PaymentPool::Pool {
owner: Address::repeat_byte(1),
status,
disputeDeadline: 0,
deposit,
totalRedeemed: redeemed,
}
}
#[tokio::test]
async fn adopts_the_newer_of_two_solvent_pools() -> anyhow::Result<()> {
use alloy::sol_types::SolValue;
let older = B256::repeat_byte(0xAA);
let newer = B256::repeat_byte(0xBB);
let contract = mocked(vec![
Some(vec![older, newer].abi_encode().into()),
Some(
pool(PaymentPool::Status::Open, 10_000_000, 2_000_000)
.abi_encode()
.into(),
),
Some(
pool(PaymentPool::Status::Open, 10_000_000, 3_000_000)
.abi_encode()
.into(),
),
]);
let (id, _) = super::newest_solvent_owned_pool(&contract, Address::repeat_byte(1))
.await?
.ok_or_else(|| anyhow::anyhow!("two solvent open pools are on chain"))?;
anyhow::ensure!(id == newer, "the walk must go newest-first, got {id}");
Ok(())
}
#[tokio::test]
async fn adopts_the_newest_pool_that_can_still_pay() -> anyhow::Result<()> {
use alloy::sol_types::SolValue;
let older = B256::repeat_byte(0xAA);
let newer = B256::repeat_byte(0xBB);
let contract = mocked(vec![
Some(vec![older, newer].abi_encode().into()),
Some(
pool(PaymentPool::Status::Open, 10_000_000, 10_000_000)
.abi_encode()
.into(),
),
Some(
pool(PaymentPool::Status::Open, 10_000_000, 1_106_908)
.abi_encode()
.into(),
),
]);
let (id, adopted) = super::newest_solvent_owned_pool(&contract, Address::repeat_byte(1))
.await?
.ok_or_else(|| anyhow::anyhow!("a solvent open pool is on chain"))?;
anyhow::ensure!(id == older, "the fully-redeemed newer pool must be skipped");
anyhow::ensure!(adopted.totalRedeemed == 1_106_908);
Ok(())
}
#[tokio::test]
async fn answers_none_when_no_owned_pool_can_pay() -> anyhow::Result<()> {
use alloy::sol_types::SolValue;
let contract = mocked(vec![
Some(vec![B256::repeat_byte(0xAA)].abi_encode().into()),
Some(
pool(PaymentPool::Status::Closing, 10_000_000, 0)
.abi_encode()
.into(),
),
]);
anyhow::ensure!(
super::newest_solvent_owned_pool(&contract, Address::repeat_byte(1))
.await?
.is_none()
);
Ok(())
}
#[tokio::test]
async fn a_faulted_pool_read_is_an_error_not_an_empty_answer() {
use alloy::sol_types::SolValue;
let contract = mocked(vec![
Some(vec![B256::repeat_byte(0xAA)].abi_encode().into()),
None,
]);
assert!(
super::newest_solvent_owned_pool(&contract, Address::repeat_byte(1))
.await
.is_err(),
"\"could not tell\" must not become \"no pool, go open one\""
);
}
#[test]
fn to_pool_u64_accepts_the_ceiling_and_refuses_one_past_it() -> anyhow::Result<()> {
let ceiling = U256::from(u64::MAX);
assert_eq!(to_pool_u64(ceiling, "deposit")?, u64::MAX);
let over = ceiling + U256::from(1u8);
let Err(err) = to_pool_u64(over, "deposit") else {
anyhow::bail!("one base unit past the ceiling must be refused, not truncated");
};
assert!(
err.to_string().contains("deposit"),
"the error names the field that overflowed, got: {err}"
);
Ok(())
}
#[test]
fn read_surface_struct_layouts_match_contract() {
use alloy::sol_types::SolStruct;
assert_eq!(
PaymentPool::Pool::eip712_encode_type(),
"Pool(address owner,uint8 status,uint64 disputeDeadline,uint64 deposit,uint64 totalRedeemed)",
"getPool"
);
assert_eq!(
PaymentPool::Authorization::eip712_encode_type(),
"Authorization(uint64 cap,uint64 expiry,uint64 spent)",
"getAuthorization"
);
assert_eq!(
PaymentPool::Lane::eip712_encode_type(),
"Lane(uint64 amount,uint64 bytesDelivered)",
"getWatermark / getWatermarks"
);
}
#[test]
fn redeem_call_struct_layouts_match_contract() {
use alloy::sol_types::SolStruct;
assert_eq!(
PaymentPool::CapabilityReg::eip712_encode_type(),
"CapabilityReg(address signer,uint64 spendingCap,uint64 expiry,bytes ownerSig)"
);
assert_eq!(
PaymentPool::LaneVoucher::eip712_encode_type(),
"LaneVoucher(address signer,uint64 cumulative,uint64 bytesDelivered,bytes32 r,bytes32 vs,bytes32 chainRoot,bytes32 preimage,uint256 chainMeter)"
);
}
#[test]
fn lifecycle_event_signatures_match_contract() {
assert_eq!(
PaymentPool::PoolOpened::SIGNATURE,
"PoolOpened(bytes32,address,uint256)"
);
assert_eq!(
PaymentPool::PoolToppedUp::SIGNATURE,
"PoolToppedUp(bytes32,uint256,uint256)"
);
assert_eq!(
PaymentPool::PoolCloseInitiated::SIGNATURE,
"PoolCloseInitiated(bytes32,address,uint256)"
);
assert_eq!(
PaymentPool::PoolReclaimed::SIGNATURE,
"PoolReclaimed(bytes32,address,uint256)"
);
assert_eq!(
PaymentPool::RateBoundsUpdated::SIGNATURE,
"RateBoundsUpdated(uint256)"
);
}
#[test]
fn get_authorizations_signature_matches_contract() {
use alloy::sol_types::SolCall;
assert_eq!(
PaymentPool::getAuthorizationsCall::SIGNATURE,
"getAuthorizations(bytes32[],address[])"
);
}
#[test]
fn get_watermarks_signature_matches_contract() {
use alloy::sol_types::SolCall;
assert_eq!(
PaymentPool::getWatermarksCall::SIGNATURE,
"getWatermarks(bytes32[],address[],address[])"
);
}
#[test]
fn signer_authorization_reads_registration_from_cap_or_expiry() {
use super::SignerAuthorization;
let row = |cap, expiry, spent| PaymentPool::Authorization { cap, expiry, spent };
assert_eq!(
SignerAuthorization::from_onchain(&row(0, 0, 0)),
SignerAuthorization::Unregistered
);
assert_eq!(
SignerAuthorization::from_onchain(&row(0, 9, 0)),
SignerAuthorization::Registered {
cap: 0,
expiry: 9,
spent: 0
}
);
}
#[test]
fn signer_authorization_covers_a_floor_within_headroom_and_expiry() {
use super::SignerAuthorization;
let auth = SignerAuthorization::Registered {
cap: 40,
expiry: 100,
spent: 30,
};
assert!(auth.covers(U256::from(10u64), 99));
assert!(!auth.covers(U256::from(11u64), 99));
assert!(!auth.covers(U256::from(1u64), 100), "expired at its expiry");
assert!(SignerAuthorization::Unregistered.covers(U256::MAX, u64::MAX));
}