Metadata-Version: 2.5
Name: solana-keychain
Version: 2.0.0b1
Summary: Unified SolanaSigner interface across key-management backends
Project-URL: Repository, https://github.com/solana-foundation/solana-keychain
Author: Solana Foundation
License-Expression: MIT
License-File: LICENSE
Keywords: ed25519,keychain,signer,solana
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: solders<1,>=0.29
Provides-Extra: aws-kms
Requires-Dist: boto3>=1.35; extra == 'aws-kms'
Provides-Extra: cdp
Requires-Dist: base58>=2.1; extra == 'cdp'
Requires-Dist: cryptography>=43; extra == 'cdp'
Requires-Dist: pyjwt>=2.8; extra == 'cdp'
Provides-Extra: crossmint
Requires-Dist: base58>=2.1; extra == 'crossmint'
Requires-Dist: cryptography>=43; extra == 'crossmint'
Provides-Extra: dev
Requires-Dist: base58>=2.1; extra == 'dev'
Requires-Dist: boto3>=1.35; extra == 'dev'
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: cryptography>=43; extra == 'dev'
Requires-Dist: google-cloud-kms>=2.24; extra == 'dev'
Requires-Dist: mypy>=1.14; extra == 'dev'
Requires-Dist: pyjwt>=2.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: respx>=0.22; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Provides-Extra: dfns
Requires-Dist: cryptography>=43; extra == 'dfns'
Provides-Extra: fireblocks
Requires-Dist: cryptography>=43; extra == 'fireblocks'
Requires-Dist: pyjwt>=2.8; extra == 'fireblocks'
Provides-Extra: fordefi
Requires-Dist: cryptography>=43; extra == 'fordefi'
Provides-Extra: gcp-kms
Requires-Dist: google-cloud-kms>=2.24; extra == 'gcp-kms'
Provides-Extra: openfort
Requires-Dist: cryptography>=43; extra == 'openfort'
Requires-Dist: pyjwt>=2.8; extra == 'openfort'
Provides-Extra: privy
Requires-Dist: cryptography>=43; extra == 'privy'
Provides-Extra: turnkey
Requires-Dist: cryptography>=43; extra == 'turnkey'
Provides-Extra: utila
Requires-Dist: cryptography>=43; extra == 'utila'
Requires-Dist: pyjwt>=2.8; extra == 'utila'
Description-Content-Type: text/markdown

# 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`](https://pypi.org/project/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

```bash
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)

```python
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:

```python
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:

```python
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.

```python
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:

```python
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):

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

Or manually:

```bash
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.
