mod chain;
mod close;
mod open;
mod payload;
mod receipt;
mod state;
mod store;
mod top_up;
mod voucher;
pub use chain::OnChainChannel;
pub(crate) use receipt::channel_receipt;
pub(crate) use store::normalize_channel_id;
pub use store::{deduct_from_channel, ChannelState, ChannelStore, InMemoryChannelStore};
use alloy::primitives::Address;
use std::sync::Arc;
use alloy::providers::Provider;
use tempo_alloy::TempoNetwork;
use super::session::{SessionCredentialPayload, TempoSessionMethodDetails};
use super::{INTENT_SESSION, METHOD_NAME};
use crate::protocol::core::{PaymentCredential, Receipt};
use crate::protocol::intents::SessionRequest;
use crate::protocol::traits::{SessionMethod as SessionMethodTrait, VerificationError};
#[derive(Debug, Clone)]
pub struct SessionMethodConfig {
pub escrow_contract: Address,
pub chain_id: u64,
pub min_voucher_delta: u128,
}
#[derive(Clone)]
pub struct SessionMethod<P> {
provider: Arc<P>,
store: Arc<dyn ChannelStore>,
config: SessionMethodConfig,
close_signer: Option<Arc<super::DynSigner>>,
}
impl<P> SessionMethod<P>
where
P: Provider<TempoNetwork> + Clone + Send + Sync + 'static,
{
pub fn new(provider: P, store: Arc<dyn ChannelStore>, config: SessionMethodConfig) -> Self {
Self {
provider: Arc::new(provider),
store,
config,
close_signer: None,
}
}
pub fn with_close_signer<S>(mut self, signer: S) -> Self
where
S: alloy::signers::Signer + Send + Sync + 'static,
{
self.close_signer = Some(Arc::new(signer));
self
}
pub fn config(&self) -> &SessionMethodConfig {
&self.config
}
fn resolve_method_details(
&self,
request: &SessionRequest,
) -> Result<TempoSessionMethodDetails, VerificationError> {
use super::session::TempoSessionExt;
match request.tempo_session_details() {
Ok(details) => Ok(details),
Err(_) => Ok(TempoSessionMethodDetails {
escrow_contract: format!("{:#x}", self.config.escrow_contract),
chain_id: Some(self.config.chain_id),
channel_id: None,
min_voucher_delta: None,
fee_payer: None,
machine_token_enabled: None,
settlement_adapter: None,
settlement_recipient: None,
settlement_token: None,
operator: None,
session_protocol: None,
session_snapshot: None,
}),
}
}
fn resolve_escrow(
&self,
details: &TempoSessionMethodDetails,
) -> Result<Address, VerificationError> {
Self::parse_address(&details.escrow_contract)
}
fn resolve_chain_id(&self, details: &TempoSessionMethodDetails) -> u64 {
details.chain_id.unwrap_or(self.config.chain_id)
}
fn resolve_min_delta(&self, details: &TempoSessionMethodDetails) -> u128 {
details
.min_voucher_delta
.as_ref()
.and_then(|s| s.parse::<u128>().ok())
.unwrap_or(self.config.min_voucher_delta)
}
}
impl<P> SessionMethodTrait for SessionMethod<P>
where
P: Provider<TempoNetwork> + Clone + Send + Sync + 'static,
{
fn method(&self) -> &str {
METHOD_NAME
}
fn challenge_method_details(&self) -> Option<serde_json::Value> {
let details = super::session::TempoSessionMethodDetails {
escrow_contract: format!("{:#x}", self.config.escrow_contract),
chain_id: Some(self.config.chain_id),
min_voucher_delta: Some(self.config.min_voucher_delta.to_string()),
channel_id: None,
fee_payer: None,
machine_token_enabled: None,
settlement_adapter: None,
settlement_recipient: None,
settlement_token: None,
operator: None,
session_protocol: None,
session_snapshot: None,
};
serde_json::to_value(details).ok()
}
fn respond(
&self,
credential: &PaymentCredential,
_receipt: &Receipt,
) -> Option<serde_json::Value> {
let payload: SessionCredentialPayload = credential.payload_as().ok()?;
match payload {
SessionCredentialPayload::Voucher { .. } => None,
_ => Some(serde_json::json!({ "status": "ok" })),
}
}
async fn verify_session(
&self,
credential: &PaymentCredential,
request: &SessionRequest,
) -> Result<Receipt, VerificationError> {
if credential.challenge.method.as_str() != METHOD_NAME {
return Err(VerificationError::credential_mismatch(format!(
"Method mismatch: expected {}, got {}",
METHOD_NAME, credential.challenge.method
)));
}
if credential.challenge.intent.as_str() != INTENT_SESSION {
return Err(VerificationError::credential_mismatch(format!(
"Intent mismatch: expected {}, got {}",
INTENT_SESSION, credential.challenge.intent
)));
}
let details = self.resolve_method_details(request)?;
let merchant = request
.recipient
.as_deref()
.ok_or_else(|| {
VerificationError::invalid_payload("session challenge missing recipient")
})
.and_then(Self::parse_address)?;
let target_token = Self::parse_address(&request.currency)?;
let (expected_payee, expected_token) = if details.machine_token_enabled == Some(true) {
let (_, swapper) = crate::protocol::methods::tempo::machine_token::session_addresses(
self.resolve_chain_id(&details),
)
.ok_or_else(|| {
VerificationError::invalid_payload(
"machine tokens are unsupported on the session chain",
)
})?;
if details.settlement_adapter.as_deref() != Some(&swapper.to_string())
|| details.settlement_recipient.as_deref() != Some(&merchant.to_string())
|| details.settlement_token.as_deref() != Some(&target_token.to_string())
{
return Err(VerificationError::credential_mismatch(
"machine-token settlement route does not match the session request",
));
}
crate::protocol::methods::tempo::machine_token::session_addresses(
self.resolve_chain_id(&details),
)
.map(|(token, swapper)| (swapper, token))
.ok_or_else(|| {
VerificationError::invalid_payload(
"machine tokens are unsupported on the session chain",
)
})?
} else {
(merchant, target_token)
};
let payload: SessionCredentialPayload = credential.payload_as().map_err(|e| {
VerificationError::invalid_payload(format!("Expected session payload: {}", e))
})?;
match &payload {
SessionCredentialPayload::Open { .. } => {
let amount = request.parse_amount().map_err(|_| {
VerificationError::invalid_challenge(format!(
"invalid session amount: {}",
request.amount
))
})?;
self.handle_open(
credential,
&payload,
&details,
expected_payee,
expected_token,
amount,
)
.await
}
SessionCredentialPayload::TopUp { .. } => {
self.handle_top_up(
credential,
&payload,
&details,
expected_payee,
expected_token,
)
.await
}
SessionCredentialPayload::Voucher { .. } => {
self.handle_voucher(
credential,
&payload,
&details,
expected_payee,
expected_token,
)
.await
}
SessionCredentialPayload::Close { .. } => {
self.handle_close(
credential,
&payload,
&details,
expected_payee,
expected_token,
)
.await
}
}
}
}
#[cfg(test)]
mod tests;