use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use std::time::Duration;
use r402::chain::{ChainId, ChainProvider};
use r402::facilitator::FacilitatorError;
use r402::proto::PaymentVerificationError;
use solana_account::Account;
use solana_client::client_error::{ClientError, ClientErrorKind};
use solana_client::nonblocking::pubsub_client::PubsubClient;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_client::pubsub_client::PubsubClientError;
use solana_client::rpc_client::SerializableTransaction;
use solana_client::rpc_config::{
RpcSendTransactionConfig, RpcSignatureSubscribeConfig, RpcSimulateTransactionConfig,
};
use solana_client::rpc_response::{RpcSignatureResult, TransactionError, UiTransactionError};
use solana_commitment_config::CommitmentConfig;
use solana_keypair::Keypair;
use solana_keypair::Signer;
use solana_pubkey::Pubkey;
use solana_signature::Signature;
use solana_signer::SignerError;
use solana_transaction::versioned::VersionedTransaction;
use crate::chain::types::{Address, SolanaChainReference};
#[derive(thiserror::Error, Debug)]
pub enum SolanaChainProviderError {
#[error(transparent)]
Signer(#[from] SignerError),
#[error("Invalid transaction: {0}")]
InvalidTransaction(#[from] UiTransactionError),
#[error(transparent)]
Transport(Box<ClientErrorKind>),
#[error(transparent)]
PubsubTransport(#[from] PubsubClientError),
#[error("{0}")]
Custom(String),
}
impl From<ClientError> for SolanaChainProviderError {
fn from(value: ClientError) -> Self {
Self::Transport(value.kind)
}
}
impl From<SolanaChainProviderError> for FacilitatorError {
fn from(value: SolanaChainProviderError) -> Self {
Self::OnchainFailure(value.to_string())
}
}
impl From<SolanaChainProviderError> for PaymentVerificationError {
fn from(value: SolanaChainProviderError) -> Self {
Self::TransactionSimulation(value.to_string())
}
}
pub struct SolanaChainProvider {
chain: SolanaChainReference,
keypair: Arc<Keypair>,
rpc_client: Arc<RpcClient>,
pubsub_client: Option<Arc<PubsubClient>>,
max_compute_unit_limit: u32,
max_compute_unit_price: u64,
}
impl Debug for SolanaChainProvider {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SolanaChainProvider")
.field("pubkey", &self.keypair.pubkey())
.field("chain", &self.chain)
.field("rpc_url", &self.rpc_client.url())
.field("max_compute_unit_limit", &self.max_compute_unit_limit)
.field("max_compute_unit_price", &self.max_compute_unit_price)
.finish_non_exhaustive()
}
}
impl SolanaChainProvider {
pub async fn new(
keypair: Keypair,
rpc_url: String,
pubsub_url: Option<String>,
chain: SolanaChainReference,
max_compute_unit_limit: u32,
max_compute_unit_price: u64,
) -> Result<Self, PubsubClientError> {
#[cfg(feature = "telemetry")]
{
let signer_addresses = vec![keypair.pubkey()];
let chain_id: ChainId = chain.into();
tracing::info!(
chain = %chain_id,
rpc = rpc_url,
pubsub = ?pubsub_url,
signers = ?signer_addresses,
max_compute_unit_limit,
max_compute_unit_price,
"Using Solana provider"
);
}
let rpc_client = RpcClient::new(rpc_url);
let pubsub_client = if let Some(pubsub_url) = pubsub_url {
let client = PubsubClient::new(pubsub_url).await?;
Some(client)
} else {
None
};
Ok(Self {
keypair: Arc::new(keypair),
chain,
rpc_client: Arc::new(rpc_client),
pubsub_client: pubsub_client.map(Arc::new),
max_compute_unit_limit,
max_compute_unit_price,
})
}
#[must_use]
pub fn rpc_client(&self) -> Arc<RpcClient> {
Arc::clone(&self.rpc_client)
}
#[must_use]
pub fn pubsub_client(&self) -> Option<Arc<PubsubClient>> {
self.pubsub_client.clone()
}
pub async fn send(
&self,
tx: &VersionedTransaction,
) -> Result<Signature, SolanaChainProviderError> {
let signature = self
.rpc_client
.send_transaction_with_config(
tx,
RpcSendTransactionConfig {
skip_preflight: true,
..RpcSendTransactionConfig::default()
},
)
.await?;
Ok(signature)
}
}
impl ChainProvider for SolanaChainProvider {
fn signer_addresses(&self) -> Vec<String> {
vec![self.fee_payer().to_string()]
}
fn chain_id(&self) -> ChainId {
self.chain.into()
}
}
pub trait SolanaChainProviderLike: Sync {
fn simulate_transaction_with_config(
&self,
tx: &VersionedTransaction,
cfg: RpcSimulateTransactionConfig,
) -> impl Future<Output = Result<(), SolanaChainProviderError>> + Send;
fn get_multiple_accounts(
&self,
pubkeys: &[Pubkey],
) -> impl Future<Output = Result<Vec<Option<Account>>, SolanaChainProviderError>> + Send;
fn max_compute_unit_limit(&self) -> u32;
fn max_compute_unit_price(&self) -> u64;
fn pubkey(&self) -> Pubkey;
fn fee_payer(&self) -> Address;
fn sign(
&self,
tx: VersionedTransaction,
) -> Result<VersionedTransaction, SolanaChainProviderError>;
fn send_and_confirm(
&self,
tx: &VersionedTransaction,
commitment_config: CommitmentConfig,
) -> impl Future<Output = Result<Signature, SolanaChainProviderError>> + Send;
}
impl SolanaChainProviderLike for SolanaChainProvider {
async fn simulate_transaction_with_config(
&self,
tx: &VersionedTransaction,
cfg: RpcSimulateTransactionConfig,
) -> Result<(), SolanaChainProviderError> {
let sim = self
.rpc_client
.simulate_transaction_with_config(tx, cfg)
.await?;
sim.value.err.map_or(Ok(()), |e| {
Err(SolanaChainProviderError::InvalidTransaction(e))
})
}
async fn get_multiple_accounts(
&self,
pubkeys: &[Pubkey],
) -> Result<Vec<Option<Account>>, SolanaChainProviderError> {
let accounts = self.rpc_client.get_multiple_accounts(pubkeys).await?;
Ok(accounts)
}
fn max_compute_unit_limit(&self) -> u32 {
self.max_compute_unit_limit
}
fn max_compute_unit_price(&self) -> u64 {
self.max_compute_unit_price
}
fn pubkey(&self) -> Pubkey {
self.keypair.pubkey()
}
fn fee_payer(&self) -> Address {
Address::new(self.keypair.pubkey())
}
fn sign(
&self,
tx: VersionedTransaction,
) -> Result<VersionedTransaction, SolanaChainProviderError> {
let mut tx = tx;
let msg_bytes = tx.message.serialize();
let signature = self.keypair.try_sign_message(msg_bytes.as_slice())?;
let num_required = tx.message.header().num_required_signatures as usize;
let static_keys = tx.message.static_account_keys();
#[allow(
clippy::indexing_slicing,
reason = "num_required <= static_keys.len() by Solana message invariant"
)]
let pos = static_keys[..num_required]
.iter()
.position(|k| *k == self.pubkey())
.ok_or(SolanaChainProviderError::InvalidTransaction(
UiTransactionError::from(TransactionError::InvalidAccountIndex),
))?;
if tx.signatures.len() < num_required {
tx.signatures.resize(num_required, Signature::default());
}
#[allow(
clippy::indexing_slicing,
reason = "pos < num_required, resize ensures len >= num_required"
)]
{
tx.signatures[pos] = signature;
}
Ok(tx)
}
#[allow(
clippy::excessive_nesting,
reason = "pubsub vs polling branches are inherently nested"
)]
async fn send_and_confirm(
&self,
tx: &VersionedTransaction,
commitment_config: CommitmentConfig,
) -> Result<Signature, SolanaChainProviderError> {
use futures_util::stream::StreamExt;
let tx_sig = tx.get_signature();
if let Some(pubsub_client) = self.pubsub_client.as_ref() {
let config = RpcSignatureSubscribeConfig {
commitment: Some(commitment_config),
enable_received_notification: None,
};
let (mut stream, unsubscribe) = pubsub_client
.signature_subscribe(tx_sig, Some(config))
.await?;
if let Err(e) = self.send(tx).await {
#[cfg(feature = "telemetry")]
tracing::error!(error = %e, "Failed to send transaction");
unsubscribe().await;
return Err(e);
}
if let Some(response) = stream.next().await {
let error = if let RpcSignatureResult::ProcessedSignature(r) = response.value {
r.err
} else {
None
};
error.map_or(Ok(*tx_sig), |e| {
Err(SolanaChainProviderError::InvalidTransaction(e))
})
} else {
Err(SolanaChainProviderError::Transport(Box::new(
ClientErrorKind::Custom(
"Can not get response from signatureSubscribe".to_owned(),
),
)))
}
} else {
const MAX_CONFIRM_TIMEOUT: Duration = Duration::from_mins(1);
const POLL_INTERVAL: Duration = Duration::from_millis(200);
self.send(tx).await?;
let deadline = tokio::time::Instant::now() + MAX_CONFIRM_TIMEOUT;
loop {
let confirmed = self
.rpc_client
.confirm_transaction_with_commitment(tx_sig, commitment_config)
.await?;
if confirmed.value {
return Ok(*tx_sig);
}
if tokio::time::Instant::now() >= deadline {
return Err(SolanaChainProviderError::Transport(Box::new(
ClientErrorKind::Custom(format!(
"Transaction confirmation timed out after {MAX_CONFIRM_TIMEOUT:?}"
)),
)));
}
tokio::time::sleep(POLL_INTERVAL).await;
}
}
}
}
impl<T: SolanaChainProviderLike + Send> SolanaChainProviderLike for Arc<T> {
fn simulate_transaction_with_config(
&self,
tx: &VersionedTransaction,
cfg: RpcSimulateTransactionConfig,
) -> impl Future<Output = Result<(), SolanaChainProviderError>> + Send {
(**self).simulate_transaction_with_config(tx, cfg)
}
fn get_multiple_accounts(
&self,
pubkeys: &[Pubkey],
) -> impl Future<Output = Result<Vec<Option<Account>>, SolanaChainProviderError>> + Send {
(**self).get_multiple_accounts(pubkeys)
}
fn max_compute_unit_limit(&self) -> u32 {
(**self).max_compute_unit_limit()
}
fn max_compute_unit_price(&self) -> u64 {
(**self).max_compute_unit_price()
}
fn pubkey(&self) -> Pubkey {
(**self).pubkey()
}
fn fee_payer(&self) -> Address {
(**self).fee_payer()
}
fn sign(
&self,
tx: VersionedTransaction,
) -> Result<VersionedTransaction, SolanaChainProviderError> {
(**self).sign(tx)
}
fn send_and_confirm(
&self,
tx: &VersionedTransaction,
commitment_config: CommitmentConfig,
) -> impl Future<Output = Result<Signature, SolanaChainProviderError>> + Send {
(**self).send_and_confirm(tx, commitment_config)
}
}