Skip to content

Commit 56aa4b6

Browse files
dev-jodeeamilz
andauthored
feat(kora): add swap_gas plugin + plugin infrastructure (#383)
* feat(kora)!: add gas_swap transaction plugin infrastructure Refs: PRO-932 --------- Co-authored-by: Jo D <dev-jodee@users.noreply.github.com> Co-authored-by: amilz <85324096+amilz@users.noreply.github.com>
1 parent fea62b1 commit 56aa4b6

File tree

16 files changed

+921
-14
lines changed

16 files changed

+921
-14
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ generated/
1111
# SDK
1212
node_modules/
1313
dist/
14+
.pnpm-store/
1415

1516
# Coverage reports
1617
coverage/

crates/lib/src/bundle/helper.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
constant::ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION,
55
fee::fee::{FeeConfigUtil, TransactionFeeUtil},
66
lighthouse::LighthouseUtil,
7+
plugin::{PluginExecutionContext, TransactionPluginRunner},
78
signer::bundle_signer::BundleSigner,
89
token::token::TokenUtil,
910
transaction::{TransactionUtil, VersionedTransactionResolved},
@@ -86,9 +87,11 @@ impl BundleProcessor {
8687
config: &Config,
8788
rpc_client: &Arc<RpcClient>,
8889
sig_verify: bool,
90+
plugin_context: Option<PluginExecutionContext>,
8991
processing_mode: BundleProcessingMode<'a>,
9092
) -> Result<Self, KoraError> {
9193
let validator = TransactionValidator::new(config, fee_payer)?;
94+
let plugin_runner = TransactionPluginRunner::from_config(config);
9295
let mut resolved_transactions = Vec::with_capacity(encoded_txs.len());
9396
let mut total_required_lamports = 0u64;
9497
let mut all_bundle_instructions: Vec<Instruction> = Vec::new();
@@ -119,6 +122,11 @@ impl BundleProcessor {
119122
}
120123

121124
validator.validate_transaction(config, &mut resolved_tx, rpc_client).await?;
125+
if let Some(context) = plugin_context {
126+
plugin_runner
127+
.run(&mut resolved_tx, config, rpc_client, &fee_payer, context)
128+
.await?;
129+
}
122130

123131
let fee_calc = FeeConfigUtil::estimate_kora_fee(
124132
&mut resolved_tx,

crates/lib/src/config.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,19 @@ impl Default for CacheConfig {
477477
}
478478
}
479479

480+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq, Hash)]
481+
#[serde(rename_all = "snake_case")]
482+
pub enum TransactionPluginType {
483+
GasSwap,
484+
}
485+
486+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
487+
#[serde(default)]
488+
pub struct PluginsConfig {
489+
/// List of enabled transaction plugins, executed for sign/signAndSend flows
490+
pub enabled: Vec<TransactionPluginType>,
491+
}
492+
480493
#[derive(Clone, Serialize, Deserialize, ToSchema)]
481494
#[serde(default)]
482495
pub struct KoraConfig {
@@ -488,6 +501,8 @@ pub struct KoraConfig {
488501
pub payment_address: Option<String>,
489502
pub cache: CacheConfig,
490503
pub usage_limit: UsageLimitConfig,
504+
/// Transaction plugins executed during sign/signAndSend flows
505+
pub plugins: PluginsConfig,
491506
/// Bundle support configuration
492507
pub bundle: BundleConfig,
493508
/// Lighthouse configuration for fee payer balance protection
@@ -507,6 +522,7 @@ impl Default for KoraConfig {
507522
payment_address: None,
508523
cache: CacheConfig::default(),
509524
usage_limit: UsageLimitConfig::default(),
525+
plugins: PluginsConfig::default(),
510526
bundle: BundleConfig::default(),
511527
lighthouse: LighthouseConfig::default(),
512528
force_sig_verify: false,

crates/lib/src/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod lighthouse;
1313
pub mod log;
1414
pub mod metrics;
1515
pub mod oracle;
16+
pub mod plugin;
1617
pub mod rpc;
1718
pub mod rpc_server;
1819
pub mod sanitize;

crates/lib/src/plugin/mod.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use async_trait::async_trait;
2+
use solana_client::nonblocking::rpc_client::RpcClient;
3+
use solana_sdk::pubkey::Pubkey;
4+
use std::collections::HashSet;
5+
6+
use crate::{
7+
config::{Config, TransactionPluginType},
8+
error::KoraError,
9+
transaction::VersionedTransactionResolved,
10+
};
11+
12+
mod plugin_gas_swap;
13+
14+
use plugin_gas_swap::GasSwapPlugin;
15+
16+
#[derive(Debug, Clone, Copy)]
17+
pub enum PluginExecutionContext {
18+
SignTransaction,
19+
SignAndSendTransaction,
20+
SignBundle,
21+
SignAndSendBundle,
22+
}
23+
24+
impl 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]
36+
trait 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+
53+
pub struct TransactionPluginRunner {
54+
plugins: Vec<Box<dyn TransactionPlugin>>,
55+
}
56+
57+
impl 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+
}

0 commit comments

Comments
 (0)