This release is a pre-release and may not be stable for production use.
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
SolanaSignercontract for every backend - Async-first:
sign_transaction/sign_message/is_availableare coroutines - Verified wire format: golden-vector tests pin the exact serialized transaction bytes, so serialization can never silently drift
- Safe errors:
SignerErrorredacts sensitive detail from its message; match on its stablecodevalues - Minimal core: built on
soldersfor 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 and
must sign before every downstream signer, so a transaction that is not vault-paid
or already carries a signature is rejected before submitting.
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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file solana_keychain-2.0.0b1.tar.gz.
File metadata
- Download URL: solana_keychain-2.0.0b1.tar.gz
- Upload date:
- Size: 94.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aeae0c2a41b3b4e666c88bd8a8d403af61efd435d3c346d735248cf6cf22ecd8
|
|
| MD5 |
8f5552554ecb270be4cb9ab3c25ff94c
|
|
| BLAKE2b-256 |
8af29fe90385a1f0c13993a7cb926c9e4ad663b9e8a015f11c8c966fcd4cb482
|
Provenance
The following attestation bundles were made for solana_keychain-2.0.0b1.tar.gz:
Publisher:
python-publish.yml on solana-foundation/solana-keychain
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
solana_keychain-2.0.0b1.tar.gz -
Subject digest:
aeae0c2a41b3b4e666c88bd8a8d403af61efd435d3c346d735248cf6cf22ecd8 - Sigstore transparency entry: 2618187772
- Sigstore integration time:
-
Permalink:
solana-foundation/solana-keychain@235f33d01681a3393da9e411535b6e0d7d78eb0d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/solana-foundation
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@235f33d01681a3393da9e411535b6e0d7d78eb0d -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file solana_keychain-2.0.0b1-py3-none-any.whl.
File metadata
- Download URL: solana_keychain-2.0.0b1-py3-none-any.whl
- Upload date:
- Size: 78.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c088714e357fa7be623b5da446096ee6ece0d2c77bb37a14eb83906272245d0
|
|
| MD5 |
d7ecf68c6b6afcb3692b2a909684db08
|
|
| BLAKE2b-256 |
6918b466d60ce78076eae9dd86440f16700817b4b2332de0e230aa9a42aaf004
|
Provenance
The following attestation bundles were made for solana_keychain-2.0.0b1-py3-none-any.whl:
Publisher:
python-publish.yml on solana-foundation/solana-keychain
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
solana_keychain-2.0.0b1-py3-none-any.whl -
Subject digest:
0c088714e357fa7be623b5da446096ee6ece0d2c77bb37a14eb83906272245d0 - Sigstore transparency entry: 2618187839
- Sigstore integration time:
-
Permalink:
solana-foundation/solana-keychain@235f33d01681a3393da9e411535b6e0d7d78eb0d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/solana-foundation
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@235f33d01681a3393da9e411535b6e0d7d78eb0d -
Trigger Event:
workflow_dispatch
-
Statement type: