mod config;
mod memo;
mod verify;
use std::collections::HashMap;
use std::future::Future;
use compact_str::CompactString;
pub use config::SolanaExactFacilitatorConfig;
use r402_core::chain::ChainProvider;
use r402_core::error::VerificationError;
use r402_core::facilitator::{DynFacilitator, Facilitator, FacilitatorError};
use r402_core::scheme::{SchemeBuilder, SchemeId};
use r402_core::wire;
pub use verify::{
TransferCheckedInstruction, TransferRequirement, VerifyTransferResult, settle_transaction,
validate_instructions, verify_compute_limit_instruction, verify_compute_price_instruction,
verify_transaction, verify_transfer, verify_transfer_instruction,
};
use crate::chain::provider::SolanaChainProviderLike;
use crate::exact::types;
use crate::exact::{ExactScheme, SolanaExact, SupportedPaymentKindExtra};
use crate::settlement_cache::{Duplicate, SettlementCache};
pub struct SolanaExactFacilitator<P> {
provider: P,
config: SolanaExactFacilitatorConfig,
settlement_cache: SettlementCache,
}
impl<P> SolanaExactFacilitator<P> {
pub fn new(provider: P, config: SolanaExactFacilitatorConfig) -> Self {
Self::with_settlement_cache(provider, config, SettlementCache::new())
}
pub const fn with_settlement_cache(
provider: P,
config: SolanaExactFacilitatorConfig,
settlement_cache: SettlementCache,
) -> Self {
Self {
provider,
config,
settlement_cache,
}
}
}
impl<P> std::fmt::Debug for SolanaExactFacilitator<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SolanaExactFacilitator")
.field("config", &self.config)
.finish_non_exhaustive()
}
}
impl<P> SchemeBuilder<P> for SolanaExact
where
P: SolanaChainProviderLike + ChainProvider + Send + Sync + 'static,
{
fn build(
&self,
provider: P,
config: Option<serde_json::Value>,
) -> Result<Box<dyn DynFacilitator>, Box<dyn std::error::Error + Send + Sync>> {
let config = config
.map(serde_json::from_value::<SolanaExactFacilitatorConfig>)
.transpose()?
.unwrap_or_default();
Ok(Box::new(SolanaExactFacilitator::new(provider, config)))
}
}
impl<P> Facilitator for SolanaExactFacilitator<P>
where
P: SolanaChainProviderLike + ChainProvider + Send + Sync,
{
async fn verify(
&self,
request: wire::VerifyRequest,
) -> Result<wire::VerifyResponse, FacilitatorError> {
let request = types::v2::VerifyRequest::from_verify(request)?;
enforce_memo(&request)?;
let verification = verify_transfer(&self.provider, &request, &self.config).await?;
Ok(wire::VerifyResponse::valid(verification.payer.to_string()))
}
async fn settle(
&self,
request: wire::SettleRequest,
) -> Result<wire::SettleResponse, FacilitatorError> {
let request = types::v2::SettleRequest::from_settle(request)?;
let cache_key = request.payment_payload.payload.transaction.as_str();
if self.settlement_cache.reserve(cache_key) == Duplicate::Yes {
return Err(VerificationError::DuplicateSettlement.into());
}
enforce_memo(&request)?;
let amount = request.payment_payload.accepted.amount.to_string();
let verification = verify_transfer(&self.provider, &request, &self.config).await?;
let payer = verification.payer.to_string();
let tx_sig = settle_transaction(&self.provider, verification).await?;
Ok(wire::SettleResponse::Success {
payer: payer.into(),
transaction: tx_sig.to_string().into(),
network: self.provider.chain_id().to_string().into(),
amount: Some(amount.into()),
extensions: wire::Extensions::new(),
})
}
fn supported(
&self,
) -> impl Future<Output = Result<wire::SupportedResponse, FacilitatorError>> + Send {
let chain_id = self.provider.chain_id();
let kinds: Vec<wire::SupportedPaymentKind> = {
let fee_payer = self.provider.fee_payer();
let extra = serde_json::to_value(SupportedPaymentKindExtra {
fee_payer,
memo: None,
})
.ok();
vec![
wire::SupportedPaymentKind::new(
wire::V2.into(),
ExactScheme.to_string(),
chain_id.to_string(),
)
.with_optional_extra(extra),
]
};
let signers = {
let mut signers: HashMap<CompactString, Vec<CompactString>> = HashMap::with_capacity(1);
let _ = signers.insert(
SolanaExact.caip_family().into(),
self.provider
.signer_addresses()
.into_iter()
.map(CompactString::from)
.collect(),
);
signers
};
std::future::ready(Ok(wire::SupportedResponse::new()
.with_kinds(kinds)
.with_signers(signers)))
}
}
fn enforce_memo(request: &types::v2::VerifyRequest) -> Result<(), FacilitatorError> {
use base64::Engine;
use solana_message::VersionedMessage;
let Some(expected) = request
.payment_payload
.accepted
.extra
.as_ref()
.and_then(|extra| extra.memo.as_deref())
else {
return Ok(());
};
let raw = base64::engine::general_purpose::STANDARD
.decode(request.payment_payload.payload.transaction.as_bytes())
.map_err(|e| VerificationError::InvalidFormat(e.to_string()))?;
let transaction: solana_transaction::versioned::VersionedTransaction =
bincode::deserialize(&raw).map_err(|e| VerificationError::InvalidFormat(e.to_string()))?;
let (instructions, static_keys): (
&[solana_message::compiled_instruction::CompiledInstruction],
&[solana_pubkey::Pubkey],
) = match &transaction.message {
VersionedMessage::Legacy(m) => (&m.instructions, &m.account_keys),
VersionedMessage::V0(m) => (&m.instructions, &m.account_keys),
};
memo::verify_memo(instructions, static_keys, Some(expected))
.map_err(|e| FacilitatorError::Verification(VerificationError::from(e)))?;
Ok(())
}