Skip to content

feat(kora): add swap_gas plugin + plugin infrastructure#383

Open
dev-jodee wants to merge 19 commits intorelease/2.2.0from
dev-jodee/gas-swap-api-5wd
Open

feat(kora): add swap_gas plugin + plugin infrastructure#383
dev-jodee wants to merge 19 commits intorelease/2.2.0from
dev-jodee/gas-swap-api-5wd

Conversation

@dev-jodee
Copy link
Contributor

@dev-jodee dev-jodee commented Mar 12, 2026

Summary

  • Replaces the dedicated swapForGas API approach with plugin-based transaction validation in existing signing flows.
  • Adds the first transaction plugin, gas_swap, for gas-station swap shape enforcement.
  • Removes the old swapForGas RPC/module/SDK surface before release.

Changes

  • Rust: Introduced plugin infrastructure and wired it into:
    • signTransaction
    • signAndSendTransaction
    • signBundle
    • signAndSendBundle
  • Rust: Added gas_swap plugin with strict outer-instruction rules:
    • exactly one System SOL transfer (Transfer or TransferWithSeed) from fee payer
    • exactly one SPL/Token2022 token transfer from a non-fee-payer owner
    • reject any additional or non-swap outer instruction/program
  • Rust: Plugins are now explicitly sign/signAndSend scoped for bundles as well:
    • estimateBundleFee skips plugin execution
  • Rust: Moved gas swap plugin implementation to crates/lib/src/plugin/plugin_gas_swap.rs for cleaner separation.
  • Config: Added transaction plugin config via [kora.plugins] enabled = ["gas_swap"].
  • Cleanup: Removed swapForGas endpoint/module paths and related SDK/API surface that are no longer part of final design.

Testing

  • just fmt
  • just check
  • just test
  • cargo test -p kora-lib gas_swap_ -- --nocapture
  • cargo test -p kora-lib test_estimate_bundle_fee_skips_plugins -- --nocapture

Closes PRO-932
Closes https://linear.app/solana-fndn/issue/PRO-932/add-swapgas-station-api-endpoint-to-kora

📊 Unit Test Coverage

Coverage

Unit Test Coverage: 83.6%

View Detailed Coverage Report

Add a trait-based swap quote provider abstraction with Jupiter default and config-driven spread for swap-for-gas quotes.

Implement the swapForGas JSON-RPC method to build token-for-SOL transactions with optional Kora fee-payer signing and no user transaction co-signing.

Wire config/method enablement, RPC docs, and TypeScript SDK/plugin request-response types with tests.

Refs: PRO-932
@linear
Copy link

linear bot commented Mar 12, 2026

@github-actions
Copy link

github-actions bot commented Mar 12, 2026

📊 TypeScript Coverage Report

Coverage: 33.1%

View detailed report

Coverage artifacts have been uploaded to this workflow run.
View Artifacts

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 12, 2026

Greptile Summary

This PR adds a new swapForGas JSON-RPC endpoint to Kora that builds a gas-station-style swap transaction: the user pays a fee token (e.g. USDC) into Kora's payment address, and Kora's fee payer sends the equivalent SOL (plus a configurable spread) to the destination wallet — without co-signing any pre-existing user transaction.

Key changes:

  • New crates/lib/src/swap/ module with a SwapQuoteProvider trait and an OracleBackedSwapQuoteProvider implementation that prices swaps via an existing oracle (Jupiter or Mock), applying a ceiling rounding on the base quote.
  • New swap_for_gas RPC method handler that validates inputs, calls the quote provider, applies basis-point spread (apply_spread_bps using correct ceil-division), builds the token-transfer + SOL-transfer instructions, and optionally has Kora pre-sign the transaction.
  • SwapForGasConfig added to KoraConfig (quote provider type + spread_bps), with config validator bounds-checking spread and requiring JUPITER_API_KEY when the Jupiter provider is selected.
  • TS SDK updated with SwapForGasRequest/SwapForGasResponse types, a KoraClient.swapForGas method, and a plugin wrapper that converts string addresses to Kit Address types.
  • kora.toml updated with the new section defaulting to swap_for_gas = false.

Issues found:

  • OracleBackedSwapQuoteProvider::get_price_in_sol calls crate::state::get_config() (the real global state) for the price-staleness check, even though config: &Config is already injected into the trait method. Because provider.rs does not have the #[cfg(test)] import shim that swap_for_gas.rs uses, unit tests exercising a Jupiter-backed provider will hit uninitialized global state, and the injected test config is silently ignored.
  • SwapQuoteProviderType::Mock is a fully public, serde-deserializable enum variant with no validation guard preventing its use in production configs, which could accidentally deploy with non-real pricing.

