Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

solana-keychain (Python)

Flexible, framework-agnostic Solana transaction signing for Python applications

solana-keychain provides a unified interface for signing Solana transactions with multiple backend implementations. Whether you need local keypairs for development, enterprise vault integration, or managed wallet services, this library offers a consistent API across all signing methods.

Features

  • Unified interface: a single SolanaSigner contract for every backend
  • Async-first: sign_transaction / sign_message / is_available are coroutines
  • Verified wire format: golden-vector tests pin the exact serialized transaction bytes, so serialization can never silently drift
  • Safe errors: SignerError redacts sensitive detail from its message; match on its stable code values
  • Minimal core: built on solders for canonical transaction serialization and Ed25519 primitives

Supported Backends

Backend Use Case Module Status
Memory Local keypairs, development, testing solana_keychain.memory ✅ Available
Vault Enterprise key management with HashiCorp Vault solana_keychain.vault ✅ Available
Privy Embedded wallets with Privy infrastructure solana_keychain.privy ✅ Available
Turnkey Non-custodial key management via Turnkey solana_keychain.turnkey ✅ Available
AWS KMS AWS Key Management Service with Ed25519 signing solana_keychain.aws_kms ✅ Available
Fireblocks Fireblocks institutional custody platform solana_keychain.fireblocks ✅ Available
Fordefi Fordefi institutional MPC custody platform solana_keychain.fordefi ✅ Available
GCP KMS Google Cloud Key Management Service with Ed25519 signing solana_keychain.gcp_kms ✅ Available
Dfns Dfns wallet infrastructure with Ed25519 signing solana_keychain.dfns ✅ Available
Para MPC wallets with Para infrastructure solana_keychain.para ✅ Available
CDP Coinbase Developer Platform managed wallets solana_keychain.cdp ✅ Available
Crossmint Crossmint managed wallets solana_keychain.crossmint ✅ Available
Openfort Openfort backend wallets with TEE-stored keys solana_keychain.openfort ✅ Available
Utila Utila MPC wallet integration solana_keychain.utila ✅ Available

Installation

pip install solana-keychain              # memory + vault
pip install 'solana-keychain[aws-kms]'   # adds the AWS KMS backend
pip install 'solana-keychain[cdp]'       # adds the CDP backend
pip install 'solana-keychain[crossmint]' # adds the Crossmint backend
pip install 'solana-keychain[dfns]'      # adds the Dfns backend
pip install 'solana-keychain[fireblocks]' # adds the Fireblocks backend
pip install 'solana-keychain[fordefi]'   # adds the Fordefi backend
pip install 'solana-keychain[gcp-kms]'   # adds the GCP KMS backend
pip install 'solana-keychain[openfort]'  # adds the Openfort backend
pip install 'solana-keychain[privy]'     # adds the Privy backend
pip install 'solana-keychain[turnkey]'   # adds the Turnkey backend
pip install 'solana-keychain[utila]'     # adds the Utila backend

Requires Python 3.10+. Backends built on heavy provider SDKs ship as optional extras; importing such a backend without its extra raises an ImportError naming the extra to install. Extras-gated backends are imported from their submodule (e.g. from solana_keychain.aws_kms import create_aws_kms_signer), not from the package root.

Quick Start

Memory Signer (Local Development)

import asyncio

from solana_keychain import MemorySigner


async def main() -> None:
    # Build a signer from a base58 key, a "[1,2,...]" byte array, raw bytes,
    # or a Solana CLI keypair file.
    signer = MemorySigner.from_private_key_file("/path/to/keypair.json")
    print("address:", signer.pubkey)

    # Sign an arbitrary message.
    signature = await signer.sign_message(b"Hello Solana!")
    print("signature:", signature)

    # Sign a transaction (tx is a solders.transaction.VersionedTransaction):
    #   result = await signer.sign_transaction(tx)
    #   result.encoded_transaction  # base64 wire transaction
    #   result.signature            # this signer's signature
    #   result.is_complete          # are all required signatures present?
    #   result.transaction          # the authoritative signed transaction


asyncio.run(main())

Remote Backends

Every remote backend follows the same pattern: a config dataclass and an async create_<backend>_signer factory that returns a ready-to-use signer:

from solana_keychain import VaultSignerConfig, create_vault_signer

signer = await create_vault_signer(
    VaultSignerConfig(
        api_base_url="https://vault.example.com",
        token=os.environ["VAULT_TOKEN"],
        key_name="my-solana-key",
        public_key="4BuiY9QUUfPoAGNJBja3JapAuVWMc9c7in6UCgyC2zPR",
    )
)

Remote HTTP backends accept an optional http_client override in their config (an httpx.AsyncClient, for custom TLS or proxies); when unset, requests go through an HTTPS-enforcing one-shot client with a 60s timeout and redirects rejected.

Core API

Every backend subclasses the SolanaSigner ABC from solana_keychain.core, plus exactly the capability class matching its provider's shape:

class SolanaSigner(ABC):
    @property
    def pubkey(self) -> Pubkey: ...

    async def sign_message(self, message: bytes) -> Signature: ...

    async def is_available(self) -> bool: ...


class TransactionSigner(SolanaSigner):
    """Signs the caller's transaction as given; the caller broadcasts the result."""

    async def sign_transaction(self, transaction: VersionedTransaction) -> SignedTransaction: ...


