Summary
While looking into the WasmPlugin TODO, I started tracing the current transaction plugin architecture to understand what would be required for an implementation.
The overall direction is clear:
- allow operator-provided transaction plugins
- keep built-in plugins alongside external plugins
- load plugins from configuration
- integrate them into
TransactionPluginRunner
However, after following the current implementation, I noticed that the remaining work extends beyond the host-side wrapper. The current TODO explains where a WasmPlugin should fit, but it doesn't yet define the contract between the Rust host and the Wasm guest.
I'd like to clarify the intended architecture so the implementation matches the long-term direction of the project rather than introducing an ABI that later needs to change.
Current implementation
Transaction plugin interface
|
#[async_trait] |
|
trait TransactionPlugin: Send + Sync { |
|
async fn validate( |
|
&self, |
|
transaction: &mut VersionedTransactionResolved, |
|
_config: &Config, |
|
_rpc_client: &RpcClient, |
|
fee_payer: &Pubkey, |
|
context: PluginExecutionContext, |
|
) -> Result<(), KoraError>; |
|
|
|
/// Returns (errors, warnings) for this plugin's config requirements. |
|
/// Called at startup by the config validator. |
|
fn validate_config(&self, _config: &Config) -> (Vec<String>, Vec<String>) { |
|
(vec![], vec![]) |
|
} |
|
} |
This defines the current TransactionPlugin interface.
The trait currently receives:
&mut VersionedTransactionResolved
&Config
&RpcClient
&Pubkey
PluginExecutionContext
This works naturally for native Rust plugins, but it raises questions for a Wasm guest because some of these values cannot cross the Wasm boundary directly.
Wasm TODO
|
// TODO: WasmPlugin — operators should be able to register custom plugins via a config |
|
// path (e.g. `plugins = [{type = "wasm", path = "my_plugin.wasm"}]`) without requiring a |
|
// Kora source change or new release. A WasmPlugin implementing TransactionPlugin would |
|
// load a .wasm module at startup and call it for each transaction. The migration is clean: |
|
// WasmPlugin sits alongside typed built-ins until we're ready to drop hardcoded dispatch. |
|
// |
|
// pub struct WasmPlugin { engine: wasmtime::Engine, module: wasmtime::Module } |
|
// impl TransactionPlugin for WasmPlugin { ... } |
|
// |
|
// TransactionPluginType would gain a `Wasm { path: PathBuf }` variant alongside GasSwap. |
The TODO clearly explains the desired direction:
- operator-provided Wasm plugins
- configuration-driven loading
WasmPlugin
wasmtime
- coexistence with existing built-in plugins
This provides the host-side direction, but it doesn't yet define the guest-side interface.
Current plugin construction
|
for plugin in &config.kora.plugins.enabled { |
|
if !enabled.insert(plugin.clone()) { |
|
continue; |
|
} |
|
|
|
match plugin { |
|
TransactionPluginType::GasSwap => { |
|
plugins.push(Box::new(GasSwapPlugin)); |
|
} |
|
TransactionPluginType::DeployAuthority => { |
|
plugins.push(Box::new(DeployAuthorityPlugin)); |
|
} |
|
} |
|
} |
|
|
|
Self { plugins } |
|
} |
Currently the runner constructs only built-in plugins through a fixed match.
This seems like the natural place where external plugins would eventually be integrated.
Configuration model
|
price_ttl: DEFAULT_CACHE_PRICE_TTL, |
|
} |
|
} |
|
} |
|
|
|
impl CacheConfig { |
|
/// Resolve the Redis URL to use. Priority: `KORA_REDIS_URL` env var over the `url` |
|
/// field from the TOML config. Returns `None` when neither is set. |
|
pub fn resolved_url(&self) -> Option<String> { |
|
std::env::var("KORA_REDIS_URL").ok().or_else(|| self.url.clone()) |
|
} |
|
} |
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq, Hash)] |
|
#[serde(rename_all = "snake_case")] |
|
pub enum TransactionPluginType { |
|
GasSwap, |
|
DeployAuthority, |
|
} |
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)] |
|
#[serde(default)] |
|
pub struct PluginsConfig { |
|
/// List of enabled transaction plugins, executed for sign/signAndSend flows |
|
pub enabled: Vec<TransactionPluginType>, |
|
} |
Currently the configuration is limited to built-in plugin types.
Supporting external plugins will likely require extending this configuration model while keeping the current format backward compatible.
Configuration validation
|
pub fn validate_config(config: &Config) -> (Vec<String>, Vec<String>) { |
|
let mut errors = Vec::new(); |
|
let mut warnings = Vec::new(); |
|
let mut seen = HashSet::new(); |
|
|
|
for plugin_type in &config.kora.plugins.enabled { |
|
if !seen.insert(plugin_type.clone()) { |
|
continue; |
|
} |
|
let plugin: Box<dyn TransactionPlugin> = match plugin_type { |
|
TransactionPluginType::GasSwap => Box::new(GasSwapPlugin), |
|
TransactionPluginType::DeployAuthority => Box::new(DeployAuthorityPlugin), |
|
}; |
|
let (e, w) = plugin.validate_config(config); |
|
errors.extend(e); |
|
warnings.extend(w); |
|
} |
|
|
|
(errors, warnings) |
|
} |
and
|
if config.kora.sign_max_retries > HIGH_SIGN_MAX_RETRIES_WARNING_THRESHOLD { |
|
warnings.push(format!( |
|
"sign_max_retries ({}) is very high - consider reducing to prevent long request hangs", |
|
config.kora.sign_max_retries |
|
)); |
|
} |
|
|
|
let mut unique_plugins = std::collections::HashSet::new(); |
|
for plugin in &config.kora.plugins.enabled { |
|
if !unique_plugins.insert(plugin.clone()) { |
|
warnings.push(format!("Duplicate transaction plugin configured: {:?}", plugin)); |
|
} |
|
} |
|
|
|
let (plugin_errors, plugin_warnings) = TransactionPluginRunner::validate_config(config); |
|
errors.extend(plugin_errors); |
|
warnings.extend(plugin_warnings); |
The existing validation flow already provides a natural place for plugin-specific validation.
Existing plugin examples
GasSwap:
https://github.com/solana-foundation/kora/blob/e84bcaba4532135af941cd4654c3bb3a100ad2b7/crates/lib/src/plugin/plugin_gas_swap.rs
DeployAuthority:
https://github.com/solana-foundation/kora/blob/e84bcaba4532135af941cd4654c3bb3a100ad2b7/crates/lib/src/plugin/plugin_deploy_authority.rs
One interesting observation is that DeployAuthorityPlugin depends on RpcClient for on-chain lookups, while GasSwapPlugin performs only local validation.
That difference made me wonder what the intended capabilities of a Wasm plugin are expected to be.
Questions
While exploring the implementation, I wasn't able to determine the intended Wasm ABI.
Some questions came up:
-
Should a Wasm plugin expose a single validation entry point, or is another interface expected?
-
Is the intention for Wasm plugins to support transaction mutation, or should they only perform validation?
-
How should a Wasm plugin perform operations that currently rely on RpcClient (for example, logic similar to DeployAuthorityPlugin)?
-
Is there an intended serialization format between the host and the guest?
-
Is wasmtime still the preferred runtime, or was it simply used as an example in the TODO?
-
Would it make sense to define a small ABI/interface document before implementing the runtime?
Summary
While looking into the
WasmPluginTODO, I started tracing the current transaction plugin architecture to understand what would be required for an implementation.The overall direction is clear:
TransactionPluginRunnerHowever, after following the current implementation, I noticed that the remaining work extends beyond the host-side wrapper. The current TODO explains where a
WasmPluginshould fit, but it doesn't yet define the contract between the Rust host and the Wasm guest.I'd like to clarify the intended architecture so the implementation matches the long-term direction of the project rather than introducing an ABI that later needs to change.
Current implementation
Transaction plugin interface
kora/crates/lib/src/plugin/mod.rs
Lines 37 to 53 in e84bcab
This defines the current
TransactionPlugininterface.The trait currently receives:
&mut VersionedTransactionResolved&Config&RpcClient&PubkeyPluginExecutionContextThis works naturally for native Rust plugins, but it raises questions for a Wasm guest because some of these values cannot cross the Wasm boundary directly.
Wasm TODO
kora/crates/lib/src/plugin/mod.rs
Lines 64 to 73 in e84bcab
The TODO clearly explains the desired direction:
WasmPluginwasmtimeThis provides the host-side direction, but it doesn't yet define the guest-side interface.
Current plugin construction
kora/crates/lib/src/plugin/mod.rs
Lines 74 to 90 in e84bcab
Currently the runner constructs only built-in plugins through a fixed
match.This seems like the natural place where external plugins would eventually be integrated.
Configuration model
kora/crates/lib/src/config.rs
Lines 674 to 699 in e84bcab
Currently the configuration is limited to built-in plugin types.
Supporting external plugins will likely require extending this configuration model while keeping the current format backward compatible.
Configuration validation
kora/crates/lib/src/plugin/mod.rs
Lines 92 to 111 in e84bcab
and
kora/crates/lib/src/validator/config_validator.rs
Lines 586 to 602 in e84bcab
The existing validation flow already provides a natural place for plugin-specific validation.
Existing plugin examples
GasSwap:
https://github.com/solana-foundation/kora/blob/e84bcaba4532135af941cd4654c3bb3a100ad2b7/crates/lib/src/plugin/plugin_gas_swap.rs
DeployAuthority:
https://github.com/solana-foundation/kora/blob/e84bcaba4532135af941cd4654c3bb3a100ad2b7/crates/lib/src/plugin/plugin_deploy_authority.rs
One interesting observation is that
DeployAuthorityPlugindepends onRpcClientfor on-chain lookups, whileGasSwapPluginperforms only local validation.That difference made me wonder what the intended capabilities of a Wasm plugin are expected to be.
Questions
While exploring the implementation, I wasn't able to determine the intended Wasm ABI.
Some questions came up:
Should a Wasm plugin expose a single validation entry point, or is another interface expected?
Is the intention for Wasm plugins to support transaction mutation, or should they only perform validation?
How should a Wasm plugin perform operations that currently rely on
RpcClient(for example, logic similar toDeployAuthorityPlugin)?Is there an intended serialization format between the host and the guest?
Is
wasmtimestill the preferred runtime, or was it simply used as an example in the TODO?Would it make sense to define a small ABI/interface document before implementing the runtime?