Confidence Score: 3/5

  • Safe to merge after addressing the provider config bypass and the Mock variant production exposure.
  • The overall structure is solid — validation, disallow-list checks, lamport bounds, ATA handling, and spread arithmetic are all implemented correctly. However, the staleness check in provider.rs bypasses the injected config and calls the global state directly, which is a real correctness/test-isolation bug for Jupiter-backed tests. The exposed Mock provider type without a production guard is a lower-severity but still meaningful risk. Neither issue is a blocker on its own but together they warrant fixes before merging.
  • crates/lib/src/swap/provider.rs (config bypass in staleness check) and crates/lib/src/config.rs (Mock variant reachable in production).

Important Files Changed

Filename Overview
crates/lib/src/swap/provider.rs New oracle-backed quote provider; staleness check bypasses injected config and calls global state directly, breaking test isolation and the dependency-injection pattern used elsewhere.
crates/lib/src/rpc_server/method/swap_for_gas.rs Core swap-for-gas handler; input validation, disallowed-account checks, token allowlist, lamport bounds, ATA creation, and spread application all look correct. Uses cfg(test) mock shim properly.
crates/lib/src/config.rs Adds SwapForGasConfig and SwapQuoteProviderType to KoraConfig; Mock variant in the public serde enum is reachable in production with no validation guard.
crates/lib/src/validator/config_validator.rs Adds spread_bps bounds check (0–10 000) and Jupiter API-key guard for swapForGas; no guard for Mock provider in production configs.
crates/lib/src/rpc_server/server.rs Registers swapForGas via the existing register_method_if_enabled! macro; guarded correctly behind the enabled_methods flag.
sdks/ts/src/types/index.ts Adds SwapForGasRequest/Response and KitSwapForGasResponse TypeScript types; well-documented and consistent with existing SDK patterns.

Sequence Diagram

sequenceDiagram
    participant Client
    participant KoraRPC
    participant SwapForGas
    participant QuoteProvider
    participant PriceOracle
    participant RpcClient
    participant Signer

    Client->>KoraRPC: swapForGas(request)
    KoraRPC->>SwapForGas: swap_for_gas(rpc_client, request)
    SwapForGas->>SwapForGas: validate lamports_out > 0
    SwapForGas->>SwapForGas: validate source/destination wallets (disallow list)
    SwapForGas->>SwapForGas: validate_lamport_fee(lamports_out)
    SwapForGas->>SwapForGas: check fee_token is supported
    SwapForGas->>QuoteProvider: quote_token_amount_in_for_lamports_out(rpc_client, token_mint, lamports_out, config)
    QuoteProvider->>PriceOracle: get_token_price(token_mint)
    PriceOracle-->>QuoteProvider: TokenPrice { price, block_id }
    QuoteProvider->>RpcClient: get_slot() [staleness check]
    RpcClient-->>QuoteProvider: current_slot
    QuoteProvider->>QuoteProvider: calculate_token_amount_in (ceil)
    QuoteProvider-->>SwapForGas: quoted_token_amount
    SwapForGas->>SwapForGas: apply_spread_bps(quoted_token_amount, spread_bps)
    SwapForGas->>RpcClient: check source ATA exists
    SwapForGas->>RpcClient: check destination ATA exists
    SwapForGas->>SwapForGas: build instructions (token transfer + SOL transfer)
    SwapForGas->>RpcClient: get_or_fetch_latest_blockhash()
    SwapForGas->>SwapForGas: build VersionedMessage (Legacy)
    alt sign_swap_transaction = true
        SwapForGas->>Signer: sign_transaction_for_bundle()
        Signer-->>SwapForGas: signed transaction
    end
    SwapForGas-->>KoraRPC: SwapForGasResponse
    KoraRPC-->>Client: SwapForGasResponse
Loading

Comments Outside Diff (1)

  1. crates/lib/src/config.rs, line 60-65 (link)

    Mock provider variant reachable in production config

    SwapQuoteProviderType::Mock is part of the public, serde-deserializable enum, which means any operator can set quote_provider = "Mock" in kora.toml and the server will start with a non-real (mock) price oracle — potentially pricing swaps at zero or a stub value — without any warning.

    The config validator in config_validator.rs only checks for the missing JUPITER_API_KEY when Jupiter is selected; it does not block or even warn when Mock is used outside of a test context.

    Consider either:

    • Annotating Mock with #[cfg(test)] so it is invisible to production builds, or
    • Adding an explicit validation error (or at least a startup warning) when swap_for_gas.quote_provider = Mock and the process is not compiled in test mode.

Last reviewed commit: c12a90b

