Skip to main content

kora_lib/plugin/
mod.rs

1use async_trait::async_trait;
2use solana_client::nonblocking::rpc_client::RpcClient;
3use solana_sdk::pubkey::Pubkey;
4use std::collections::HashSet;
5
6use crate::{
7    config::{Config, TransactionPluginType},
8    error::KoraError,
9    transaction::VersionedTransactionResolved,
10};
11
12mod plugin_gas_swap;
13
14use plugin_gas_swap::GasSwapPlugin;
15
16#[derive(Debug, Clone, Copy)]
17pub enum PluginExecutionContext {
18    SignTransaction,
19    SignAndSendTransaction,
20    SignBundle,
21    SignAndSendBundle,
22}
23
24impl PluginExecutionContext {
25    pub(super) fn method_name(self) -> &'static str {
26        match self {
27            Self::SignTransaction => "signTransaction",
28            Self::SignAndSendTransaction => "signAndSendTransaction",
29            Self::SignBundle => "signBundle",
30            Self::SignAndSendBundle => "signAndSendBundle",
31        }
32    }
33}
34
35#[async_trait]
36trait TransactionPlugin: Send + Sync {
37    async fn validate(
38        &self,
39        transaction: &mut VersionedTransactionResolved,
40        _config: &Config,
41        _rpc_client: &RpcClient,
42        fee_payer: &Pubkey,
43        context: PluginExecutionContext,
44    ) -> Result<(), KoraError>;
45
46    /// Returns (errors, warnings) for this plugin's config requirements.
47    /// Called at startup by the config validator.
48    fn validate_config(&self, _config: &Config) -> (Vec<String>, Vec<String>) {
49        (vec![], vec![])
50    }
51}
52
53pub struct TransactionPluginRunner {
54    plugins: Vec<Box<dyn TransactionPlugin>>,
55}
56
57impl TransactionPluginRunner {
58    pub fn from_config(config: &Config) -> Self {
59        let mut enabled = HashSet::new();
60        let mut plugins: Vec<Box<dyn TransactionPlugin>> = Vec::new();
61
62        // TODO: WasmPlugin — operators should be able to register custom plugins via a config
63        // path (e.g. `plugins = [{type = "wasm", path = "my_plugin.wasm"}]`) without requiring a
64        // Kora source change or new release. A WasmPlugin implementing TransactionPlugin would
65        // load a .wasm module at startup and call it for each transaction. The migration is clean:
66        // WasmPlugin sits alongside typed built-ins until we're ready to drop hardcoded dispatch.
67        //
68        //   pub struct WasmPlugin { engine: wasmtime::Engine, module: wasmtime::Module }
69        //   impl TransactionPlugin for WasmPlugin { ... }
70        //
71        // TransactionPluginType would gain a `Wasm { path: PathBuf }` variant alongside GasSwap.
72        for plugin in &config.kora.plugins.enabled {
73            if !enabled.insert(plugin.clone()) {
74                continue;
75            }
76
77            match plugin {
78                TransactionPluginType::GasSwap => {
79                    plugins.push(Box::new(GasSwapPlugin));
80                }
81            }
82        }
83
84        Self { plugins }
85    }
86
87    pub fn validate_config(config: &Config) -> (Vec<String>, Vec<String>) {
88        let mut errors = Vec::new();
89        let mut warnings = Vec::new();
90        let mut seen = HashSet::new();
91
92        for plugin_type in &config.kora.plugins.enabled {
93            if !seen.insert(plugin_type.clone()) {
94                continue;
95            }
96            let plugin: Box<dyn TransactionPlugin> = match plugin_type {
97                TransactionPluginType::GasSwap => Box::new(GasSwapPlugin),
98            };
99            let (e, w) = plugin.validate_config(config);
100            errors.extend(e);
101            warnings.extend(w);
102        }
103
104        (errors, warnings)
105    }
106
107    pub async fn run(
108        &self,
109        transaction: &mut VersionedTransactionResolved,
110        config: &Config,
111        rpc_client: &RpcClient,
112        fee_payer: &Pubkey,
113        context: PluginExecutionContext,
114    ) -> Result<(), KoraError> {
115        for plugin in &self.plugins {
116            plugin.validate(transaction, config, rpc_client, fee_payer, context).await?;
117        }
118
119        Ok(())
120    }
121}