Skip to content

Conversation

@fengtality
Copy link
Contributor

@fengtality fengtality commented Dec 24, 2025

Summary

This PR introduces a pluggable signer abstraction layer for Solana, modeled after the @solana/keychain library patterns. The abstraction provides a unified interface for signing transactions regardless of wallet type (hot wallet, hardware wallet, or future integrations like AWS KMS).

Relationship to @solana/keychain

The @solana/keychain library from the Solana Foundation provides a standardized signer abstraction for the Solana ecosystem. However, it is not yet published to npm and uses the new @solana/kit (v2) SDK which would require a significant migration from our current @solana/web3.js (v1) implementation.

This PR implements the same patterns and interfaces from @solana/keychain-core adapted for @solana/web3.js v1:

  • SolanaSigner interface - Matches keychain-core's unified signer interface with signTransactions(), signMessages(), isAvailable(), and address properties
  • SignerErrorCode enum - Uses SIGNER_ prefixed codes matching keychain-core (e.g., SIGNER_SIGNING_FAILED, SIGNER_NOT_AVAILABLE)
  • Utility functions - Implements keychain-core patterns:
    • createSignerError() / throwSignerError() - Error creation helpers
    • isSolanaSigner() / assertIsSolanaSigner() - Type guards

When @solana/keychain is published to npm, we can migrate to use it directly with minimal changes.

Key Changes

  • New signer interface and implementations (src/chains/solana/signers/)

    • SolanaSigner interface with standardized methods for signing
    • KeypairSigner for hot wallets (encrypted keypair files)
    • LedgerSigner for hardware wallets (wraps existing HardwareWalletService)
    • SignerFactory for automatic wallet type detection
  • Solana chain enhancements (src/chains/solana/solana.ts)

    • Added getSigner(address) method to get appropriate signer for any wallet
    • Added signAndSend(transaction, signer, options) for unified transaction signing and submission
    • Support for additionalKeypairs option for multi-signer transactions (e.g., new position accounts)
    • Updated getBalance to accept PublicKey in addition to Keypair
  • Jupiter connector update (src/connectors/jupiter/)

    • Added buildUnsignedSwapTransaction() method
    • Simplified executeQuote route using new signer pattern
  • Meteora connector update (src/connectors/meteora/clmm-routes/)

    • Updated all CLMM routes to use signer abstraction:
      • addLiquidity - now supports hardware wallets
      • removeLiquidity - now supports hardware wallets
      • openPosition - now supports hardware wallets (with additionalKeypairs for position account)
      • closePosition - now supports hardware wallets
      • collectFees - now supports hardware wallets
      • executeSwap - now supports hardware wallets

Benefits

  1. Wallet-agnostic signing - Connectors don't need to check wallet type or implement separate code paths
  2. Simplified connector code - Jupiter executeQuote reduced from ~30 lines to ~10 lines
  3. Extensible - Easy to add AWS KMS, Fireblocks, Turnkey, Privy, or other signers in the future
  4. Unified error handling - Consistent SignerError with error codes across all implementations
  5. Hardware wallet support - All Meteora and Jupiter operations now work with Ledger devices
  6. Future-proof - Interface compatible with @solana/keychain for easy migration when available

Usage Example

// Old pattern (connector must handle wallet types)
const isHardware = await solana.isHardwareWallet(address);
if (isHardware) {
  const tx = await connector.buildTxForHardwareWallet(...);
  const ledger = new SolanaLedger();
  tx = await ledger.signTransaction(address, tx);
  // ... send
} else {
  const wallet = await solana.getWallet(address);
  const tx = await connector.buildSwapTransaction(wallet, ...);
  // ... send
}

// New pattern (simplified)
const signer = await solana.getSigner(walletAddress);
const tx = await connector.buildUnsignedTransaction(...);
const result = await solana.signAndSend(tx, signer);

// For transactions needing additional signers (e.g., new position account)
const newPosition = new Keypair();
const result = await solana.signAndSend(tx, signer, {
  additionalKeypairs: [newPosition]
});

Test plan

  • Run signer unit tests: GATEWAY_TEST_MODE=dev npx jest test/chains/solana/signers/ (32 passed)
  • Run Jupiter connector tests: GATEWAY_TEST_MODE=dev npx jest test/connectors/jupiter/ (25 passed)
  • Run Solana chain tests: GATEWAY_TEST_MODE=dev npx jest test/chains/solana/ (102 passed)
  • Run Meteora connector tests: GATEWAY_TEST_MODE=dev npx jest test/connectors/meteora/ (44 passed)
  • TypeScript typecheck passes
  • Manual testing with hot wallet swap
  • Manual testing with Ledger hardware wallet swap (if available)

🤖 Generated with Claude Code

fengtality and others added 4 commits December 23, 2025 20:53
Introduces a pluggable signer architecture for Solana transactions:

- SolanaSigner interface modeled after @solana/keychain-core
- KeypairSigner for existing encrypted file-based wallets
- LedgerSigner wrapping existing hardware wallet support
- SignerFactory for auto-detecting wallet type and creating signers
- Typed errors with SignerErrorCode for better error handling

This lays the groundwork for:
- Unified signing across all connectors
- Future support for AWS KMS, Fireblocks, and other custody solutions
- Cleaner connector code without duplicated signing logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- getSigner(address) returns abstract SolanaSigner for any wallet type
- signAndSend(tx, signer) provides unified sign + simulate + send flow
- Connectors can now use these methods instead of handling signing themselves

Usage:
  const signer = await solana.getSigner(walletAddress);
  const result = await solana.signAndSend(transaction, signer);

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add buildUnsignedSwapTransaction method for unified signing pattern
- Deprecate buildSwapTransactionForHardwareWallet in favor of new method
- Simplify executeQuote route using getSigner and signAndSend
- Remove direct hardware wallet detection logic from connector

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Test SignerError with error codes and cause
- Test KeypairSigner creation, signing transactions, and signing messages
- Test LedgerSigner creation and hardware wallet integration
- Test SignerFactory for automatic wallet type detection
- Mock fs-extra and wallet utils for isolated testing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@fengtality fengtality changed the base branch from main to development December 24, 2025 05:54
fengtality and others added 3 commits December 23, 2025 22:03
- Update all Meteora CLMM routes to use getSigner and signAndSend
- Add hardware wallet support to addLiquidity, removeLiquidity,
  openPosition, closePosition, collectFees, and executeSwap
- Update signAndSend to accept additionalKeypairs for multi-signer txs
- Update getBalance to accept PublicKey in addition to Keypair
- Update execute-swap tests to mock new signer pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update SignerErrorCode values to use SIGNER_ prefix for keychain-core compatibility
- Add new error codes: PARSING_ERROR, HTTP_ERROR, REMOTE_API_ERROR, IO_ERROR, EXPECTED_SOLANA_SIGNER
- Add SignerErrorContext interface for structured error context
- Update SignerError to accept either string message or context object
- Add utility functions matching keychain-core patterns:
  - createSignerError() - create error without throwing
  - throwSignerError() - create and throw immediately
  - isSolanaSigner() - type guard for SolanaSigner interface
  - assertIsSolanaSigner() - type assertion function
- Export new types and utilities from signers/index.ts

These changes prepare for future migration to @solana/keychain when it's
published to npm, while maintaining full backward compatibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@rapcmia rapcmia added this to the v2.12 milestone Jan 5, 2026
@rapcmia rapcmia self-requested a review January 5, 2026 16:29
@rapcmia rapcmia modified the milestones: v2.12, v2.13 Jan 5, 2026
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.

3 participants