Metadata-Version: 2.4
Name: dynamic-wallet-sdk
Version: 0.1.1
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Rust
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Dist: httpx>=0.27
Requires-Dist: cryptography>=42.0
Requires-Dist: eth-account>=0.13
Requires-Dist: eth-hash[pycryptodome]>=0.7
Requires-Dist: base58>=2.1
Requires-Dist: solders>=0.21
Requires-Dist: pytest>=8.0 ; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
Requires-Dist: pytest-cov>=5.0 ; extra == 'dev'
Requires-Dist: respx>=0.21 ; extra == 'dev'
Requires-Dist: maturin>=1.5 ; extra == 'dev'
Requires-Dist: pynacl>=1.5 ; extra == 'dev'
Provides-Extra: dev
Summary: Dynamic MPC Wallet SDK for Python — create and manage multi-party computation wallets for EVM and Solana
Author-email: Dynamic Labs <support@dynamic.xyz>
License: Apache-2.0
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://docs.dynamic.xyz
Project-URL: Homepage, https://dynamic.xyz
Project-URL: Repository, https://github.com/dynamic-labs/dynamic-wallet-sdk

# Dynamic Wallet SDK for Python

A Python SDK for [Dynamic](https://www.dynamic.xyz/)'s MPC (Multi-Party Computation) wallet infrastructure. Create, sign with, and manage non-custodial wallets from Python — no browser required.

**Guides:** [ARCHITECTURE.md](ARCHITECTURE.md) — internals deep-dive | [DEMO_GUIDE.md](DEMO_GUIDE.md) — build a working demo in minutes

The SDK is split into two layers:

- **`dynamic_wallet_mpc`** — A native Rust extension (via [PyO3](https://pyo3.rs/)) wrapping Dynamic's MPC library. Handles all low-level cryptographic MPC operations: key generation, signing, key refresh, import/export.
- **`dynamic_wallet_sdk`** — Pure Python package providing API client orchestration, key backup encryption (AES-256-GCM), SSE event parsing, and chain-specific logic for EVM and SVM.

## Prerequisites

- **Python 3.10+**
- **Rust toolchain** (for building the native extension) — install via [rustup](https://rustup.rs/)
- **MPC registry access** — the native MPC crate is hosted on a private Cargo registry
- **Dynamic environment ID + API key** — from the [Dynamic dashboard](https://app.dynamic.xyz/)

## Setup

### 1. Clone and navigate

```bash
cd python/
```

### 2. Create a virtual environment

```bash
python3 -m venv .venv
source .venv/bin/activate
```

### 3. Configure the private Cargo registry

The native extension depends on an internal MPC crate hosted on a private Cargo registry.

**`.cargo/config.toml`** (already present in the repo) configures the registry URL.

Add credentials to your global Cargo config:

```bash
cat >> ~/.cargo/credentials.toml << 'EOF'
[registries.dynamic-mpc]
token = "Bearer <YOUR_MPC_REGISTRY_TOKEN>"
EOF
```

The registry token is stored in 1Password. Ask a team member if you don't have access.

### 4. Install Python dependencies

```bash
pip install -e ".[dev]"
```

This installs:

| Dependency                 | Purpose                                                                       |
| -------------------------- | ----------------------------------------------------------------------------- |
| `httpx`                    | Async HTTP client for Dynamic API + SSE streaming                             |
| `cryptography`             | AES-256-GCM encryption, PBKDF2 key derivation, RSA-OAEP for delegated signing |
| `eth-account`              | EIP-712 typed data encoding                                                   |
| `eth-hash[pycryptodome]`   | Keccak-256 hashing for EVM address derivation and EIP-191                     |
| `base58`                   | Solana address encoding                                                       |
| `solders`                  | Solana primitives                                                             |
| `pytest`, `pytest-asyncio` | Testing                                                                       |
| `respx`                    | HTTP mocking for tests                                                        |
| `maturin`                  | Rust-to-Python build tool                                                     |

### 5. Build the native extension

```bash
maturin develop
```

This compiles the Rust code in `src/` and installs the resulting `dynamic_wallet_mpc` native module into your venv. The first build downloads and compiles dependencies, which takes a few minutes. Subsequent builds are incremental (~2-5s).

To verify the build succeeded:

```bash
python -c "from dynamic_wallet_mpc import PyEcdsa, PyEd25519; print('Native module loaded')"
```

## Running Tests

```bash
pytest
```

All tests run in ~2 seconds. The test suite covers:

- `test_encryption.py` — AES-256-GCM encrypt/decrypt round-trips, v1/v2 compatibility, wrong-password detection
- `test_evm_utils.py` — EVM address derivation from public keys, EIP-191 message formatting, EIP-55 checksum encoding, ECDSA signature serialization
- `test_svm_utils.py` — Solana address derivation (base58), hex round-trips, message formatting
- `test_sse.py` — SSE event stream parsing, error events, edge cases
- `test_wallet_client.py` — Wallet client initialization, authentication flow (mocked), wallet map operations, address normalization

Tests don't call any live relay or Dynamic API — they exercise the pure Python logic. The native module is imported only for type checks.

## Project Structure

```
python/
├── .cargo/config.toml              # Private MPC registry URL
├── Cargo.toml                      # Rust crate config: MPC library + pyo3 + tokio
├── pyproject.toml                  # Python package config (maturin build backend)
│
├── src/                            # Rust native extension (PyO3)
│   ├── lib.rs                      # #[pymodule] entry point, tokio runtime init
│   ├── ecdsa.rs                    # PyEcdsa — ECDSA MPC operations
│   ├── ed25519.rs                  # PyEd25519 — standard Ed25519 MPC operations
│   ├── exportable_ed25519.rs       # PyExportableEd25519 — ExportableEd25519 (used for SVM)
│   ├── types.rs                    # Python-visible types (PyInitKeygenResult, PyMessageHash, etc.)
│   ├── error.rs                    # MPCError Python exception
│   └── unsafesend.rs               # SendFuture wrapper for !Send FFI futures
│
├── dynamic_wallet_sdk/             # Pure Python SDK
│   ├── __init__.py                 # Public API re-exports
│   ├── constants.py                # URLs, headers, enums (Environment, ChainType, BackupLocation)
│   ├── mpc_config.py               # TSS schemes, chain configs, derivation paths
│   ├── exceptions.py               # Error hierarchy (DynamicSDKError, AuthenticationError, etc.)
│   │
│   ├── api/
│   │   ├── client.py               # BaseClient: dual httpx clients (API + Evervault relay)
│   │   ├── dynamic_api.py          # All Dynamic API endpoints (SSE + REST)
│   │   └── sse.py                  # SSE event stream parser
│   │
│   ├── crypto/
│   │   └── encryption.py           # AES-256-GCM + PBKDF2 key share backup encryption
│   │
│   ├── mpc/
│   │   ├── signer.py               # get_mpc_signer() factory — returns PyEcdsa or PyExportableEd25519
│   │   └── types.py                # ServerKeyShare, WalletProperties dataclasses
│   │
│   ├── wallet_client.py            # DynamicWalletClient — orchestrates keygen, sign, refresh, export, import, backup
│   │
│   ├── evm/
│   │   ├── client.py               # DynamicEvmWalletClient — EIP-191/712 signing, tx signing
│   │   └── utils.py                # EVM address derivation, message formatting, signature serialization
│   │
│   ├── svm/
│   │   ├── client.py               # DynamicSvmWalletClient — Solana message/tx signing
│   │   └── utils.py                # Solana address derivation, message formatting
│   │
│   └── delegated/
│       ├── client.py               # Delegated signing — factory + sign with delegated key shares
│       └── decrypt.py              # RSA-OAEP + AES-GCM webhook decryption
│
└── tests/
    ├── conftest.py                 # Shared fixtures
    ├── test_encryption.py
    ├── test_evm_utils.py
    ├── test_svm_utils.py
    ├── test_sse.py
    └── test_wallet_client.py
```

## Architecture

### Native Extension (`dynamic_wallet_mpc`)

The Rust layer exposes these classes to Python:

| Class                   | Description                                                                          |
| ----------------------- | ------------------------------------------------------------------------------------ |
| `PyEcdsa`               | ECDSA MPC operations (EVM, BTC segwit)                                               |
| `PyEd25519`             | Ed25519 MPC operations                                                               |
| `PyExportableEd25519`   | ExportableEd25519 MPC operations (Solana)                                            |
| `PyInitKeygenResult`    | Returned by `init_keygen()` — contains `keygen_id` (str) and `keygen_secret` (bytes) |
| `PyEcdsaKeygenResult`   | ECDSA keygen output — `pubkey_uncompressed`, `pubkey_compressed`, `secret_share`     |
| `PyEd25519KeygenResult` | Ed25519 keygen output — `pubkey` (32 bytes), `secret_share`                          |
| `PyEcdsaSignature`      | ECDSA signature — `r`, `s`, `v`, `der`                                               |
| `PyMessageHash`         | 32-byte message hash with `sha256()`, `keccak256()` static constructors              |

**Key API details:**

- `init_keygen()`, `derive_pubkey()`, and `export_id()` are **synchronous**
- `keygen()`, `sign()`, `refresh()`, `export_full_private_key()`, `import_*`, `reshare_*` are **async** (return Python awaitables)
- All async operations share a single tokio runtime initialized at module load
- The `SendFuture` wrapper in `unsafesend.rs` makes the MPC library's FFI futures compatible with `pyo3_async_runtimes::tokio::future_into_py`

### Python SDK (`dynamic_wallet_sdk`)

The Python layer handles everything above the MPC primitives:

- **Authentication** — Exchange API tokens for JWTs via Dynamic's API
- **Keygen orchestration** — `init_keygen()` → SSE call to Dynamic API → MPC `keygen()` → derive address
- **Sign orchestration** — SSE call for signing room → MPC `sign()` → format chain-specific signature
- **Key backup** — Encrypt key shares with AES-256-GCM (PBKDF2, 1M iterations) and store via Evervault relay
- **Key recovery** — Retrieve and decrypt backed-up key shares
- **Key export/import** — Full private key export via MPC, or import existing keys into MPC
- **Delegated signing** — Sign using server-held delegated key shares, decrypt webhook payloads

### Chain Support

| Chain         | Algorithm | Client Class             | Derivation Path     |
| ------------- | --------- | ------------------------ | ------------------- |
| EVM           | ECDSA     | `DynamicEvmWalletClient` | `m/44'/60'/0'/0/0`  |
| SVM (Solana)  | Ed25519   | `DynamicSvmWalletClient` | `m/44'/501'/0'/0/0` |
| Cosmos        | Ed25519   | —                        | `m/44'/118'/0'/0/0` |
| Sui           | Ed25519   | —                        | `m/44'/784'/0'/0/0` |
| TON           | Ed25519   | —                        | `m/44'/607'/0'/0/0` |
| Stellar       | Ed25519   | —                        | `m/44'/148'/0'/0/0` |
| BTC (taproot) | BIP340    | —                        | `m/86'/0'/0'/0/0`   |
| BTC (segwit)  | ECDSA     | —                        | `m/84'/0'/0'/0/0`   |

EVM and SVM have full client implementations. Other chains have config entries but need client classes.

## Usage

### EVM Wallet

```python
import asyncio
from dynamic_wallet_sdk.evm.client import DynamicEvmWalletClient

async def main():
    async with DynamicEvmWalletClient("your-environment-id") as client:
        # Authenticate
        await client.authenticate_api_token("your-api-token")

        # Create wallet (keygen)
        address = await client.create_wallet_account(password="backup-password")
        print(f"Created EVM wallet: {address}")

        # Sign a message (EIP-191)
        signature = await client.sign_message(
            message="Hello, Dynamic!",
            address=address,
            password="backup-password",
        )
        print(f"Signature: {signature}")

        # Sign EIP-712 typed data
        typed_data = {
            "types": {...},
            "primaryType": "...",
            "domain": {...},
            "message": {...},
        }
        sig = await client.sign_typed_data(address, typed_data, password="backup-password")

asyncio.run(main())
```

### SVM (Solana) Wallet

```python
import asyncio
from dynamic_wallet_sdk.svm.client import DynamicSvmWalletClient

async def main():
    async with DynamicSvmWalletClient("your-environment-id") as client:
        await client.authenticate_api_token("your-api-token")

        address = await client.create_wallet_account(password="backup-password")
        print(f"Created Solana wallet: {address}")

        # Sign a message
        signature = await client.sign_message(
            message="Hello from Solana!",
            address=address,
            password="backup-password",
        )
        print(f"Signature (base58): {signature}")

asyncio.run(main())
```

### Delegated Signing

For server-side signing where key shares are held by the customer's server:

```python
from dynamic_wallet_sdk.delegated.client import (
    create_delegated_evm_client,
    delegated_sign_message,
)
from dynamic_wallet_sdk.delegated.decrypt import decrypt_delegated_webhook_data

# 1. Decrypt the webhook data you received
decrypted = decrypt_delegated_webhook_data(
    private_key_pem=rsa_private_key,
    encrypted_delegated_key_share=webhook["encryptedDelegatedShare"],
    encrypted_wallet_api_key=webhook["encryptedWalletApiKey"],
)

# 2. Create a client and sign
client = await create_delegated_evm_client("env-id", "api-key")
sig = await delegated_sign_message(
    client,
    wallet_id="wallet-id",
    wallet_api_key=decrypted.decrypted_wallet_api_key,
    key_share=decrypted.decrypted_delegated_share["secretShare"],
    message="0x...",
    chain_name="EVM",
    is_formatted=True,
)
```

### Using the Native Module Directly

If you want to use the MPC primitives without the SDK orchestration:

```python
from dynamic_wallet_mpc import PyEcdsa, PyEd25519, PyMessageHash

# ECDSA
ecdsa = PyEcdsa("https://relay.dynamicauth.com")
init = ecdsa.init_keygen()  # synchronous
print(init.keygen_id, len(init.keygen_secret))  # str, 32 bytes

# Ed25519
ed = PyEd25519("https://relay.dynamicauth.com")
init = ed.init_keygen()

# Hashing
h = PyMessageHash.keccak256(b"hello world")
print(h.to_hex())

h2 = PyMessageHash.sha256(b"hello world")
h3 = PyMessageHash("ab" * 32)  # from 64-char hex string
```

## Key Concepts

### Threshold Signature Schemes

The SDK supports three TSS configurations:

| Scheme          | Parties | Threshold | Client Shares | Server Shares |
| --------------- | ------- | --------- | ------------- | ------------- |
| `TWO_OF_TWO`    | 2       | 2         | 1             | 1             |
| `TWO_OF_THREE`  | 3       | 2         | 2             | 1             |
| `THREE_OF_FIVE` | 5       | 3         | 3             | 2             |

### Wallet Map

The `DynamicWalletClient` maintains an in-memory `_wallet_map` keyed by normalized address. Always use the accessor methods:

```python
wp = client.get_wallet_from_map(address)       # raises WalletNotFoundError if missing
client.update_wallet_in_map(address, wallet_id="new-id")
```

EVM addresses are normalized to lowercase. Solana addresses are stored as-is.

### Key Share Backup

Key shares are encrypted with AES-256-GCM using a password-derived key (PBKDF2, SHA-256, 1M iterations). Two versions exist:

- **v1** — 100K PBKDF2 iterations (legacy)
- **v2** — 1M PBKDF2 iterations (current default)

Decryption auto-detects the version. Encrypted backups are stored through the Evervault keyshares relay.

### SSE Communication

Most mutating API calls (create wallet, sign, refresh, export, import) use Server-Sent Events:

1. Client POSTs to the API endpoint with `Accept: text/event-stream`
2. Server creates an MPC room and streams events
3. Client waits for a success event (`keygen_complete` or `room_created`)
4. Client uses the room ID to perform the MPC operation via the Dynamic relay

Timeout is 30 seconds by default. See `api/sse.py`.

## Build Commands

| Command                           | Description                                              |
| --------------------------------- | -------------------------------------------------------- |
| `maturin develop`                 | Build native extension in dev mode and install into venv |
| `maturin develop --release`       | Build with optimizations                                 |
| `maturin build --release`         | Build a distributable wheel in `target/wheels/`          |
| `pytest`                          | Run all tests                                            |
| `pytest -v`                       | Run tests with verbose output                            |
| `pytest tests/test_encryption.py` | Run a specific test file                                 |

## Troubleshooting

### `cargo` not found

Install the Rust toolchain:

```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
```

### Registry authentication failure

If you see `authenticated registries require a credential-provider to be available`:

1. Verify `~/.cargo/credentials.toml` has the `[registries.dynamic-mpc]` section with your registry token
2. Verify `.cargo/config.toml` has `global-credential-providers = ["cargo:token"]`

### `maturin develop` fails with compilation errors

Make sure you're using a compatible Rust version (1.75+). Update with:

```bash
rustup update stable
```

### ImportError for `dynamic_wallet_mpc`

The native module wasn't built or the venv isn't activated. Run:

```bash
source .venv/bin/activate
maturin develop
```

### Slow encryption tests

PBKDF2 with 1M iterations takes ~1-2 seconds per encrypt/decrypt. This is by design.

