use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock, PoisonError};
use alloy::dyn_abi::Eip712Domain;
use alloy::primitives::{Address, B256, U256};
use alloy::signers::local::PrivateKeySigner;
use anyhow::Context as _;
use decdn_incentive::buyer_pool::{AdvanceOutcome, BuyerPoolState, BuyerPoolStore};
use decdn_incentive::payment_pool::{PaymentPool, newest_solvent_owned_pool};
use decdn_incentive::{CapabilityGrant, Deployment, LaneKey, PoolId, bind_node_id_domain};
use iroh::Endpoint;
use crate::buyer_pool::{
LOW_WATER_DIVISOR, PoolNotOpen, TopUpUnconfirmed, ToppedUpPool, UnconfirmedOpens,
WalletShortfall, ensure_allowance, escrowed_but_untracked, grade_deposit_credit, open_pool,
pool_accepts_funds, refill_amount, self_owned_lane_ctx, top_up, topped_up_effect,
};
use crate::lanes::LaneDeps;
use crate::session::{ChainAdoption, ChainConfig, DepositPolicy};
use crate::source::{Funder, SourceFuture};
use crate::{
NoAffordableSource, PoolContext, PoolLedger, PoolReplaced, Recovery, sign_client_binding,
};
const fn status_label(status: PaymentPool::Status) -> &'static str {
match status {
PaymentPool::Status::Open => "open",
PaymentPool::Status::Closing => "closing",
PaymentPool::Status::Closed => "closed",
_ => "?",
}
}
pub fn cached_pool_token(
store: &dyn BuyerPoolStore,
self_address: Address,
deployment: Deployment,
) -> anyhow::Result<Option<Address>> {
Ok(store
.get_by_owner(self_address)?
.filter(|state| state.is_on(deployment))
.map(|state| state.token))
}
#[derive(Debug, Default)]
pub struct RunFunding {
spend: Mutex<RunSpend>,
shortfall: Mutex<Option<String>>,
pool_id: OnceLock<PoolId>,
opens: UnconfirmedOpens,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LaneSpend {
pub(crate) outside: U256,
pub(crate) prior: U256,
pub(crate) recorded: bool,
pub(crate) from_chain: bool,
}
#[derive(Debug, Default)]
struct RunSpend {
baseline: Option<U256>,
from_chain: bool,
lanes: HashMap<LaneKey, Vec<Arc<PoolLedger>>>,
}
impl RunFunding {
#[must_use]
pub fn reporting_opens_to(opens: UnconfirmedOpens) -> Self {
Self {
opens,
..Self::default()
}
}
pub const fn opens(&self) -> &UnconfirmedOpens {
&self.opens
}
pub(crate) fn note_pool(&self, pool_id: PoolId) {
let _ = self.pool_id.set(pool_id);
}
pub fn pool_id(&self) -> Option<PoolId> {
self.pool_id.get().copied()
}
pub(crate) fn join_lane(&self, lane: LaneKey, spend: LaneSpend, ledger: &Arc<PoolLedger>) {
let mut run = self.spend.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(ledgers) = run.lanes.get_mut(&lane) {
if !ledgers.iter().any(|known| Arc::ptr_eq(known, ledger)) {
ledgers.push(Arc::clone(ledger));
}
return;
}
run.lanes.insert(lane, vec![Arc::clone(ledger)]);
run.baseline = Some(match run.baseline {
None => {
run.from_chain = spend.from_chain;
spend.outside
}
Some(baseline) if spend.recorded || run.from_chain => {
baseline.saturating_sub(spend.prior)
}
Some(baseline) => baseline,
});
}
pub(crate) fn pool_spent(&self) -> Option<U256> {
let run = self.spend.lock().unwrap_or_else(PoisonError::into_inner);
let baseline = run.baseline?;
Some(run.lanes.values().fold(baseline, |total, ledgers| {
let lane = ledgers
.iter()
.map(|ledger| ledger.committed().amount)
.max()
.unwrap_or(U256::ZERO);
total.saturating_add(lane)
}))
}
pub fn shortfall(&self) -> Option<String> {
self.shortfall
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
pub(crate) async fn check_wallet<P>(
&self,
rpc: &P,
token: Address,
owner: Address,
pool_id: PoolId,
additional: U256,
err: &anyhow::Error,
) -> Option<String>
where
P: alloy::providers::Provider + Clone,
{
let error = decdn_common::redact::sanitize_err_chain(err);
let balance = match decdn_incentive::Erc20::new(token, rpc.clone())
.balanceOf(owner)
.call()
.await
{
Ok(balance) => balance,
Err(read) => {
let read = decdn_common::redact::sanitize_err_chain(&anyhow::Error::new(read));
tracing::warn!(
%pool_id,
%additional,
%error,
wallet_usdc_error = %read,
"buyer pool top-up failed, and the wallet's USDC balance could not be read"
);
return None;
}
};
if balance >= additional {
tracing::warn!(
%pool_id,
%additional,
wallet_usdc = %balance,
%error,
"buyer pool top-up failed although the wallet holds enough USDC"
);
return None;
}
let warning = format!(
"wallet {owner} holds {balance} µUSDC, less than the {additional} µUSDC top-up \
buyer pool {pool_id} needs; fund the wallet so the pool can be topped up"
);
let mut shortfall = self
.shortfall
.lock()
.unwrap_or_else(PoisonError::into_inner);
if shortfall.is_none() {
tracing::warn!(
%pool_id,
%additional,
wallet_usdc = %balance,
%error,
"the wallet holds too little USDC to top up the buyer pool"
);
*shortfall = Some(warning.clone());
}
Some(warning)
}
}
pub struct PoolFunder<'a, P> {
pub(crate) contract: &'a PaymentPool::PaymentPoolInstance<P>,
pub(crate) rpc: &'a P,
pub(crate) store: &'a dyn BuyerPoolStore,
pub(crate) owner: Address,
pub(crate) signer: Arc<PrivateKeySigner>,
pub(crate) deployment: Deployment,
pub(crate) token: Address,
pub(crate) payment_pool_addr: Address,
pub(crate) working_deposit: U256,
pub(crate) max_approve: bool,
pub(crate) funding: &'a RunFunding,
}
impl<P> std::fmt::Debug for PoolFunder<'_, P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PoolFunder")
.field("owner", &self.owner)
.field("deployment", &self.deployment)
.field("working_deposit", &self.working_deposit)
.finish_non_exhaustive()
}
}
impl<P> Funder for PoolFunder<'_, P>
where
P: alloy::providers::Provider + Clone,
{
fn pool_spent(&self) -> Option<U256> {
self.funding.pool_spent()
}
fn recover(&self, remaining: U256) -> SourceFuture<'_, Recovery> {
Box::pin(async move {
if self.working_deposit.is_zero() {
return Ok(Recovery::Unavailable);
}
let pool_id = self
.funding
.pool_id()
.ok_or_else(|| anyhow::anyhow!("no payment lane is built, so no pool to top up"))?;
let additional = self.working_deposit.saturating_sub(remaining);
if additional.is_zero() {
return self.settle_or_replace(pool_id, remaining).await;
}
if let Some(shortfall) = self.funding.shortfall() {
return Err(
anyhow::anyhow!("the wallet cannot fund a top-up: {shortfall}")
.context(WalletShortfall),
);
}
let escrowed = async {
ensure_allowance(
self.rpc,
self.token,
self.owner,
self.payment_pool_addr,
if self.max_approve {
None
} else {
Some(additional)
},
)
.await?;
top_up(self.contract, self.owner, pool_id, additional).await
}
.await;
let ToppedUpPool { credited, tx, .. } = match escrowed {
Ok(topped_up) => topped_up,
Err(err) if err.downcast_ref::<TopUpUnconfirmed>().is_some() => return Err(err),
Err(err) if err.downcast_ref::<PoolNotOpen>().is_some() => {
tracing::info!(
%pool_id,
"the current buyer pool no longer accepts funds; opening a new pool"
);
let opened = self.open_replacement().await?;
return Ok(Recovery::Replaced(PoolReplaced {
closed: pool_id,
opened,
}));
}
Err(err) => {
let short = self
.funding
.check_wallet(self.rpc, self.token, self.owner, pool_id, additional, &err)
.await;
return Err(match short {
Some(_) => err.context(WalletShortfall),
None => err,
});
}
};
let effect = topped_up_effect(pool_id, credited);
let new_deposit = grade_deposit_credit(
self.store.add_deposit(self.owner, pool_id, credited),
&effect,
tx,
)?;
Ok(Recovery::ToppedUp(new_deposit))
})
}
}
impl<P> PoolFunder<'_, P>
where
P: alloy::providers::Provider + Clone,
{
async fn settle_or_replace(
&self,
pool_id: PoolId,
remaining: U256,
) -> anyhow::Result<Recovery> {
if !pool_accepts_funds(self.contract, self.owner, pool_id).await {
tracing::info!(
%pool_id,
"the current buyer pool holds its working deposit but accepts no more funds; \
opening a new pool"
);
let opened = self.open_replacement().await?;
return Ok(Recovery::Replaced(PoolReplaced {
closed: pool_id,
opened,
}));
}
let estimate = || remaining.saturating_add(self.funding.pool_spent().unwrap_or(U256::ZERO));
let deposit = match self.store.get_by_pool_id(pool_id) {
Ok(Some(row)) => row.deposit,
Ok(None) => estimate(),
Err(error) => {
tracing::warn!(
%pool_id,
%error,
"could not read the buyer pool's row; estimating its deposit from the run"
);
estimate()
}
};
tracing::info!(
%pool_id,
%deposit,
"the buyer pool already holds its working deposit; waiting for the nodes to see it"
);
Ok(Recovery::ToppedUp(deposit))
}
async fn open_replacement(&self) -> anyhow::Result<PoolId> {
let deposit = self.working_deposit;
ensure_allowance(
self.rpc,
self.token,
self.owner,
self.payment_pool_addr,
if self.max_approve {
None
} else {
Some(deposit)
},
)
.await?;
let opened = open_pool(
self.contract,
Arc::clone(&self.signer),
self.deployment,
self.token,
self.owner,
deposit,
Some(self.funding.opens()),
)
.await?;
self.store
.record(&opened.state)
.map_err(|e| escrowed_but_untracked("buyer pool opened", opened.tx, e))?;
tracing::info!(pool_id = %opened.state.pool_id, %deposit, "opened a new buyer pool");
Ok(opened.state.pool_id)
}
}
pub(crate) async fn build_ctx_for_fetch<P>(
deps: &LaneDeps<'_, P>,
grant: Option<&CapabilityGrant>,
signer: &Arc<PrivateKeySigner>,
voucher_dom: &Eip712Domain,
provider: Address,
) -> anyhow::Result<(PoolContext, Option<LaneSpend>)>
where
P: alloy::providers::Provider + Clone,
{
if let Some(grant) = grant {
build_delegated_pool_ctx(
deps.store.as_ref(),
deps.contract,
signer,
voucher_dom,
provider,
deps.chain,
deps.endpoint,
grant,
)
.await
.map(|ctx| (ctx, None))
} else {
build_pool_ctx(deps, signer, provider)
.await
.map(|(ctx, spend)| (ctx, Some(spend)))
}
}
async fn build_pool_ctx<P>(
deps: &LaneDeps<'_, P>,
signer: &Arc<PrivateKeySigner>,
provider: Address,
) -> anyhow::Result<(PoolContext, LaneSpend)>
where
P: alloy::providers::Provider + Clone,
{
let (ctx, spend) = open_or_reuse_pool(
deps.store.as_ref(),
deps.contract,
deps.rpc,
deps.funding,
signer,
provider,
deps.self_address,
deps.chain.deployment(),
deps.deposit,
)
.await?;
Ok((
attach_client_binding(ctx, deps.chain, deps.endpoint, signer)?,
spend,
))
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn build_delegated_pool_ctx<P>(
store: &dyn BuyerPoolStore,
contract: &PaymentPool::PaymentPoolInstance<P>,
signer: &Arc<PrivateKeySigner>,
voucher_dom: &Eip712Domain,
provider: Address,
chain: &ChainConfig,
endpoint: &Endpoint,
grant: &CapabilityGrant,
) -> anyhow::Result<PoolContext>
where
P: alloy::providers::Provider + Clone,
{
let self_address = signer.address();
let signed_capability = grant.to_signed_capability().map_err(|e| {
anyhow::anyhow!("capability token carries a malformed owner signature: {e}")
})?;
let pool_id = grant.pool_id;
let pool = contract.getPool(pool_id).call().await.map_err(|e| {
anyhow::Error::new(e).context(format!("read on-chain pool {pool_id} for the capability"))
})?;
anyhow::ensure!(
pool.owner != Address::ZERO,
"pool {pool_id} named by the capability does not exist on this PaymentPool contract \
({}) — wrong --payment-pool-address/chain, or a stale token",
chain.payment_pool,
);
let (prior_bytes, prior_amount) = store
.delegate_lanes(chain.deployment(), pool_id, self_address)
.map_err(|e| {
anyhow::anyhow!(
"read this key's delegate-lane watermarks for pool {pool_id} from the buyer \
store: {e}; the store needs repair before a delegated fetch can run"
)
.context(crate::LocalPullFault)
})?
.into_iter()
.find(|(lane_provider, _)| *lane_provider == provider)
.map_or((U256::ZERO, U256::ZERO), |(_, p)| {
(p.last_bytes, p.last_amount)
});
let token = contract
.usdc()
.call()
.await
.map_err(|e| anyhow::Error::new(e).context("read PaymentPool.usdc()"))?;
let state = BuyerPoolState::new(
pool_id,
chain.deployment(),
pool.owner,
token,
U256::from(pool.deposit),
);
let ctx = PoolContext::for_pool(&state, Arc::clone(signer), voucher_dom.clone())
.with_provider(provider, prior_bytes, prior_amount)
.with_capability(signed_capability);
attach_client_binding(ctx, chain, endpoint, signer)
}
pub(crate) fn attach_client_binding(
ctx: PoolContext,
chain: &ChainConfig,
endpoint: &Endpoint,
signer: &Arc<PrivateKeySigner>,
) -> anyhow::Result<PoolContext> {
let Some(capacity_bond) = chain.capacity_bond else {
return Ok(ctx);
};
let bind_dom = bind_node_id_domain(chain.chain_id, capacity_bond);
let own_node_id = B256::from(*endpoint.id().as_bytes());
Ok(ctx.with_client_binding(sign_client_binding(signer, own_node_id, &bind_dom)?))
}
async fn adopt_owned_pool<P>(
store: &dyn BuyerPoolStore,
contract: &PaymentPool::PaymentPoolInstance<P>,
owner: Address,
deployment: Deployment,
) -> anyhow::Result<Option<BuyerPoolState>>
where
P: alloy::providers::Provider + Clone,
{
let Some((pool_id, pool)) = newest_solvent_owned_pool(contract, owner).await.context(
"could not ask the chain whether this wallet already owns a pool, so refusing to \
open one blind — rerun, or check the RPC endpoint",
)?
else {
return Ok(None);
};
let token =
contract.usdc().call().await.with_context(|| {
format!("read PaymentPool.usdc() while adopting live pool {pool_id}")
})?;
let state = BuyerPoolState::adopt(
pool_id,
deployment,
owner,
token,
U256::from(pool.deposit),
U256::from(pool.totalRedeemed),
);
store.record(&state).with_context(|| {
format!(
"found live pool {pool_id} on chain but could not record it in the local buyer \
store; nothing was escrowed, so rerunning retries the adoption"
)
})?;
tracing::info!(
%pool_id,
deposit = pool.deposit,
redeemed = pool.totalRedeemed,
"adopted a live buyer pool this wallet already owns on chain, instead of opening a \
second one"
);
Ok(Some(state))
}
async fn lane_watermark<P>(
contract: &PaymentPool::PaymentPoolInstance<P>,
lane: LaneKey,
) -> anyhow::Result<(U256, U256)>
where
P: alloy::providers::Provider + Clone,
{
let onchain = contract
.getWatermark(lane.pool_id, lane.signer, lane.provider)
.call()
.await
.context(
"could not read this lane's on-chain watermark, so refusing to resume it from zero, \
where it would stream bytes the provider cannot cash",
)?;
Ok((
U256::from(onchain.bytesDelivered),
U256::from(onchain.amount),
))
}
async fn pool_to_reuse<P>(
store: &dyn BuyerPoolStore,
contract: &PaymentPool::PaymentPoolInstance<P>,
funding: &RunFunding,
self_address: Address,
deployment: Deployment,
adoption: ChainAdoption,
) -> anyhow::Result<Option<BuyerPoolState>>
where
P: alloy::providers::Provider + Clone,
{
let tracked = match store.get_by_owner(self_address)? {
Some(state) if state.is_on(deployment) => Some(state),
Some(state) => {
tracing::warn!(
pool_id = %state.pool_id,
foreign_payment_pool = %state.deployment.payment_pool,
foreign_chain_id = state.deployment.chain_id,
configured_payment_pool = %deployment.payment_pool,
configured_chain_id = deployment.chain_id,
"ignoring a tracked buyer pool from another PaymentPool deployment; its deposit, \
if any, is recoverable only against `foreign_payment_pool`"
);
None
}
None => None,
};
let tracked = match tracked {
Some(state) if funding.pool_id() != Some(state.pool_id) => {
let kept = still_open(contract, self_address, state).await;
if let Some(state) = &kept {
funding.note_pool(state.pool_id);
}
kept
}
other => other,
};
Ok(match (tracked, adoption) {
(Some(state), _) => Some(state),
(None, ChainAdoption::Refused) => None,
(None, ChainAdoption::Allowed) => {
adopt_owned_pool(store, contract, self_address, deployment).await?
}
})
}
async fn still_open<P>(
contract: &PaymentPool::PaymentPoolInstance<P>,
self_address: Address,
state: BuyerPoolState,
) -> Option<BuyerPoolState>
where
P: alloy::providers::Provider + Clone,
{
match contract.getPool(state.pool_id).call().await {
Ok(pool)
if pool.owner == self_address && !matches!(pool.status, PaymentPool::Status::Open) =>
{
tracing::info!(
pool_id = %state.pool_id,
status = status_label(pool.status),
"the tracked buyer pool no longer accepts work; a new pool replaces it"
);
None
}
Ok(_) => Some(state),
Err(error) => {
tracing::warn!(
pool_id = %state.pool_id,
error = %error,
"could not read the tracked buyer pool's status; reusing it"
);
Some(state)
}
}
}
fn seed_lane(
store: &dyn BuyerPoolStore,
mut state: BuyerPoolState,
lane: LaneKey,
bytes: U256,
amount: U256,
) -> anyhow::Result<(BuyerPoolState, U256, U256)> {
if bytes.is_zero() && amount.is_zero() {
return Ok((state, bytes, amount));
}
let pool_id = state.pool_id;
let outcome = store
.seed_progress(state.owner, pool_id, lane, bytes, amount)
.with_context(|| {
format!(
"record the on-chain watermark of lane {} in pool {pool_id}",
lane.provider
)
})?;
match outcome {
AdvanceOutcome::Advanced => {
if let Err(err) = state.seed_lane(lane, bytes, amount) {
tracing::debug!(error = %err, "lane seed did not advance the in-memory row");
}
Ok((state, bytes, amount))
}
AdvanceOutcome::Regressed(_) => {
let row = store.get_by_pool_id(pool_id)?.ok_or_else(|| {
anyhow::anyhow!("buyer row for pool {pool_id} vanished during a lane seed")
})?;
let progress = row.lane_progress(lane).ok_or_else(|| {
anyhow::anyhow!(
"buyer row for pool {pool_id} lost lane {} during a seed",
lane.provider
)
})?;
Ok((row, progress.last_bytes, progress.last_amount))
}
AdvanceOutcome::UnknownPool | AdvanceOutcome::PoolMismatch => Err(anyhow::anyhow!(
"the buyer row for pool {pool_id} was forgotten or replaced while lane {} was built; \
the build retries from the store",
lane.provider
)),
}
}
fn cannot_pay(state: &BuyerPoolState, shortfall: &str) -> anyhow::Error {
anyhow::Error::new(NoAffordableSource {
deposit: state.deposit,
})
.context(format!(
"buyer pool {} has no unspent deposit, and the wallet cannot fund a top-up: \
{shortfall}",
state.pool_id
))
}
#[allow(clippy::too_many_arguments)]
async fn refill_if_low<P>(
store: &dyn BuyerPoolStore,
contract: &PaymentPool::PaymentPoolInstance<P>,
rpc: &P,
funding: &RunFunding,
state: BuyerPoolState,
self_address: Address,
spent: U256,
working_deposit: U256,
max_approve: bool,
) -> anyhow::Result<BuyerPoolState>
where
P: alloy::providers::Provider + Clone,
{
let low_water = working_deposit / U256::from(LOW_WATER_DIVISOR);
let additional = refill_amount(state.deposit, spent, working_deposit, low_water);
if additional.is_zero() {
return Ok(state);
}
let remaining = state.deposit.saturating_sub(spent);
if let Some(shortfall) = funding.shortfall() {
if remaining.is_zero() {
return Err(cannot_pay(&state, &shortfall));
}
return Ok(state);
}
tracing::info!(
"buyer pool {} low on deposit ({remaining} µUSDC remaining of {} deposited); \
topping up {additional} µUSDC",
state.pool_id,
state.deposit,
);
let escrowed = async {
ensure_allowance(
rpc,
state.token,
self_address,
*contract.address(),
if max_approve { None } else { Some(additional) },
)
.await?;
top_up(contract, self_address, state.pool_id, additional).await
}
.await;
let err = match escrowed {
Ok(ToppedUpPool { credited, tx, .. }) => {
let effect = topped_up_effect(state.pool_id, credited);
grade_deposit_credit(
store.add_deposit(self_address, state.pool_id, credited),
&effect,
tx,
)?;
return store
.get_by_pool_id(state.pool_id)?
.ok_or_else(|| escrowed_but_untracked(&effect, tx, "the credited row vanished"));
}
Err(err) => err,
};
if err.downcast_ref::<TopUpUnconfirmed>().is_some() {
return Err(err);
}
let Some(shortfall) = funding
.check_wallet(
rpc,
state.token,
self_address,
state.pool_id,
additional,
&err,
)
.await
else {
return Err(err);
};
if remaining.is_zero() {
return Err(cannot_pay(&state, &shortfall));
}
tracing::info!(
pool_id = %state.pool_id,
%remaining,
"continuing on the pool's remaining deposit (µUSDC) without a top-up"
);
Ok(state)
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn open_or_reuse_pool<P>(
store: &dyn BuyerPoolStore,
contract: &PaymentPool::PaymentPoolInstance<P>,
rpc: &P,
funding: &RunFunding,
signer: &Arc<PrivateKeySigner>,
provider: Address,
self_address: Address,
deployment: Deployment,
deposit: DepositPolicy,
) -> anyhow::Result<(PoolContext, LaneSpend)>
where
P: alloy::providers::Provider + Clone,
{
let DepositPolicy {
working_deposit,
max_approve,
adoption,
} = deposit;
let payment_pool_addr = deployment.payment_pool;
let tracked =
pool_to_reuse(store, contract, funding, self_address, deployment, adoption).await?;
if let Some(state) = tracked {
let lane = LaneKey {
pool_id: state.pool_id,
signer: self_address,
provider,
};
let recorded = state.lane_progress(lane).is_some();
let from_chain = !state.redeemed_elsewhere().is_zero();
let (state, prior_bytes, prior_amount) = if let Some(p) = state.lane_progress(lane) {
(state, p.last_bytes, p.last_amount)
} else {
let (bytes, amount) = lane_watermark(contract, lane).await?;
seed_lane(store, state, lane, bytes, amount)?
};
let spent = state.pool_spend();
let state = refill_if_low(
store,
contract,
rpc,
funding,
state,
self_address,
spent,
working_deposit,
max_approve,
)
.await?;
let lane_spend = LaneSpend {
outside: spent.saturating_sub(prior_amount),
prior: prior_amount,
recorded,
from_chain,
};
return self_owned_lane_ctx(
&state,
signer,
&deployment.voucher_domain(),
provider,
prior_bytes,
prior_amount,
)
.map(|ctx| (ctx, lane_spend));
}
let token = contract
.usdc()
.call()
.await
.map_err(|e| anyhow::Error::new(e).context("read PaymentPool.usdc()"))?;
let deposit = working_deposit;
let approve_amount = if max_approve { None } else { Some(deposit) };
ensure_allowance(rpc, token, self_address, payment_pool_addr, approve_amount).await?;
let opened = open_pool(
contract,
Arc::clone(signer),
deployment,
token,
self_address,
deposit,
Some(funding.opens()),
)
.await?;
store
.record(&opened.state)
.map_err(|e| escrowed_but_untracked("buyer pool opened", opened.tx, e))?;
let spend = LaneSpend {
outside: U256::ZERO,
prior: U256::ZERO,
recorded: false,
from_chain: false,
};
Ok((
opened
.ctx
.with_provider(provider, U256::ZERO, U256::ZERO)
.with_capability(opened.capability),
spend,
))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
mod tests;
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
mod adoption_tests;