greptile-apps[bot]

This comment was marked as resolved.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

- use injected config for Jupiter staleness checks in swap quote provider

- reject swap_for_gas Mock provider when swapForGas is enabled

- add validator test for Mock-provider rejection path

Refs: PRO-932
Trim self-explanatory comments from swap config/method/provider changes while keeping API docs where helpful.

Refs: PRO-932
devin-ai-integration[bot]

This comment was marked as resolved.

Add swap-for-gas RPC integration tests, replace the default spread literal with a shared constant, and allow mock quote provider in test harnesses via env override while keeping production validation strict by default.

Refs: PRO-932
Prevent swapForGas requests from using the Kora signer as source_wallet, which could otherwise allow a self-token-transfer plus signer-funded SOL transfer path when pre-signing is enabled. Add an integration regression test that exercises this rejection path.

Refs: PRO-932
Treat swap_for_gas.quote_provider = Mock the same way as validation.price_source = Mock: allow it with a production warning instead of requiring an env override. Remove the temporary KORA_ALLOW_MOCK_SWAP_QUOTE_PROVIDER test-runner/env path and update validator tests accordingly.

Refs: PRO-932
@amilz amilz self-requested a review March 12, 2026 17:38
Replace swap-for-gas API flow with dedicated sign and sign-and-send methods.

Use a swap-specific signAndSend path to validate signer consistency and submit user-signed transactions.

Update config flags, integration tests, and TypeScript SDK/plugin/types to the new method names.

Refs: PRO-932
Ignore local pnpm store and replace legacy swap_for_gas method toggle with sign_swap_for_gas and sign_and_send_swap_for_gas.

Refs: PRO-932
Refactor swap-for-gas RPC handlers to share validation/business logic in a processor, and enforce prebuilt transaction validation in signAndSendSwapForGas before Kora signing and forwarding.

Add integration coverage for signAndSendSwapForGas with user signature flow and missing Kora signature slot recovery.

Refs: PRO-932
devin-ai-integration[bot]

This comment was marked as resolved.

Hard-rename the swap endpoint to swapForGas, remove signAndSendSwapForGas, and keep only build+partial-sign flow for gas swaps. Update Rust config/validator surfaces, integration fixtures/tests, and TypeScript SDK/plugin/types/tests to match the new API.

BREAKING CHANGE: signSwapForGas and signAndSendSwapForGas are removed. Use swapForGas.

Refs: PRO-932
Add swapForGas to DEFAULT_PROTECTED_METHODS so reCAPTCHA protects it by default, and update the config example comment accordingly.

Refactor swap processor into focused helper methods and keep signing local to swap flow to avoid BundleSigner coupling.

Refs: PRO-932
devin-ai-integration[bot]

This comment was marked as resolved.

Prevent swapForGas from building transactions where destination_wallet is the Kora signer, which could otherwise collect user tokens without net SOL payout.

Add integration coverage for destination_wallet == signer rejection.

Refs: PRO-932
devin-ai-integration[bot]

This comment was marked as resolved.

- remove swap-specific quote provider and use validation.price_source\n- hard-rename spread_bps to buffer_bps and lamports_out request/response fields\n- add swap_for_gas.max_lamports_out and max_token_amount_in guard\n- remove swapForGas signer_key/sig_verify options\n- update Rust + TS + integration tests for new contract

Refs: PRO-932
devin-ai-integration[bot]

This comment was marked as resolved.

@dev-jodee dev-jodee requested a review from amilz March 13, 2026 12:38
remove the dedicated swapForGas RPC path and wire gas-swap validation through plugin execution in sign/signAndSend transaction and bundle flows.

BREAKING CHANGE: swapForGas has been removed. Enable gas swap rules via kora.plugins.enabled = ["gas_swap"] on existing signing RPC methods.

Refs: PRO-932
@dev-jodee dev-jodee changed the title feat(kora): add swapForGas gas-station endpoint feat(kora): add swap_gas plugin + plugin infrastructure Mar 17, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

make bundle plugin execution optional and disable it for estimateBundleFee so plugin rules remain scoped to sign/signAndSend flows.

also move the gas_swap plugin implementation into plugin_gas_swap.rs to keep plugin runner wiring focused in plugin/mod.rs.

Refs: PRO-932
dispatch validation by outer program id, reject any non-swap outer instruction, and require exactly one system transfer plus one token transfer without re-uncompiling instructions.

Refs: PRO-932
switch gas_swap semantic validation to VersionedTransactionResolved parsed system/spl instruction getters and keep strict outer-shape gating for exactly one system + one token instruction.

Refs: PRO-932
devin-ai-integration[bot]

This comment was marked as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants