use super::*;
use alloy::primitives::Bytes;
use decdn_incentive::buyer_pool_redb::RedbBuyerPoolStore;
use crate::Cumulative;
use crate::buyer_pool::OpenUnconfirmed;
use alloy::providers::ProviderBuilder;
use alloy::providers::mock::Asserter;
use alloy::sol_types::SolValue;
use std::assert_matches;
const PP: Address = Address::repeat_byte(0x9c);
const DEPLOYMENT: Deployment = Deployment {
chain_id: 1,
payment_pool: PP,
};
const PROVIDER: Address = Address::repeat_byte(0x77);
const TOKEN: Address = Address::repeat_byte(0x22);
const WORKING: u64 = 10_000_000;
fn pool(owner: Address, deposit: u64, redeemed: u64) -> PaymentPool::Pool {
PaymentPool::Pool {
owner,
status: PaymentPool::Status::Open,
disputeDeadline: 0,
deposit,
totalRedeemed: redeemed,
}
}
fn still_open(owner: Address) -> Bytes {
pool(owner, WORKING, 0).abi_encode().into()
}
fn lane(amount: u64, bytes: u64) -> Bytes {
PaymentPool::Lane {
amount,
bytesDelivered: bytes,
}
.abi_encode()
.into()
}
async fn run(
store: &RedbBuyerPoolStore,
signer: &Arc<PrivateKeySigner>,
adoption: ChainAdoption,
calls: Vec<Option<Bytes>>,
) -> anyhow::Result<PoolContext> {
run_in(store, signer, adoption, calls, &RunFunding::default())
.await
.0
.map(|(ctx, _)| ctx)
}
async fn run_in(
store: &RedbBuyerPoolStore,
signer: &Arc<PrivateKeySigner>,
adoption: ChainAdoption,
calls: Vec<Option<Bytes>>,
funding: &RunFunding,
) -> (anyhow::Result<(PoolContext, LaneSpend)>, bool) {
let asserter = Asserter::new();
for call in calls {
match call {
Some(response) => asserter.push_success(&response),
None => asserter.push_failure_msg("transient rpc fault"),
}
}
let rpc = ProviderBuilder::new().connect_mocked_client(asserter.clone());
let contract = PaymentPool::new(PP, rpc.clone());
let result = open_or_reuse_pool(
store,
&contract,
&rpc,
funding,
signer,
PROVIDER,
signer.address(),
DEPLOYMENT,
DepositPolicy {
working_deposit: U256::from(WORKING),
max_approve: false,
adoption,
},
)
.await;
(result, asserter.read_q().is_empty())
}
fn client_store(dir: &tempfile::TempDir) -> RedbBuyerPoolStore {
RedbBuyerPoolStore::open(&dir.path().join("data")).unwrap()
}
#[tokio::test]
async fn a_lost_row_adopts_the_live_pool_and_resumes_from_the_chain_watermark() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let owner = signer.address();
let older = B256::repeat_byte(0xAA);
let newer = B256::repeat_byte(0xBB);
let ctx = run(
&store,
&signer,
ChainAdoption::Allowed,
vec![
Some(vec![older, newer].abi_encode().into()),
Some(pool(owner, 10_000_000, 10_000_000).abi_encode().into()),
Some(pool(owner, 10_000_000, 1_106_908).abi_encode().into()),
Some(TOKEN.abi_encode().into()),
Some(lane(1_106_908, 1_106_908_000)),
],
)
.await
.expect("a live, solvent pool is adopted — nothing is opened");
assert_eq!(ctx.pool_id, older);
assert_eq!(
(ctx.prior_bytes_delivered, ctx.prior_amount),
(U256::from(1_106_908_000u64), U256::from(1_106_908u64)),
"the lane resumes from the chain watermark; from zero, every voucher at or \
below it would redeem nothing"
);
let row = store
.get_by_owner(owner)
.unwrap()
.expect("adopted row is recorded");
assert_eq!(row.pool_id, older);
assert!(row.is_on(DEPLOYMENT));
}
#[tokio::test]
async fn a_faulted_enumeration_refuses_rather_than_opening() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let err = run(&store, &signer, ChainAdoption::Allowed, vec![None])
.await
.unwrap_err();
assert!(
format!("{err:#}").contains("refusing to open one blind"),
"expected the deliberate refusal, got: {err:#}"
);
assert!(store.get_by_owner(signer.address()).unwrap().is_none());
}
#[tokio::test]
async fn a_tracked_pool_resumes_a_new_lane_from_the_chain_watermark() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let id = B256::repeat_byte(0xCC);
store
.record(&BuyerPoolState::new(
id,
DEPLOYMENT,
signer.address(),
TOKEN,
U256::from(WORKING),
))
.unwrap();
let ctx = run(
&store,
&signer,
ChainAdoption::Allowed,
vec![Some(still_open(signer.address())), Some(lane(500, 500_000))],
)
.await
.expect("a tracked pool reuses without touching getPools");
assert_eq!(ctx.pool_id, id);
assert_eq!(
(ctx.prior_bytes_delivered, ctx.prior_amount),
(U256::from(500_000u64), U256::from(500u64))
);
}
#[tokio::test]
async fn a_row_from_another_deployment_is_not_reused() {
let signer = Arc::new(PrivateKeySigner::random());
let rpc = ProviderBuilder::new().connect_mocked_client(Asserter::new());
let contract = PaymentPool::new(PP, rpc);
let other_address = Deployment {
payment_pool: Address::repeat_byte(0xDE),
..DEPLOYMENT
};
let other_chain = Deployment {
chain_id: 421_614,
..DEPLOYMENT
};
for (foreign, reused) in [
(other_address, false),
(other_chain, false),
(DEPLOYMENT, true),
] {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let id = B256::repeat_byte(0xCC);
store
.record(&BuyerPoolState::new(
id,
foreign,
signer.address(),
TOKEN,
U256::from(WORKING),
))
.unwrap();
let tracked = pool_to_reuse(
&store,
&contract,
&RunFunding::default(),
signer.address(),
DEPLOYMENT,
ChainAdoption::Refused,
)
.await
.expect("a refused adoption reads nothing from the chain");
assert_eq!(
tracked.as_ref().map(|state| state.pool_id),
reused.then_some(id),
"row on {foreign:?}, buying on {DEPLOYMENT:?}"
);
assert!(tracked.is_none_or(|state| state.redeemed_elsewhere().is_zero()));
}
}
#[tokio::test]
async fn a_tracked_pool_is_reused_only_while_the_chain_shows_it_open() {
let signer = Arc::new(PrivateKeySigner::random());
let owner = signer.address();
let id = B256::repeat_byte(0xCC);
let read = |owner: Address, status: PaymentPool::Status| -> Option<Bytes> {
let mut answer = pool(owner, WORKING, 0);
answer.status = status;
Some(answer.abi_encode().into())
};
let cases = [
("open", read(owner, PaymentPool::Status::Open), true),
("closing", read(owner, PaymentPool::Status::Closing), false),
("closed", read(owner, PaymentPool::Status::Closed), false),
("read fault", None, true),
(
"no record",
read(Address::ZERO, PaymentPool::Status::Open),
true,
),
(
"another owner",
read(Address::repeat_byte(0x99), PaymentPool::Status::Closed),
true,
),
];
for (case, answer, reused) in cases {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
store
.record(&BuyerPoolState::new(
id,
DEPLOYMENT,
owner,
TOKEN,
U256::from(WORKING),
))
.unwrap();
let asserter = Asserter::new();
match answer {
Some(response) => asserter.push_success(&response),
None => asserter.push_failure_msg("transient rpc fault"),
}
let contract = PaymentPool::new(
PP,
ProviderBuilder::new().connect_mocked_client(asserter.clone()),
);
let funding = RunFunding::default();
let tracked = pool_to_reuse(
&store,
&contract,
&funding,
owner,
DEPLOYMENT,
ChainAdoption::Refused,
)
.await
.unwrap();
assert_eq!(
tracked.map(|state| state.pool_id),
reused.then_some(id),
"{case}"
);
assert_eq!(funding.pool_id(), reused.then_some(id), "{case}");
assert!(asserter.read_q().is_empty(), "{case}: the status was read");
}
}
#[tokio::test]
async fn a_node_data_dir_never_adopts_the_daemons_pool() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let err = run(
&store,
&signer,
ChainAdoption::Refused,
vec![Some(TOKEN.abi_encode().into())],
)
.await
.unwrap_err();
assert!(
!format!("{err:#}").contains("already owns a pool"),
"a node dir went down the adoption path: {err:#}"
);
assert!(
store.get_by_owner(signer.address()).unwrap().is_none(),
"nothing may be adopted into a node dir's client store"
);
}
const LIVE_LANES: [u64; 6] = [2_374_000, 2_360_000, 2_147_000, 1_043_000, 483_000, 248_000];
fn tracked_row(id: B256, owner: Address, deposit: u64, amounts: &[u64]) -> BuyerPoolState {
let lanes = amounts
.iter()
.zip(0u8..)
.map(|(&amount, i)| {
let provider = if i == 0 {
PROVIDER
} else {
Address::repeat_byte(0x40 + i)
};
(
lane_key(id, owner, provider),
decdn_incentive::BuyerLaneProgress {
last_amount: U256::from(amount),
last_bytes: U256::from(amount) * U256::from(1000u64),
},
)
})
.collect();
BuyerPoolState::hydrate(
id,
DEPLOYMENT,
owner,
TOKEN,
U256::from(deposit),
lanes,
U256::ZERO,
)
}
fn lane_key(id: B256, owner: Address, provider: Address) -> LaneKey {
LaneKey {
pool_id: id,
signer: owner,
provider,
}
}
fn refill_fails_with_wallet(usdc: u64) -> Vec<Option<Bytes>> {
vec![None, Some(U256::from(usdc).abi_encode().into())]
}
#[test]
fn pool_spend_sums_every_tracked_lane() {
let owner = Address::repeat_byte(0x01);
let id = B256::repeat_byte(0xEE);
let row = tracked_row(id, owner, WORKING, &LIVE_LANES);
let working = U256::from(WORKING);
let low_water = working / U256::from(LOW_WATER_DIVISOR);
let spent = row.pool_spend();
assert_eq!(spent, U256::from(8_655_000u64));
assert_eq!(
refill_amount(row.deposit, spent, working, low_water),
U256::from(8_655_000u64),
"the refill restores the 1.345 USDC remaining to the 10 USDC working deposit"
);
for &one_lane in &LIVE_LANES {
assert!(
refill_amount(row.deposit, U256::from(one_lane), working, low_water).is_zero(),
"no single lane reaches the low water, so the spend must be the sum over \
every lane"
);
}
}
#[test]
fn a_seeded_lanes_chain_prior_counts_toward_the_pool_spend() {
let owner = Address::repeat_byte(0x01);
let id = B256::repeat_byte(0xEE);
let mut row = tracked_row(id, owner, WORKING, &[1_000]);
let new_lane = lane_key(id, owner, Address::repeat_byte(0x55));
row.seed_lane(new_lane, U256::from(250_000u64), U256::from(250u64))
.unwrap();
assert_eq!(row.pool_spend(), U256::from(1_250u64));
row.seed_lane(
lane_key(id, owner, PROVIDER),
U256::from(1_000_000u64),
U256::from(1_000u64),
)
.unwrap();
assert_eq!(row.pool_spend(), U256::from(1_250u64));
}
#[test]
fn pool_spend_keeps_an_adopted_pools_redeemed_spend_as_its_lanes_advance() {
let owner = Address::repeat_byte(0x01);
let id = B256::repeat_byte(0xEE);
let adopted = |redeemed: u64| {
BuyerPoolState::adopt(
id,
DEPLOYMENT,
owner,
TOKEN,
U256::from(WORKING),
U256::from(redeemed),
)
};
assert_eq!(adopted(9_500_000).pool_spend(), U256::from(9_500_000u64));
let a = lane_key(id, owner, PROVIDER);
let mut row = adopted(100);
row.seed_lane(a, U256::from(60_000u64), U256::from(60u64))
.unwrap();
assert_eq!(row.redeemed_elsewhere(), U256::from(40u64));
assert_eq!(row.pool_spend(), U256::from(100u64));
row.advance_lane(a, U256::from(80_000u64), U256::from(80u64))
.unwrap();
assert_eq!(row.pool_spend(), U256::from(120u64));
}
#[tokio::test]
async fn a_pool_spent_across_lanes_refills_and_fails_when_nothing_is_left() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let id = B256::repeat_byte(0xEE);
store
.record(&tracked_row(id, signer.address(), WORKING, &[2_000_000; 5]))
.unwrap();
let mut calls = vec![Some(still_open(signer.address()))];
calls.extend(refill_fails_with_wallet(0));
let (result, drained) = run_in(
&store,
&signer,
ChainAdoption::Allowed,
calls,
&RunFunding::default(),
)
.await;
let err = result.expect_err("a pool with no unspent deposit cannot pay");
assert!(
format!("{err:#}").contains("has no unspent deposit, and the wallet cannot fund"),
"expected the refill failure, got: {err:#}"
);
assert!(
err.downcast_ref::<NoAffordableSource>().is_some(),
"the lane build ends the command rather than retrying: {err:#}"
);
assert!(
drained,
"the refill read the allowance and the wallet balance"
);
}
#[tokio::test]
async fn an_untracked_lanes_chain_watermark_counts_toward_the_pool_spend() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let id = B256::repeat_byte(0xEE);
let other = lane_key(id, signer.address(), Address::repeat_byte(0x55));
let progress = decdn_incentive::BuyerLaneProgress {
last_amount: U256::from(9_000_000u64),
last_bytes: U256::from(9_000_000_000u64),
};
store
.record(&BuyerPoolState::hydrate(
id,
DEPLOYMENT,
signer.address(),
TOKEN,
U256::from(WORKING),
vec![(other, progress)],
U256::ZERO,
))
.unwrap();
let mut calls = vec![
Some(still_open(signer.address())),
Some(lane(1_000_000, 1_000_000_000)),
];
calls.extend(refill_fails_with_wallet(0));
let (result, drained) = run_in(
&store,
&signer,
ChainAdoption::Allowed,
calls,
&RunFunding::default(),
)
.await;
let err = result.expect_err("the untracked lane's watermark spends the rest");
assert!(
format!("{err:#}").contains("has no unspent deposit"),
"{err:#}"
);
assert!(drained);
}
#[tokio::test]
async fn a_wallet_short_of_usdc_keeps_the_lane_while_the_pool_can_still_pay() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let id = B256::repeat_byte(0xEE);
store
.record(&tracked_row(id, signer.address(), WORKING, &LIVE_LANES))
.unwrap();
let funding = RunFunding::default();
let (result, drained) = run_in(
&store,
&signer,
ChainAdoption::Allowed,
[
vec![Some(still_open(signer.address()))],
refill_fails_with_wallet(0),
]
.concat(),
&funding,
)
.await;
let (ctx, _) = result.expect("1.345 USDC is left, so a short wallet must not drop the lane");
assert!(drained, "the refill fired and read the wallet balance");
assert_eq!(ctx.pool_id, id);
assert_eq!(
(ctx.prior_bytes_delivered, ctx.prior_amount),
(U256::from(LIVE_LANES[0] * 1000), U256::from(LIVE_LANES[0])),
"the lane resumes from its own recorded progress"
);
let row = store.get_by_owner(signer.address()).unwrap().unwrap();
assert_eq!(row.deposit, U256::from(WORKING), "nothing was credited");
assert_eq!(row.committed_amount(), U256::from(8_655_000u64));
let shortfall = funding.shortfall().expect("the shortfall is recorded");
assert!(shortfall.contains("holds 0 µUSDC"), "{shortfall}");
assert!(shortfall.contains("8655000 µUSDC top-up"), "{shortfall}");
}
#[tokio::test]
async fn a_later_lane_build_skips_the_refill_after_a_wallet_shortfall() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let id = B256::repeat_byte(0xEE);
store
.record(&tracked_row(id, signer.address(), WORKING, &LIVE_LANES))
.unwrap();
let funding = RunFunding::default();
let (first, _) = run_in(
&store,
&signer,
ChainAdoption::Allowed,
[
vec![Some(still_open(signer.address()))],
refill_fails_with_wallet(0),
]
.concat(),
&funding,
)
.await;
first.expect("the first build records the shortfall and keeps its lane");
let (later, _) = run_in(&store, &signer, ChainAdoption::Allowed, vec![], &funding).await;
later.expect("the later build reads nothing from the chain");
}
#[tokio::test]
async fn a_refill_failure_with_a_funded_wallet_fails_the_lane_build() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let id = B256::repeat_byte(0xEE);
store
.record(&tracked_row(id, signer.address(), WORKING, &LIVE_LANES))
.unwrap();
let funding = RunFunding::default();
let (result, drained) = run_in(
&store,
&signer,
ChainAdoption::Allowed,
[
vec![Some(still_open(signer.address()))],
refill_fails_with_wallet(WORKING),
]
.concat(),
&funding,
)
.await;
let err = result.expect_err("a funded wallet's failed refill is retried, not skipped");
assert!(
format!("{err:#}").contains("read USDC allowance"),
"{err:#}"
);
assert!(drained);
assert!(funding.shortfall().is_none());
}
#[tokio::test]
async fn an_adopted_pool_drained_by_other_lanes_refills_and_buys_on_its_remainder() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let owner = signer.address();
let id = B256::repeat_byte(0xDD);
let mut calls = vec![
Some(vec![id].abi_encode().into()),
Some(pool(owner, 10_000_000, 9_500_000).abi_encode().into()),
Some(TOKEN.abi_encode().into()),
Some(lane(0, 0)),
];
calls.extend(refill_fails_with_wallet(0));
let funding = RunFunding::default();
let (result, drained) = run_in(&store, &signer, ChainAdoption::Allowed, calls, &funding).await;
let (ctx, spend) = result.expect("0.5 USDC is left, so a short wallet must not drop the lane");
assert!(
drained,
"0.5 USDC left is below the 2 USDC low water, so the refill must fire"
);
assert_eq!(ctx.pool_id, id);
assert_eq!(
spend,
LaneSpend {
outside: U256::from(9_500_000u64),
prior: U256::ZERO,
recorded: false,
from_chain: true,
}
);
}
#[tokio::test]
async fn a_reused_adopted_row_keeps_the_pools_redeemed_spend() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let owner = signer.address();
let id = B256::repeat_byte(0xDE);
let lane_spend = |outside: u64, recorded: bool| LaneSpend {
outside: U256::from(outside),
prior: U256::from(1_000_000u64),
recorded,
from_chain: true,
};
let (adopting, drained) = run_in(
&store,
&signer,
ChainAdoption::Allowed,
vec![
Some(vec![id].abi_encode().into()),
Some(pool(owner, 10_000_000, 3_000_000).abi_encode().into()),
Some(TOKEN.abi_encode().into()),
Some(lane(1_000_000, 1_000_000_000)),
],
&RunFunding::default(),
)
.await;
assert!(drained);
assert_eq!(
adopting.expect("the live pool is adopted").1,
lane_spend(2_000_000, false)
);
let row = store
.get_by_owner(owner)
.unwrap()
.expect("adopted row is recorded");
assert_eq!(row.redeemed_elsewhere(), U256::from(2_000_000u64));
assert_eq!(row.pool_spend(), U256::from(3_000_000u64));
let (reusing, drained) = run_in(
&store,
&signer,
ChainAdoption::Allowed,
vec![],
&RunFunding::default(),
)
.await;
assert!(drained);
let (ctx, spend) = reusing.expect("the tracked row is reused");
assert_eq!(ctx.pool_id, id);
assert_eq!(
spend,
lane_spend(2_000_000, true),
"a later run sees the redeemed spend the adopting run saw"
);
let a = lane_key(id, owner, PROVIDER);
let outcome = store
.advance_progress(
owner,
id,
a,
U256::from(1_500_000_000u64),
U256::from(1_500_000u64),
)
.unwrap();
assert_matches!(outcome, AdvanceOutcome::Advanced);
assert_eq!(
store.get_by_owner(owner).unwrap().unwrap().pool_spend(),
U256::from(3_500_000u64)
);
}
fn ledger_at(amount: u64) -> Arc<PoolLedger> {
Arc::new(PoolLedger::new(Cumulative {
bytes: U256::from(amount) * U256::from(1000u64),
amount: U256::from(amount),
}))
}
fn spend(outside: u64, prior: u64, recorded: bool, from_chain: bool) -> LaneSpend {
LaneSpend {
outside: U256::from(outside),
prior: U256::from(prior),
recorded,
from_chain,
}
}
#[test]
fn run_funding_counts_every_lane_once_on_a_tracked_pool() {
let id = B256::repeat_byte(0xEE);
let owner = Address::repeat_byte(0x01);
let funding = RunFunding::default();
assert_eq!(funding.pool_spent(), None);
let first = lane_key(id, owner, PROVIDER);
let first_ledger = ledger_at(2_374_000);
funding.join_lane(
first,
spend(6_281_000, 2_374_000, true, false),
&first_ledger,
);
assert_eq!(funding.pool_spent(), Some(U256::from(8_655_000u64)));
let second = lane_key(id, owner, Address::repeat_byte(0x41));
funding.join_lane(
second,
spend(0, 2_360_000, true, false),
&ledger_at(2_360_000),
);
assert_eq!(funding.pool_spent(), Some(U256::from(8_655_000u64)));
let fresh = lane_key(id, owner, Address::repeat_byte(0x55));
funding.join_lane(fresh, spend(0, 100, false, false), &ledger_at(100));
assert_eq!(funding.pool_spent(), Some(U256::from(8_655_100u64)));
funding.join_lane(first, spend(0, 6_281_000, true, false), &first_ledger);
assert_eq!(funding.pool_spent(), Some(U256::from(8_655_100u64)));
}
#[test]
fn run_funding_counts_every_lane_once_on_an_adopted_pool() {
let id = B256::repeat_byte(0xEE);
let owner = Address::repeat_byte(0x01);
let funding = RunFunding::default();
funding.join_lane(
lane_key(id, owner, PROVIDER),
spend(9_000_000, 500_000, false, true),
&ledger_at(500_000),
);
assert_eq!(funding.pool_spent(), Some(U256::from(9_500_000u64)));
funding.join_lane(
lane_key(id, owner, Address::repeat_byte(0x41)),
spend(0, 1_000_000, false, false),
&ledger_at(1_000_000),
);
assert_eq!(funding.pool_spent(), Some(U256::from(9_500_000u64)));
}
#[test]
fn run_funding_follows_each_lanes_largest_ledger() {
let id = B256::repeat_byte(0xEE);
let owner = Address::repeat_byte(0x01);
let funding = RunFunding::default();
let lane = lane_key(id, owner, PROVIDER);
let paid = ledger_at(0);
funding.join_lane(lane, spend(1_000, 0, false, false), &paid);
assert!(paid.reseed(Cumulative {
bytes: U256::from(300_000u64),
amount: U256::from(300u64),
}));
assert_eq!(funding.pool_spent(), Some(U256::from(1_300u64)));
funding.join_lane(lane, spend(1_000, 0, false, false), &ledger_at(0));
assert_eq!(funding.pool_spent(), Some(U256::from(1_300u64)));
}
#[tokio::test]
async fn a_lane_build_reports_the_pools_other_spend() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let id = B256::repeat_byte(0xEE);
store
.record(&tracked_row(id, signer.address(), 2 * WORKING, &LIVE_LANES))
.unwrap();
let (result, drained) = run_in(
&store,
&signer,
ChainAdoption::Allowed,
vec![],
&RunFunding::default(),
)
.await;
let (_, spend) = result.expect("11.345 USDC is left, so no refill fires");
assert!(drained);
assert_eq!(
spend,
LaneSpend {
outside: U256::from(8_655_000u64 - LIVE_LANES[0]),
prior: U256::from(LIVE_LANES[0]),
recorded: true,
from_chain: false,
}
);
}
#[tokio::test]
async fn the_cli_funder_reads_the_run_funding() {
let signer = Arc::new(PrivateKeySigner::random());
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let rpc = ProviderBuilder::new().connect_mocked_client(Asserter::new());
let contract = PaymentPool::new(PP, rpc.clone());
let funding = RunFunding::default();
funding.join_lane(
lane_key(
B256::repeat_byte(0xEE),
Address::repeat_byte(0x01),
PROVIDER,
),
spend(700, 0, false, false),
&ledger_at(0),
);
funding.note_pool(B256::repeat_byte(0xEE));
let funder = PoolFunder {
contract: &contract,
rpc: &rpc,
store: &store,
owner: Address::repeat_byte(0x01),
signer: Arc::clone(&signer),
deployment: DEPLOYMENT,
token: TOKEN,
payment_pool_addr: PP,
working_deposit: U256::from(WORKING),
max_approve: false,
funding: &funding,
};
assert_eq!(funder.pool_spent(), Some(U256::from(700u64)));
let err = funder.recover(U256::from(5u64)).await.unwrap_err();
assert!(funding.shortfall().is_none(), "{err:#}");
assert!(err.downcast_ref::<WalletShortfall>().is_none(), "{err:#}");
*funding.shortfall.lock().unwrap() = Some("wallet 0x01 holds 0 µUSDC".to_owned());
let err = funder.recover(U256::from(5u64)).await.unwrap_err();
assert!(
format!("{err:#}").contains("the wallet cannot fund a top-up: wallet 0x01 holds 0"),
"{err:#}"
);
assert!(err.downcast_ref::<WalletShortfall>().is_some(), "{err:#}");
let delegated = PoolFunder {
working_deposit: U256::ZERO,
..funder
};
assert_matches!(
delegated.recover(U256::ZERO).await,
Ok(Recovery::Unavailable)
);
}
async fn step_on_a_full_pool(
asserter: Asserter,
) -> (anyhow::Result<Recovery>, Vec<OpenUnconfirmed>) {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let id = B256::repeat_byte(0xEE);
store
.record(&tracked_row(id, signer.address(), WORKING, &[]))
.unwrap();
let rpc = ProviderBuilder::new().connect_mocked_client(asserter);
let contract = PaymentPool::new(PP, rpc.clone());
let opens = UnconfirmedOpens::default();
let funding = RunFunding::reporting_opens_to(opens.clone());
funding.note_pool(id);
let funder = PoolFunder {
contract: &contract,
rpc: &rpc,
store: &store,
owner: signer.address(),
signer: Arc::clone(&signer),
deployment: DEPLOYMENT,
token: TOKEN,
payment_pool_addr: PP,
working_deposit: U256::from(WORKING),
max_approve: false,
funding: &funding,
};
(funder.recover(U256::from(WORKING)).await, opens.pending())
}
#[tokio::test]
async fn a_full_open_pool_settles_without_a_top_up() {
let asserter = Asserter::new();
asserter.push_success(&alloy::primitives::U64::from(50_000u64));
let stepped = step_on_a_full_pool(asserter.clone()).await.0.unwrap();
assert_matches!(stepped, Recovery::ToppedUp(d) if d == U256::from(WORKING));
assert!(asserter.read_q().is_empty(), "only the estimate ran");
}
#[tokio::test]
async fn a_full_closing_pool_is_replaced() {
let selector = alloy::primitives::keccak256("PoolNotOpen()");
let asserter = Asserter::new();
asserter.push_failure(
serde_json::from_value::<alloy_json_rpc::ErrorPayload>(serde_json::json!({
"code": 3,
"message": "execution reverted",
"data": format!("0x{}", alloy::primitives::hex::encode(&selector[..4])),
}))
.unwrap(),
);
asserter.push_success(&U256::MAX.abi_encode());
asserter.push_success(&U256::ZERO);
let (stepped, opens) = step_on_a_full_pool(asserter.clone()).await;
let err = stepped.expect_err("the replacement open reaches the chain");
assert!(
format!("{err:#}").contains("submit openPool"),
"the step submitted an openPool, not a topUp: {err:#}"
);
assert!(
asserter.read_q().is_empty(),
"the estimate and the allowance ran"
);
assert_eq!(opens, vec![OpenUnconfirmed { tx: None, nonce: 0 }]);
}
#[tokio::test]
async fn a_lane_build_open_is_held_in_the_runs_unconfirmed_opens() {
let dir = tempfile::tempdir().unwrap();
let store = client_store(&dir);
let signer = Arc::new(PrivateKeySigner::random());
let asserter = Asserter::new();
asserter.push_success(&Bytes::from(TOKEN.abi_encode()));
asserter.push_success(&U256::MAX.abi_encode());
asserter.push_success(&U256::from(3u64));
let rpc = ProviderBuilder::new().connect_mocked_client(asserter.clone());
let contract = PaymentPool::new(PP, rpc.clone());
let opens = UnconfirmedOpens::default();
let funding = RunFunding::reporting_opens_to(opens.clone());
let err = open_or_reuse_pool(
&store,
&contract,
&rpc,
&funding,
&signer,
PROVIDER,
signer.address(),
DEPLOYMENT,
DepositPolicy {
working_deposit: U256::from(WORKING),
max_approve: false,
adoption: ChainAdoption::Refused,
},
)
.await
.expect_err("the open's submit fails in transport");
assert!(format!("{err:#}").contains("submit openPool"), "{err:#}");
assert_eq!(
opens.pending(),
vec![OpenUnconfirmed { tx: None, nonce: 3 }]
);
}
#[tokio::test]
async fn a_delegated_lane_resumes_from_its_delegate_record() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let store = RedbBuyerPoolStore::open(&dir.path().join("data"))?;
let owner = Arc::new(PrivateKeySigner::random());
let delegate = Arc::new(PrivateKeySigner::random());
let pool_id = PoolId::repeat_byte(0x11);
let signed = decdn_incentive::Capability {
signer: delegate.address(),
spending_cap: 1_000_000,
pool_id,
expiry: u64::MAX,
}
.sign(owner.as_ref(), &bind_node_id_domain(1, Address::ZERO))?;
let grant = CapabilityGrant::from_signed_capability(&signed);
let lane = LaneKey {
pool_id,
signer: delegate.address(),
provider: PROVIDER,
};
let foreign = Deployment {
chain_id: 2,
..DEPLOYMENT
};
let elsewhere = LaneKey {
provider: Address::repeat_byte(0x78),
..lane
};
let outcome = store.advance_delegate_progress(
DEPLOYMENT,
elsewhere,
decdn_incentive::BuyerLaneProgress {
last_amount: U256::from(500u64),
last_bytes: U256::from(50_000u64),
},
)?;
anyhow::ensure!(outcome == AdvanceOutcome::Advanced);
for (deployment, bytes, amount) in [(DEPLOYMENT, 4_096u64, 40u64), (foreign, 9_999, 99)] {
let outcome = store.advance_delegate_progress(
deployment,
lane,
decdn_incentive::BuyerLaneProgress {
last_amount: U256::from(amount),
last_bytes: U256::from(bytes),
},
)?;
anyhow::ensure!(outcome == AdvanceOutcome::Advanced);
}
let ctx = delegated_ctx(&store, owner.address(), &delegate, &grant).await?;
anyhow::ensure!(
(ctx.prior_bytes_delivered, ctx.prior_amount) == (U256::from(4_096u64), U256::from(40u64)),
"resumed at ({}, {})",
ctx.prior_bytes_delivered,
ctx.prior_amount
);
Ok(())
}
async fn delegated_ctx(
store: &dyn BuyerPoolStore,
owner: Address,
delegate: &Arc<PrivateKeySigner>,
grant: &CapabilityGrant,
) -> anyhow::Result<PoolContext> {
let asserter = Asserter::new();
asserter.push_success(&pool(owner, WORKING, 0).abi_encode());
asserter.push_success(&TOKEN.abi_encode());
let rpc = ProviderBuilder::new().connect_mocked_client(asserter);
let contract = PaymentPool::new(PP, rpc);
let chain = ChainConfig {
rpc_url: "http://config:8545".to_string(),
chain_id: DEPLOYMENT.chain_id,
payment_pool: PP,
slash_judge: Address::repeat_byte(0x44),
capacity_bond: None,
};
let endpoint = Endpoint::builder(iroh::endpoint::presets::Minimal)
.relay_mode(iroh::RelayMode::Disabled)
.bind()
.await?;
let ctx = build_delegated_pool_ctx(
store,
&contract,
delegate,
&DEPLOYMENT.voucher_domain(),
PROVIDER,
&chain,
&endpoint,
grant,
)
.await;
endpoint.close().await;
ctx
}
#[derive(Debug, Default)]
struct UnreadableDelegateLanes(decdn_incentive::MemoryBuyerPoolStore);
impl BuyerPoolStore for UnreadableDelegateLanes {
fn load_all(&self) -> Result<decdn_incentive::BuyerLoad, decdn_incentive::StoreError> {
self.0.load_all()
}
fn record(&self, state: &BuyerPoolState) -> Result<(), decdn_incentive::StoreError> {
self.0.record(state)
}
fn forget(&self, owner: Address) -> Result<(), decdn_incentive::StoreError> {
self.0.forget(owner)
}
fn get_by_pool_id(
&self,
pool_id: PoolId,
) -> Result<Option<BuyerPoolState>, decdn_incentive::StoreError> {
self.0.get_by_pool_id(pool_id)
}
fn forget_if_pool(
&self,
owner: Address,
pool_id: PoolId,
) -> Result<bool, decdn_incentive::StoreError> {
self.0.forget_if_pool(owner, pool_id)
}
fn get_by_owner(
&self,
owner: Address,
) -> Result<Option<BuyerPoolState>, decdn_incentive::StoreError> {
self.0.get_by_owner(owner)
}
fn advance_progress(
&self,
owner: Address,
pool_id: PoolId,
lane: LaneKey,
bytes: U256,
amount: U256,
) -> Result<AdvanceOutcome, decdn_incentive::StoreError> {
self.0.advance_progress(owner, pool_id, lane, bytes, amount)
}
fn rebase_progress(
&self,
owner: Address,
pool_id: PoolId,
lane: LaneKey,
anchor: decdn_incentive::BuyerLaneProgress,
totals: decdn_incentive::BuyerLaneProgress,
) -> Result<AdvanceOutcome, decdn_incentive::StoreError> {
self.0.rebase_progress(owner, pool_id, lane, anchor, totals)
}
fn seed_progress(
&self,
owner: Address,
pool_id: PoolId,
lane: LaneKey,
bytes: U256,
amount: U256,
) -> Result<AdvanceOutcome, decdn_incentive::StoreError> {
self.0.seed_progress(owner, pool_id, lane, bytes, amount)
}
fn add_deposit(
&self,
owner: Address,
pool_id: PoolId,
additional: U256,
) -> Result<decdn_incentive::DepositOutcome, decdn_incentive::StoreError> {
self.0.add_deposit(owner, pool_id, additional)
}
fn delegate_lanes(
&self,
_deployment: Deployment,
pool_id: PoolId,
_signer: Address,
) -> Result<Vec<(Address, decdn_incentive::BuyerLaneProgress)>, decdn_incentive::StoreError>
{
Err(decdn_incentive::StoreError::Corrupt {
pool_id: Some(pool_id),
detail: "delegate lane record damaged".into(),
})
}
fn advance_delegate_progress(
&self,
deployment: Deployment,
lane: LaneKey,
totals: decdn_incentive::BuyerLaneProgress,
) -> Result<AdvanceOutcome, decdn_incentive::StoreError> {
self.0.advance_delegate_progress(deployment, lane, totals)
}
fn rebase_delegate_progress(
&self,
deployment: Deployment,
lane: LaneKey,
anchor: decdn_incentive::BuyerLaneProgress,
totals: decdn_incentive::BuyerLaneProgress,
) -> Result<AdvanceOutcome, decdn_incentive::StoreError> {
self.0
.rebase_delegate_progress(deployment, lane, anchor, totals)
}
}
#[tokio::test]
async fn an_unreadable_delegate_record_fails_the_lane_build_fatally() -> anyhow::Result<()> {
let owner = Arc::new(PrivateKeySigner::random());
let delegate = Arc::new(PrivateKeySigner::random());
let signed = decdn_incentive::Capability {
signer: delegate.address(),
spending_cap: 1_000_000,
pool_id: PoolId::repeat_byte(0x11),
expiry: u64::MAX,
}
.sign(owner.as_ref(), &bind_node_id_domain(1, Address::ZERO))?;
let grant = CapabilityGrant::from_signed_capability(&signed);
let Err(err) = delegated_ctx(
&UnreadableDelegateLanes::default(),
owner.address(),
&delegate,
&grant,
)
.await
else {
anyhow::bail!("an unreadable delegate record built a lane");
};
let fault = crate::fault::classify(&anyhow::Error::new(crate::fault::LaneBuildFault(err)));
anyhow::ensure!(
matches!(
fault,
crate::fault::Fault::Fatal(crate::fault::FatalScope::Command)
),
"classified {fault:?}"
);
Ok(())
}