use anyhow::{Result, ensure};
use std::sync::Arc;
use mesh_llm_payments_types::contract::{
AuthorizeRequest, CancelRequest, CancelStage, PayInputRequest, ReconcileResponse,
RoutingBudgetRequest, RoutingBudgetResponse, SettleOutputRequest,
};
use crate::intent::PaymentIntent;
use crate::ledger::Charge;
use crate::pricing::payment_cap_msat;
use crate::service::PaymentService;
use crate::wallet::Transaction;
impl PaymentService {
pub async fn routing_budget(&self, request: RoutingBudgetRequest) -> RoutingBudgetResponse {
let profile = self.ledger.payment_intent().unwrap_or_default();
let intent = match request.request_intent {
Some(value) => match serde_json::from_value::<PaymentIntent>(value) {
Ok(request) if request.validate().is_ok() => profile.restrict(&request),
_ => PaymentIntent::FreeOnly,
},
None => profile,
};
let available_msat = if matches!(intent, PaymentIntent::FreeOnly) || !self.has_wallet() {
0
} else {
self.spendable_budget().await
};
RoutingBudgetResponse {
intent,
available_msat,
}
}
async fn spendable_budget(&self) -> u64 {
let Ok(wallet) = self.wallet().await else {
return 0;
};
let Ok(balance) = wallet.balance().await else {
return 0;
};
self.ledger
.available_budget(balance.spendable_msat, crate::now_ms())
.unwrap_or(0)
}
pub async fn reconcile(&self) -> Result<ReconcileResponse> {
let _ = self.reconcile_pending().await;
let _ = self.recover_output_debt().await;
let approved = self
.ledger
.requests()?
.into_iter()
.filter(|request| request.state == "approved" && request.terms.peer != "wallet-send")
.map(|request| request.terms)
.collect();
Ok(ReconcileResponse { approved })
}
pub async fn settle_output(&self, request: SettleOutputRequest) -> Result<Transaction> {
let SettleOutputRequest {
terms,
tokens,
invoice,
} = request;
ensure!(
tokens > 0 && tokens <= terms.max_output_tokens,
"output token allowance exceeded"
);
ensure!(
terms.payee.as_deref() == Some(invoice.payee.as_str()),
"output invoice changed receiving wallet"
);
let amount_msat = terms.pricing.output_charge(tokens)?;
ensure!(
invoice.amount_msat == Some(amount_msat),
"output invoice amount mismatch"
);
self.pay_charge(&Charge {
request_id: terms.id.clone(),
segment: 1,
invoice,
amount_msat,
max_total_msat: payment_cap_msat(amount_msat)?,
})
.await
}
pub fn prefetch(self: &Arc<Self>, id: String) -> Result<()> {
const MAX_PREFETCHED: usize = 256;
let handle = self.prefetch_balance();
let mut prefetched = self
.prefetched
.lock()
.map_err(|_| anyhow::anyhow!("prefetch table poisoned"))?;
if prefetched.len() >= MAX_PREFETCHED {
prefetched.retain(|_, handle| !handle.is_finished());
}
if prefetched.len() < MAX_PREFETCHED {
prefetched.insert(id, handle);
}
Ok(())
}
fn take_prefetched(
&self,
id: &str,
) -> Option<tokio::task::JoinHandle<Result<crate::wallet::Balance>>> {
self.prefetched.lock().ok()?.remove(id)
}
pub async fn authorize(&self, request: AuthorizeRequest) -> Result<()> {
let prefetched = self.take_prefetched(&request.terms.id);
self.await_authorization_with(&request.terms, async {
match prefetched {
Some(handle) => handle.await?,
None => self.wallet().await?.balance().await,
}
})
.await
}
pub fn cancel(&self, request: CancelRequest) -> Result<()> {
drop(self.take_prefetched(&request.id));
match request.stage {
CancelStage::Unstarted => self.ledger.cancel_unstarted(&request.id),
CancelStage::Authorization => self.ledger.fail_authorization_if_idle(&request.id),
}
}
pub async fn pay_input(&self, request: PayInputRequest) -> Result<Transaction> {
let PayInputRequest { terms, invoice } = request;
let amount_msat = terms.pricing.input_charge(terms.input_tokens)?;
ensure!(
invoice.amount_msat == Some(amount_msat),
"fixed-amount inference invoice required"
);
self.pay_charge(&Charge {
request_id: terms.id.clone(),
segment: 0,
invoice,
amount_msat,
max_total_msat: payment_cap_msat(amount_msat)?,
})
.await
}
}