class ModifyingSigner(SolanaSigner):
    """The provider rewrites the transaction before signing it; continue from
    `SignedTransaction.transaction`, never from the bytes submitted."""

    async def modify_and_sign_transaction(
        self, transaction: VersionedTransaction
    ) -> SignedTransaction: ...


class SendingSigner(SolanaSigner):
    """The provider signs and broadcasts server-side; the caller's transaction is
    never mutated, and the returned signature identifies what landed."""

    async def sign_and_send_transaction(self, transaction: VersionedTransaction) -> Signature: ...

Both signing entry points return a SignedTransaction(encoded_transaction, signature, is_complete, transaction); is_complete reports whether every required signature is present. A TransactionSigner signs the transaction in place and hands it back as transaction; a ModifyingSigner leaves the caller's object untouched, because solders messages are read-only, and hands back the provider's rewritten transaction instead. Only transaction is guaranteed to match encoded_transaction and the bytes signature covers. Legacy, v0 and v1 transactions are all accepted.

Errors are always SignerError with a stable code (SIGNER_INVALID_PRIVATE_KEY, SIGNER_SIGNING_FAILED, …). str()/repr() of a SignerError never include key material or raw remote responses.

Signer capabilities

The capability class a backend subclasses says whether the provider broadcasts; whether it can sign arbitrary bytes is fixed per backend:

Backend Capability class sign_message
memory, vault, privy, turnkey, aws-kms, fireblocks, gcp-kms, dfns, para, openfort TransactionSigner yes
cdp TransactionSigner UTF-8 payloads only, otherwise SERIALIZATION_ERROR
utila TransactionSigner SIGNING_FAILED
crossmint SendingSigner SIGNING_FAILED
fordefi black box (FordefiBlackBoxSigner) TransactionSigner yes
fordefi native auto (FordefiNativeAutoSigner) SendingSigner yes
fordefi native manual (FordefiNativeManualSigner) ModifyingSigner yes

Crossmint executes every approved transaction server-side and exposes no sign-only API, so it is a SendingSigner only. It may rewrite the transaction to sponsor gas, in which case the returned signature identifies the transaction it landed rather than covering the caller's bytes; the caller's transaction is never modified.

Fordefi signing modes

create_fordefi_signer picks the Fordefi type from config.chain and config.push_mode, and each type rejects a config meant for another:

Config Signer Entry point
no chain FordefiBlackBoxSigner sign_transaction
chain, push_mode unset or "auto" FordefiNativeAutoSigner sign_and_send_transaction
chain, push_mode="manual" FordefiNativeManualSigner modify_and_sign_transaction

Black-box mode signs the caller's exact message bytes and leaves broadcasting to the caller. Native auto lets Fordefi update the blockhash and fees, then sign and broadcast; the caller's transaction is left untouched and the returned signature identifies what landed.

Native manual lets Fordefi rewrite the recent blockhash and the Compute Budget fee instructions, then sign without broadcasting, so the caller broadcasts. The returned signature covers Fordefi's bytes, not the ones submitted, and the rewrite is not diffed against them: Fordefi is trusted for the rewrite. Inspect result.transaction before broadcasting it. Fordefi must be the fee payer, so a transaction that is not vault-paid is rejected before submitting, and it must sign before every downstream signer: a transaction that already carries signatures is accepted, but the rewrite voids them and the returned transaction carries only Fordefi's.

The signature is ed25519-verified against the returned transaction's own message at the vault's required-signer position; a signature that does not verify, or a returned transaction the vault does not sign, fails with SIGNER_SIGNING_FAILED and leaves the caller's transaction untouched.

import os

from solana_keychain.fordefi import FordefiSignerConfig, create_fordefi_signer

signer = await create_fordefi_signer(
    FordefiSignerConfig(
        access_token=os.environ["FORDEFI_ACCESS_TOKEN"],
        vault_id=os.environ["FORDEFI_VAULT_ID"],
        public_key=os.environ["FORDEFI_PUBLIC_KEY"],
        private_key_pem=os.environ["FORDEFI_PRIVATE_KEY_PEM"],
        chain="solana_mainnet",
        push_mode="manual",
    )
)

result = await signer.modify_and_sign_transaction(transaction)
if result.is_complete:
    # Broadcast result.encoded_transaction through your RPC client.
    pass
else:
    # Sign result.transaction with the downstream signers, reserialize, broadcast.
    pass

Bound what Fordefi may spend through config.fee, for example {"type": "custom", "priority_fee": "1000"}; that request is what Fordefi honours, and the returned fee instructions are not checked against it locally.

Fordefi normally refreshes the blockhash but does not return its exact lastValidBlockHeight, so broadcast manual results promptly rather than relying on a locally known block-height expiry.

Sign and Send

sign_and_send_transaction gets a transaction on chain with one call. A SendingSigner (Crossmint, Fordefi native auto) broadcasts through its provider and the send function is never called; a TransactionSigner or ModifyingSigner signs and the send function broadcasts the base64-encoded result:

from solana_keychain import sign_and_send_transaction

signature = await sign_and_send_transaction(signer, transaction, rpc_send)

Development

From the repo root (recipes bootstrap python/.venv automatically):

just py-test    # unit tests
just py-fmt     # ruff format + lint + mypy
just py-build   # sdist + wheel

Or manually:

cd python
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/pytest

Golden wire-format vectors are pinned in tests/test_parity.py — the exact serialized bytes for one canonical transaction. Never regenerate them to make the suite pass; a mismatch means the library's output has drifted from the Solana wire format.