diff --git a/sdks/golang.mdx b/sdks/golang.mdx index 64792fe8..b8f37e86 100644 --- a/sdks/golang.mdx +++ b/sdks/golang.mdx @@ -1,6 +1,388 @@ --- -title: "Golang" -description: "Turnkey offers native tooling for interacting with the API using Golang. See [https://github.com/tkhq/go-sdk](https://github.com/tkhq/go-sdk) for more details." +title: "Go" +description: "The Turnkey Go SDK provides a native interface for interacting with the Turnkey API, including API key signing, wallet management, and HPKE encryption for secure enclave communication." +sidebarTitle: "Go" mode: wide --- +## Overview + +The [Turnkey Go SDK](https://github.com/tkhq/go-sdk) provides a complete implementation for interacting with the Turnkey API from Go applications. It includes: + +- **Swagger-generated API client** with 300+ typed request/response models +- **Request signing** supporting P-256, secp256k1, and ED25519 API keys +- **HPKE encryption** for secure wallet export/import with Turnkey's enclaves +- **Nitro attestation verification** for validating enclave proofs + + +Go is the only Turnkey SDK that supports **ED25519 API key signing**, which is useful for chains like Near, Aptos, and Sui that use ED25519 natively. + + +## Installation + +```bash +go get github.com/tkhq/go-sdk +``` + +## Authentication + +The Go SDK uses API keys to sign requests via the `X-Stamp` header. API keys can be generated on the [Turnkey Dashboard](https://app.turnkey.com) or via the CLI. + +### Loading an API Key + +```go +import ( + sdk "github.com/tkhq/go-sdk" + "github.com/tkhq/go-sdk/pkg/apikey" + "github.com/tkhq/go-sdk/pkg/store/local" +) + +// Load from ~/.config/turnkey/keys/ directory +client, err := sdk.New(sdk.WithAPIKeyName("default")) + +// Or load from a Turnkey private key string directly +apiKey, err := apikey.FromTurnkeyPrivateKey(privateKeyHex, apikey.SchemeP256) +client, err := sdk.New(sdk.WithAPIKey(apiKey)) +``` + +### Supported Signature Schemes + +| Scheme | Constant | Use Case | +|--------|----------|----------| +| P-256 (secp256r1) | `apikey.SchemeP256` | Default, most common | +| secp256k1 | `apikey.SchemeSECP256K1` | Ethereum-compatible | +| ED25519 | `apikey.SchemeED25519` | Near, Aptos, Sui | + +## Client Setup + +```go +import ( + sdk "github.com/tkhq/go-sdk" +) + +client, err := sdk.New( + sdk.WithAPIKeyName("default"), + // Optional: custom transport config + // sdk.WithTransportConfig(customConfig), +) +if err != nil { + log.Fatalf("Failed to create client: %v", err) +} +``` + + +**Important**: You must pass `client.Authenticator` to every API call. The Go SDK does not set a global default authenticator. + +```go +// ✅ Correct - pass Authenticator explicitly +resp, err := client.V0().Sessions.GetWhoami(params, client.Authenticator) + +// ❌ Wrong - missing Authenticator will fail +resp, err := client.V0().Sessions.GetWhoami(params, nil) +``` + + +## Usage Examples + +### Get Whoami + +The simplest API call to verify your credentials: + +```go +import ( + sdk "github.com/tkhq/go-sdk" + "github.com/tkhq/go-sdk/pkg/api/client/sessions" + "github.com/tkhq/go-sdk/pkg/api/models" +) + +client, _ := sdk.New(sdk.WithAPIKeyName("default")) + +params := sessions.NewGetWhoamiParams().WithBody(&models.GetWhoamiRequest{ + OrganizationID: client.DefaultOrganization(), +}) + +resp, err := client.V0().Sessions.GetWhoami(params, client.Authenticator) +if err != nil { + log.Fatalf("Whoami failed: %v", err) +} + +fmt.Printf("User ID: %s\n", *resp.Payload.UserID) +fmt.Printf("Organization ID: %s\n", *resp.Payload.OrganizationID) +``` + +### Create Wallet + +```go +import ( + "github.com/tkhq/go-sdk/pkg/api/client/wallets" + "github.com/tkhq/go-sdk/pkg/api/models" + "github.com/tkhq/go-sdk/pkg/util" +) + +walletName := "My Wallet" +path := "m/44'/60'/0'/0/0" + +params := wallets.NewCreateWalletParams().WithBody(&models.CreateWalletRequest{ + OrganizationID: client.DefaultOrganization(), + TimestampMs: util.RequestTimestamp(), + Type: (*string)(models.ActivityTypeCreateWallet.Pointer()), + Parameters: &models.CreateWalletIntent{ + WalletName: &walletName, + Accounts: []*models.WalletAccountParams{{ + AddressFormat: models.AddressFormatEthereum.Pointer(), + Curve: models.CurveSecp256k1.Pointer(), + Path: &path, + PathFormat: models.PathFormatBip32.Pointer(), + }}, + }, +}) + +resp, err := client.V0().Wallets.CreateWallet(params, client.Authenticator) +if err != nil { + log.Fatalf("Failed to create wallet: %v", err) +} + +walletID := resp.Payload.Activity.Result.CreateWalletResult.WalletID +address := resp.Payload.Activity.Result.CreateWalletResult.Addresses[0] +fmt.Printf("Wallet ID: %s\n", *walletID) +fmt.Printf("Address: %s\n", *address) +``` + +### Sign a Message + +```go +import ( + "github.com/tkhq/go-sdk/pkg/api/client/signing" + "github.com/tkhq/go-sdk/pkg/api/models" +) + +params := signing.NewSignRawPayloadParams().WithBody(&models.SignRawPayloadRequest{ + OrganizationID: client.DefaultOrganization(), + TimestampMs: util.RequestTimestamp(), + Type: (*string)(models.ActivityTypeSignRawPayloadV2.Pointer()), + Parameters: &models.SignRawPayloadIntentV2{ + SignWith: &address, + Payload: stringPtr("hello world"), + Encoding: models.PayloadEncodingTextUtf8.Pointer(), + HashFunction: models.HashFunctionKeccak256.Pointer(), + }, +}) + +resp, err := client.V0().Signing.SignRawPayload(params, client.Authenticator) +if err != nil { + log.Fatalf("Failed to sign: %v", err) +} + +signature := resp.Payload.Activity.Result.SignRawPayloadResult +fmt.Printf("R: %s\n", *signature.R) +fmt.Printf("S: %s\n", *signature.S) +fmt.Printf("V: %s\n", *signature.V) +``` + +### Email OTP Authentication + +```go +import ( + "github.com/tkhq/go-sdk/pkg/api/client/sessions" + "github.com/tkhq/go-sdk/pkg/api/models" +) + +// Step 1: Initialize OTP +initParams := sessions.NewInitOtpParams().WithBody(&models.InitOtpRequest{ + OrganizationID: &orgID, + TimestampMs: util.RequestTimestamp(), + Type: (*string)(models.ActivityTypeInitOtp.Pointer()), + Parameters: &models.InitOtpIntent{ + OtpType: models.OtpTypeOtpTypeEmail.Pointer(), + Contact: &email, + }, +}) + +initResp, err := client.V0().Sessions.InitOtp(initParams, client.Authenticator) +// Handle error, extract otpID from result + +// Step 2: User receives code, then verify +verifyParams := sessions.NewOtpParams().WithBody(&models.OtpRequest{ + OrganizationID: &orgID, + TimestampMs: util.RequestTimestamp(), + Type: (*string)(models.ActivityTypeOtp.Pointer()), + Parameters: &models.OtpIntent{ + OtpID: &otpID, + OtpCode: &userProvidedCode, + }, +}) + +verifyResp, err := client.V0().Sessions.Otp(verifyParams, client.Authenticator) + +// Step 3: Create authenticated session +authParams := sessions.NewOtpAuthParams().WithBody(&models.OtpAuthRequest{ + OrganizationID: &orgID, + TimestampMs: util.RequestTimestamp(), + Type: (*string)(models.ActivityTypeOtpAuth.Pointer()), + Parameters: &models.OtpAuthIntent{ + OtpID: &otpID, + TargetPublicKey: &targetPublicKey, + SessionLengthSeconds: int64Ptr(3600), + }, +}) + +authResp, err := client.V0().Sessions.OtpAuth(authParams, client.Authenticator) +``` + +## Activity Polling + + +**The Go SDK does not auto-poll for activity completion.** Unlike TypeScript and Rust, you must implement polling yourself for activities that may be pending. + + +```go +import ( + "time" + "github.com/tkhq/go-sdk/pkg/api/client/activities" + "github.com/tkhq/go-sdk/pkg/api/models" +) + +func pollForCompletion(client *sdk.Client, activityID, orgID string) (*models.V1Activity, error) { + maxAttempts := 10 + for i := 0; i < maxAttempts; i++ { + params := activities.NewGetActivityParams().WithBody(&models.GetActivityRequest{ + OrganizationID: &orgID, + ActivityID: &activityID, + }) + + resp, err := client.V0().Activities.GetActivity(params, client.Authenticator) + if err != nil { + return nil, err + } + + status := resp.Payload.Activity.Status + switch *status { + case models.ActivityStatusActivityStatusCompleted: + return resp.Payload.Activity, nil + case models.ActivityStatusActivityStatusFailed: + return nil, fmt.Errorf("activity failed") + case models.ActivityStatusActivityStatusPending: + time.Sleep(time.Second) + continue + default: + return nil, fmt.Errorf("unexpected status: %s", *status) + } + } + return nil, fmt.Errorf("polling timeout") +} +``` + +## HPKE Encryption (Wallet Export) + +The Go SDK includes full HPKE support for encrypting/decrypting wallet exports: + +```go +import ( + "github.com/tkhq/go-sdk/pkg/encryptionkey" + "github.com/tkhq/go-sdk/pkg/enclave_encrypt" +) + +// 1. Generate a client-side encryption key +encryptionKey, err := encryptionkey.New(userID, organizationID) + +// 2. Export wallet (encrypted to your target key) +params := wallets.NewExportWalletParams().WithBody(&models.ExportWalletRequest{ + OrganizationID: &orgID, + TimestampMs: util.RequestTimestamp(), + Type: (*string)(models.ActivityTypeExportWallet.Pointer()), + Parameters: &models.ExportWalletIntent{ + WalletID: &walletID, + TargetPublicKey: &encryptionKey.TkPublicKey, + }, +}) + +resp, err := client.V0().Wallets.ExportWallet(params, client.Authenticator) +exportBundle := resp.Payload.Activity.Result.ExportWalletResult.ExportBundle + +// 3. Decrypt locally +kemPrivateKey, _ := encryptionkey.DecodeTurnkeyPrivateKey(encryptionKey.GetPrivateKey()) +signerKey, _ := hexToPublicKey(encryptionkey.SignerProductionPublicKey) +encryptClient, _ := enclave_encrypt.NewEnclaveEncryptClientFromTargetKey(signerKey, *kemPrivateKey) +mnemonic, err := encryptClient.Decrypt([]byte(*exportBundle), organizationID) +``` + +## Nitro Attestation Verification + +The Go SDK can verify AWS Nitro attestation documents from Turnkey's enclaves: + +```go +import ( + "github.com/tkhq/go-sdk/pkg/proofs" +) + +// Verify an attestation document +doc, err := proofs.VerifyNitroAttestation(attestationBytes, timestamp) +if err != nil { + log.Fatalf("Attestation verification failed: %v", err) +} + +// Access verified enclave measurements +fmt.Printf("PCR0: %x\n", doc.PCR0) +``` + +## Ethereum Integration + +The Go SDK includes examples for integrating with go-ethereum: + +```go +import ( + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" +) + +// Create a Turnkey-backed SignerFn for go-ethereum +func MakeTurnkeySignerFn(client *sdk.Client, signWith string, chainID *big.Int, orgID string) bind.SignerFn { + return func(from common.Address, tx *types.Transaction) (*types.Transaction, error) { + // Build unsigned EIP-1559 payload + unsignedPayload := []any{ + tx.ChainId(), tx.Nonce(), tx.GasTipCap(), tx.GasFeeCap(), + tx.Gas(), tx.To(), tx.Value(), tx.Data(), tx.AccessList(), + } + rlpBytes, _ := rlp.EncodeToBytes(unsignedPayload) + unsigned := append([]byte{types.DynamicFeeTxType}, rlpBytes...) + + // Sign with Turnkey + params := signing.NewSignTransactionParams().WithBody(&models.SignTransactionRequest{ + OrganizationID: &orgID, + TimestampMs: util.RequestTimestamp(), + Type: (*string)(models.ActivityTypeSignTransactionV2.Pointer()), + Parameters: &models.SignTransactionIntentV2{ + SignWith: &signWith, + Type: models.TransactionTypeEthereum.Pointer(), + UnsignedTransaction: stringPtr(hex.EncodeToString(unsigned)), + }, + }) + + resp, _ := client.V0().Signing.SignTransaction(params, client.Authenticator) + rawSigned, _ := hex.DecodeString(*resp.Payload.Activity.Result.SignTransactionResult.SignedTransaction) + + finalTx := new(types.Transaction) + finalTx.UnmarshalBinary(rawSigned) + return finalTx, nil + } +} +``` + +## Examples + +The SDK includes working examples in the [`examples/`](https://github.com/tkhq/go-sdk/tree/main/examples) directory: + +| Example | Description | +|---------|-------------| +| [`whoami`](https://github.com/tkhq/go-sdk/tree/main/examples/whoami) | Basic API call to verify credentials | +| [`wallets`](https://github.com/tkhq/go-sdk/tree/main/examples/wallets) | Wallet creation and export | +| [`signing`](https://github.com/tkhq/go-sdk/tree/main/examples/signing) | Transaction and payload signing | +| [`delegated_access`](https://github.com/tkhq/go-sdk/tree/main/examples/delegated_access) | Sub-organization and policy setup | +| [`email_otp`](https://github.com/tkhq/go-sdk/tree/main/examples/email_otp) | OTP authentication flow | +| [`go-ethereum`](https://github.com/tkhq/go-sdk/tree/main/examples/go-ethereum) | Ethereum integration patterns | + +## Resources + +- [GitHub Repository](https://github.com/tkhq/go-sdk) +- [pkg.go.dev Documentation](https://pkg.go.dev/github.com/tkhq/go-sdk) +- [API Reference](/api-reference/overview) diff --git a/sdks/introduction.mdx b/sdks/introduction.mdx index 336c4015..adfc6a1a 100644 --- a/sdks/introduction.mdx +++ b/sdks/introduction.mdx @@ -34,9 +34,16 @@ Turnkey also has several [wrappers for popular web3 libraries](/category/web3-li ## Server Side SDKs -| | TypeScript | Go | Ruby | Rust | Python | -|---------------------|------------|-----|------|------|--------| -| **Authentication** | | | | | | -| **Wallet Management** | | | | | | -| **Policy Management** | | | | | | +| | TypeScript | Go | Ruby | Rust | Python | +|------------------------------|------------|-----|------|------|--------| +| **Authentication** | | | | | | +| **Wallet Management** | | | | | | +| **Policy Management** | | | | | | +| **Activity Auto-Polling** | | | | | * | +| **HPKE (Export Decrypt)** | | | | | | +| **Nitro Attestation** | | | | | | +| **Gas Station** | | | | | | +| **ED25519 API Key Signing** | | | | | | + +\* Python's activity polling uses synchronous `time.sleep()` which blocks the thread. See the [Python SDK docs](/sdks/python#activity-polling) for async workarounds. diff --git a/sdks/python.mdx b/sdks/python.mdx index 60162c77..c259a9c4 100644 --- a/sdks/python.mdx +++ b/sdks/python.mdx @@ -1,26 +1,381 @@ --- title: "Python" -description: "Turnkey offers support for interacting with the API using Python. See [https://github.com/tkhq/python-sdk](https://github.com/tkhq/python-sdk) for more details." +description: "The Turnkey Python SDK provides a type-safe HTTP client with Pydantic v2 models, request stamping for authentication, and automatic activity polling." +sidebarTitle: "Python" mode: wide --- -# Turnkey Python SDK +## Overview -This repository contains support for interacting with the Turnkey API using Python. +The [Turnkey Python SDK](https://github.com/tkhq/python-sdk) is a full-featured SDK for interacting with the Turnkey API from Python applications. It consists of three pip-installable packages: -Unlike other languages ([Typescript](https://github.com/tkhq/sdk), [Ruby](https://github.com/tkhq/ruby-sdk)), we do not yet offer a full SDK for Rust. +| Package | PyPI | Description | +|---------|------|-------------| +| `turnkey-http` | [turnkey-http](https://pypi.org/project/turnkey-http/) | HTTP client with all API endpoints | +| `turnkey-api-key-stamper` | [turnkey-api-key-stamper](https://pypi.org/project/turnkey-api-key-stamper/) | P-256 ECDSA request signing | +| `turnkey-sdk-types` | [turnkey-sdk-types](https://pypi.org/project/turnkey-sdk-types/) | Pydantic v2 type definitions | -If you are working on a project in Python and would benefit from a Python SDK please open an issue or get in touch with us at [hello@turnkey.com](mailto:hello@turnkey.com) to discuss prioritizing this. +### Key Features -## Stamper +- **100+ typed API methods** with full Pydantic v2 model definitions +- **Automatic activity polling** with configurable retries +- **Code generation** from OpenAPI specification +- **Stamp-then-send** pattern for flexible request handling -The Stamper utility stamps requests to the Turnkey API and authenticates your calls. To use it, fill out the fields at the top of the stamper script: +## Installation + +```bash +pip install turnkey-http turnkey-api-key-stamper +``` + +Or with Poetry: + +```bash +poetry add turnkey-http turnkey-api-key-stamper +``` + + +**Python 3.8+** is required. The SDK uses Pydantic v2 for type definitions. + + +## Authentication + +The SDK uses the `ApiKeyStamper` class to sign requests with your API credentials: ```python -ENDPOINT = "https://api.turnkey.com/public/v1/whoami" -API_PUBLIC_KEY = "" -API_PRIVATE_KEY = "" -ORG_ID = "" +from turnkey_api_key_stamper import ApiKeyStamper, ApiKeyStamperConfig + +config = ApiKeyStamperConfig( + api_public_key="your-api-public-key", # 02... compressed public key + api_private_key="your-api-private-key" # hex-encoded private key +) + +stamper = ApiKeyStamper(config) +``` + + +The Python SDK only supports **P-256 (secp256r1)** API key signing. secp256k1 and ED25519 are not supported. + + +### Key Validation + +The stamper validates that your public key matches the private key at initialization time, preventing configuration errors. + +## Client Setup + +```python +from turnkey_http import TurnkeyClient +from turnkey_api_key_stamper import ApiKeyStamper, ApiKeyStamperConfig + +# Configure the stamper +config = ApiKeyStamperConfig( + api_public_key="your-api-public-key", + api_private_key="your-api-private-key" +) +stamper = ApiKeyStamper(config) + +# Create the client +client = TurnkeyClient( + base_url="https://api.turnkey.com", + stamper=stamper, + organization_id="your-organization-id", + default_timeout=30, # seconds + polling_interval_ms=1000, # milliseconds between polls + max_polling_retries=10 # increase from default 3 for production +) +``` + +### Configuration Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `base_url` | Required | API base URL (`https://api.turnkey.com`) | +| `stamper` | Required | `ApiKeyStamper` instance for request signing | +| `organization_id` | Required | Your organization ID | +| `default_timeout` | `30` | HTTP request timeout in seconds | +| `polling_interval_ms` | `1000` | Delay between activity polls | +| `max_polling_retries` | `3` | Max poll attempts before timeout | + + +**Increase `max_polling_retries` for production.** The default of 3 retries is often too low for activities that require consensus or have network delays. We recommend at least 10 retries. + + +## Usage Examples + +### Get Whoami + +```python +from turnkey_http import TurnkeyClient + +response = client.get_whoami() + +print(f"User ID: {response.user_id}") +print(f"Organization ID: {response.organization_id}") +print(f"Username: {response.username}") ``` -You can find the script and examples in the [python-sdk repo](https://github.com/tkhq/python-sdk). +### Create Wallet + +```python +from turnkey_sdk_types import ( + CreateWalletBody, + v1WalletAccountParams, + v1Curve, + v1PathFormat, + v1AddressFormat, +) + +response = client.create_wallet(CreateWalletBody( + wallet_name="My Wallet", + accounts=[v1WalletAccountParams( + curve=v1Curve.CURVE_SECP256K1, + path_format=v1PathFormat.PATH_FORMAT_BIP32, + path="m/44'/60'/0'/0/0", + address_format=v1AddressFormat.ADDRESS_FORMAT_ETHEREUM, + )] +)) + +print(f"Wallet ID: {response.wallet_id}") +print(f"Address: {response.addresses[0]}") +``` + +### Sign a Message + +```python +from turnkey_sdk_types import ( + SignRawPayloadBody, + v1PayloadEncoding, + v1HashFunction, +) + +response = client.sign_raw_payload(SignRawPayloadBody( + sign_with=address, + payload="hello world", + encoding=v1PayloadEncoding.PAYLOAD_ENCODING_TEXT_UTF8, + hash_function=v1HashFunction.HASH_FUNCTION_KECCAK256, +)) + +print(f"R: {response.r}") +print(f"S: {response.s}") +print(f"V: {response.v}") +``` + +### Email OTP Authentication + +```python +from turnkey_sdk_types import ( + InitOtpBody, + OtpBody, + OtpAuthBody, + v1OtpType, +) + +# Step 1: Initialize OTP +init_response = client.init_otp(InitOtpBody( + otp_type=v1OtpType.OTP_TYPE_EMAIL, + contact=email, + otp_length=6, + alphanumeric=True, +)) + +otp_id = init_response.otp_id +print(f"OTP sent, ID: {otp_id}") + +# Step 2: User receives code, then verify +verify_response = client.otp(OtpBody( + otp_id=otp_id, + otp_code=user_provided_code, +)) + +# Step 3: Create authenticated session +auth_response = client.otp_auth(OtpAuthBody( + otp_id=otp_id, + target_public_key=target_public_key, + session_length_seconds=3600, +)) + +print(f"Session created: {auth_response.credential_bundle}") +``` + +### Stamp-Then-Send Pattern + +For more control, you can stamp a request without sending it: + +```python +from turnkey_sdk_types import CreateWalletBody + +# Create a signed request without sending +signed_request = client.stamp_create_wallet(CreateWalletBody( + wallet_name="My Wallet", + accounts=[...] +)) + +print(f"URL: {signed_request.url}") +print(f"Body: {signed_request.body}") +print(f"Stamp Header: {signed_request.stamp.stamp_header_name}") +print(f"Stamp Value: {signed_request.stamp.stamp_header_value}") + +# Later: send the signed request +response = client.send_signed_request(signed_request, CreateWalletResponse) +``` + +This is useful for: +- Signing on one machine and sending from another +- Queuing requests for batch processing +- Debugging request construction + +## Activity Polling + +The Python SDK automatically polls for activity completion. Activities return when they reach a terminal status: + +- `ACTIVITY_STATUS_COMPLETED` — Success +- `ACTIVITY_STATUS_FAILED` — Failure +- `ACTIVITY_STATUS_CONSENSUS_NEEDED` — Requires approval +- `ACTIVITY_STATUS_REJECTED` — Rejected by policy + + +**Blocking Behavior**: The Python SDK uses synchronous `time.sleep()` for polling. This **blocks the thread** until the activity completes or times out. + +This is a problem for async Python frameworks like **FastAPI**, **Starlette**, or **Django async views**. Consider running Turnkey calls in a thread pool or using `asyncio.to_thread()`: + +```python +import asyncio +from turnkey_http import TurnkeyClient + +async def create_wallet_async(client: TurnkeyClient, body): + return await asyncio.to_thread(client.create_wallet, body) +``` + + +### Result Flattening + +When an activity completes, result fields are flattened into the response for convenience: + +```python +response = client.create_wallet(CreateWalletBody(...)) + +# These are flattened from activity.result.create_wallet_result +print(response.wallet_id) # Direct access +print(response.addresses) # Direct access + +# You can also access the full activity +print(response.activity.status) +print(response.activity.id) +``` + +## Limitations + +### No Async Support + +The SDK uses the synchronous `requests` library. There is no `asyncio` support. For async applications, wrap calls in `asyncio.to_thread()`. + +### No HPKE (Wallet Export Decryption) + +The Python SDK can call the wallet export API, but **cannot decrypt the result locally**. The export bundle is encrypted with HPKE, and Python lacks the decryption implementation. + +For wallet export workflows, use the TypeScript, Go, or Rust SDKs which have full HPKE support. + +### P-256 Only + +Only P-256 API key signing is supported. secp256k1 and ED25519 API keys cannot be used with the Python SDK. + +### Polling Defaults + +The default `max_polling_retries=3` is low for production. Activities requiring consensus or experiencing network delays may timeout. Increase to at least 10. + +## Error Handling + +```python +from turnkey_sdk_types import TurnkeyNetworkError, TurnkeyError + +try: + response = client.create_wallet(CreateWalletBody(...)) +except TurnkeyNetworkError as e: + print(f"Network error: {e}") + print(f"Status code: {e.status_code}") + print(f"Error code: {e.code}") +except TurnkeyError as e: + print(f"Turnkey error: {e}") + print(f"Cause: {e.cause}") +``` + +### Error Types + +| Exception | Description | +|-----------|-------------| +| `TurnkeyError` | Base exception for all Turnkey errors | +| `TurnkeyNetworkError` | HTTP errors with status code | + +## Integration with Web Frameworks + +### Flask + +```python +from flask import Flask, jsonify +from turnkey_http import TurnkeyClient + +app = Flask(__name__) +client = TurnkeyClient(...) + +@app.route("/whoami") +def whoami(): + response = client.get_whoami() + return jsonify({ + "user_id": response.user_id, + "organization_id": response.organization_id, + }) +``` + +### FastAPI (with thread pool) + +```python +from fastapi import FastAPI +import asyncio +from turnkey_http import TurnkeyClient + +app = FastAPI() +client = TurnkeyClient(...) + +@app.get("/whoami") +async def whoami(): + response = await asyncio.to_thread(client.get_whoami) + return { + "user_id": response.user_id, + "organization_id": response.organization_id, + } +``` + +### Django + +```python +# settings.py or utils.py +from turnkey_http import TurnkeyClient +from turnkey_api_key_stamper import ApiKeyStamper, ApiKeyStamperConfig + +TURNKEY_CLIENT = TurnkeyClient( + base_url="https://api.turnkey.com", + stamper=ApiKeyStamper(ApiKeyStamperConfig( + api_public_key=os.environ["TURNKEY_API_PUBLIC_KEY"], + api_private_key=os.environ["TURNKEY_API_PRIVATE_KEY"], + )), + organization_id=os.environ["TURNKEY_ORGANIZATION_ID"], +) + +# views.py +from django.http import JsonResponse +from .settings import TURNKEY_CLIENT + +def whoami(request): + response = TURNKEY_CLIENT.get_whoami() + return JsonResponse({ + "user_id": response.user_id, + "organization_id": response.organization_id, + }) +``` + +## Resources + +- [GitHub Repository](https://github.com/tkhq/python-sdk) +- [PyPI - turnkey-http](https://pypi.org/project/turnkey-http/) +- [PyPI - turnkey-api-key-stamper](https://pypi.org/project/turnkey-api-key-stamper/) +- [API Reference](/api-reference/overview) diff --git a/sdks/rust.mdx b/sdks/rust.mdx index e5a2fa62..aa21195a 100644 --- a/sdks/rust.mdx +++ b/sdks/rust.mdx @@ -1,7 +1,427 @@ --- title: "Rust" -description: "Turnkey offers native tooling for interacting with the API using Rust. See [https://github.com/tkhq/rust-sdk](https://github.com/tkhq/rust-sdk) for more details." +description: "The Turnkey Rust SDK provides an async HTTP client with automatic activity polling, HPKE encryption for secure enclave communication, and AWS Nitro attestation verification." +sidebarTitle: "Rust" mode: wide --- +## Overview +The [Turnkey Rust SDK](https://github.com/tkhq/rust-sdk) provides a complete, production-ready implementation for interacting with the Turnkey API. The SDK is organized as a Cargo workspace with four crates: + +| Crate | crates.io | Description | +|-------|-----------|-------------| +| `turnkey_client` | [turnkey_client](https://crates.io/crates/turnkey_client) | Async HTTP client with all API endpoints | +| `turnkey_api_key_stamper` | [turnkey_api_key_stamper](https://crates.io/crates/turnkey_api_key_stamper) | P-256 and secp256k1 API key signing | +| `turnkey_enclave_encrypt` | [turnkey_enclave_encrypt](https://crates.io/crates/turnkey_enclave_encrypt) | HPKE encryption for wallet import/export | +| `turnkey_proofs` | [turnkey_proofs](https://crates.io/crates/turnkey_proofs) | AWS Nitro attestation verification | + +### Key Features + +- **Full API coverage** for wallets, policies, signing, OTP, OAuth, and gas station +- **Automatic activity polling** with configurable exponential backoff +- **Strongly typed** with code generation from protobuf definitions +- **`#![forbid(unsafe_code)]`** for memory safety guarantees + +## Installation + +Add the crates to your `Cargo.toml`: + +```toml +[dependencies] +turnkey_client = "0.6" +turnkey_api_key_stamper = "0.6" + +# Optional: for wallet export/import +turnkey_enclave_encrypt = "0.6" + +# Optional: for attestation verification +turnkey_proofs = "0.6" +``` + +Or install via Cargo: + +```bash +cargo add turnkey_client turnkey_api_key_stamper +``` + +## Authentication + +The Rust SDK uses the `Stamp` trait for request signing. Two implementations are provided: + +### P-256 (Default) + +```rust +use turnkey_api_key_stamper::TurnkeyP256ApiKey; + +// Generate a new key +let api_key = TurnkeyP256ApiKey::generate(); + +// Load from hex strings +let api_key = TurnkeyP256ApiKey::from_strings( + "your-private-key-hex", + Some("your-public-key-hex".to_string()), +)?; + +// Load from PEM files (tkcli format) +let api_key = TurnkeyP256ApiKey::from_files( + "path/to/private.pem", + "path/to/public.pem", +)?; +``` + +### secp256k1 (Alternative) + +```rust +use turnkey_api_key_stamper::TurnkeySecp256k1ApiKey; + +let api_key = TurnkeySecp256k1ApiKey::generate(); +``` + +## Client Setup + +The SDK uses a builder pattern for client configuration: + +```rust +use turnkey_client::TurnkeyClient; +use turnkey_api_key_stamper::TurnkeyP256ApiKey; +use std::time::Duration; + +let api_key = TurnkeyP256ApiKey::from_strings( + std::env::var("TURNKEY_API_PRIVATE_KEY")?, + Some(std::env::var("TURNKEY_API_PUBLIC_KEY")?), +)?; + +let client = TurnkeyClient::builder() + .api_key(api_key) + .base_url("https://api.turnkey.com") + .organization_id("your-organization-id") + .timeout(Duration::from_secs(30)) + .retry_config(RetryConfig::default()) + .build()?; +``` + +### Retry Configuration + +The SDK includes built-in exponential backoff for activity polling: + +```rust +use turnkey_client::RetryConfig; + +let config = RetryConfig { + initial_delay: Duration::from_millis(500), + multiplier: 2.0, + max_delay: Duration::from_secs(5), + max_retries: 5, +}; + +let client = TurnkeyClient::builder() + .api_key(api_key) + .retry_config(config) + .build()?; +``` + +## Usage Examples + +### Get Whoami + +```rust +use turnkey_client::TurnkeyClient; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = TurnkeyClient::builder() + .api_key(api_key) + .organization_id(&org_id) + .build()?; + + let response = client.get_whoami(org_id.clone()).await?; + + println!("User ID: {:?}", response.user_id); + println!("Organization ID: {:?}", response.organization_id); + + Ok(()) +} +``` + +### Create Wallet + +```rust +use turnkey_client::generated::*; + +let result = client.create_wallet( + org_id.clone(), + client.current_timestamp(), + CreateWalletIntent { + wallet_name: "My Wallet".to_string(), + accounts: vec![WalletAccountParams { + curve: Curve::Secp256k1, + path_format: PathFormat::Bip32, + path: "m/44'/60'/0'/0/0".to_string(), + address_format: AddressFormat::Ethereum, + }], + mnemonic_length: None, + }, +).await?; + +println!("Wallet ID: {:?}", result.result.wallet_id); +println!("Address: {:?}", result.result.addresses.first()); +``` + +### Sign a Message + +```rust +use turnkey_client::generated::*; + +let signature = client.sign_raw_payload( + org_id.clone(), + client.current_timestamp(), + SignRawPayloadIntentV2 { + sign_with: address.clone(), + payload: "hello world".to_string(), + encoding: PayloadEncoding::TextUtf8, + hash_function: HashFunction::Keccak256, + }, +).await?; + +println!("R: {:?}", signature.result.r); +println!("S: {:?}", signature.result.s); +println!("V: {:?}", signature.result.v); +``` + +### Email OTP Authentication + +```rust +use turnkey_client::generated::*; + +// Step 1: Initialize OTP +let init_result = client.init_otp( + org_id.clone(), + client.current_timestamp(), + InitOtpIntent { + otp_type: OtpType::OtpTypeEmail, + contact: email.clone(), + email_customization: None, + sms_customization: None, + user_identifier: None, + send_from_email_address: None, + redirect_url: None, + alphanumeric: Some(true), + otp_length: Some(6), + }, +).await?; + +let otp_id = init_result.result.otp_id; + +// Step 2: User receives code, then verify +let verify_result = client.otp( + org_id.clone(), + client.current_timestamp(), + OtpIntent { + otp_id: otp_id.clone(), + otp_code: user_provided_code, + }, +).await?; + +// Step 3: Create authenticated session +let auth_result = client.otp_auth( + org_id.clone(), + client.current_timestamp(), + OtpAuthIntent { + otp_id: otp_id.clone(), + target_public_key: target_public_key.clone(), + api_key_name: None, + expiration_seconds: Some("3600".to_string()), + session_length_seconds: None, + invalidate_existing: None, + }, +).await?; +``` + +## Activity Auto-Polling + + +The Rust SDK **automatically polls** for activity completion with configurable exponential backoff. You don't need to implement polling yourself. + + +Activities return an `ActivityResult` that includes metadata: + +```rust +pub struct ActivityResult { + pub result: T, + pub activity_id: String, + pub status: ActivityStatus, + pub app_proofs: Vec, +} +``` + +The client handles all polling internally: +- Starts with a 500ms delay +- Uses exponential backoff (2x multiplier) +- Retries up to 5 times by default +- Returns error on timeout or failure + +## HPKE Encryption (Wallet Export/Import) + +The `turnkey_enclave_encrypt` crate provides HPKE encryption for secure wallet operations: + +### Export Wallet + +```rust +use turnkey_enclave_encrypt::{ExportClient, QuorumPublicKey}; + +// 1. Create export client with production quorum key +let mut export_client = ExportClient::new(&QuorumPublicKey::production_signer()); + +// 2. Request export (target_public_key goes to Turnkey) +let export_result = client.export_wallet( + org_id.clone(), + client.current_timestamp(), + ExportWalletIntent { + wallet_id: wallet_id.clone(), + target_public_key: export_client.target_public_key()?, + language: None, + }, +).await?; + +// 3. Decrypt the bundle locally +let mnemonic = export_client.decrypt_wallet_mnemonic_phrase( + export_result.result.export_bundle, + org_id.clone(), +)?; + +println!("Mnemonic: {}", mnemonic); +``` + +### Import Wallet + +```rust +use turnkey_enclave_encrypt::{ImportClient, QuorumPublicKey}; + +let mut import_client = ImportClient::new(&QuorumPublicKey::production_signer()); + +// Encrypt mnemonic for import +let encrypted_bundle = import_client.encrypt_wallet_mnemonic( + mnemonic, + org_id.clone(), + target_enclave_public_key, +)?; + +// Send to Turnkey +let import_result = client.import_wallet( + org_id.clone(), + client.current_timestamp(), + ImportWalletIntent { + wallet_name: "Imported Wallet".to_string(), + encrypted_bundle, + accounts: vec![...], + user_id: None, + }, +).await?; +``` + +## Nitro Attestation Verification + +The `turnkey_proofs` crate verifies AWS Nitro attestation documents: + +```rust +use turnkey_proofs::{verify, verify_app_proof_signature, get_boot_proof_for_app_proof}; + +// Enable app proofs on the client +let client = TurnkeyClient::builder() + .api_key(api_key) + .build()? + .with_app_proofs(); + +// Create an activity and get proofs +let result = client.create_wallet(...).await?; + +// Verify each app proof +for app_proof in result.app_proofs { + // Verify the app proof signature + verify_app_proof_signature(&app_proof)?; + + // Fetch and verify the full proof chain + let boot_proof_response = get_boot_proof_for_app_proof( + &client, + org_id.clone(), + &app_proof + ).await?; + + let boot_proof = boot_proof_response.boot_proof.unwrap(); + verify(&app_proof, &boot_proof)?; + + println!("Proof verified successfully"); +} +``` + +### Parse Attestation Documents + +```rust +use turnkey_proofs::parse_and_verify_aws_nitro_attestation; + +let attestation_doc = parse_and_verify_aws_nitro_attestation( + &encoded_attestation, + Some(validation_time), +)?; + +println!("PCR0: {:?}", attestation_doc.pcrs.get(&0)); +println!("Public Key: {:?}", attestation_doc.public_key); +``` + +## Gas Station + +The Rust SDK supports Turnkey's gas station for sponsored transactions: + +```rust +use turnkey_client::generated::*; + +// Create a gas station configuration +let result = client.create_gas_station( + org_id.clone(), + client.current_timestamp(), + CreateGasStationIntent { + name: "My Gas Station".to_string(), + funding_wallet_id: wallet_id.clone(), + chain_ids: vec!["1".to_string(), "137".to_string()], + }, +).await?; +``` + +## TVC CLI (Experimental) + +The Rust SDK repository includes an experimental Turnkey Verified Cloud (TVC) CLI tool for verifiable deployments. See the [`tvc/`](https://github.com/tkhq/rust-sdk/tree/main/tvc) directory for more information. + +## Code Quality + +The Rust SDK maintains high code quality standards: + +```rust +#![forbid(unsafe_code)] +#![deny(clippy::all, clippy::unwrap_used)] +#![warn(missing_docs, clippy::pedantic)] +``` + +- **No unsafe code** — memory safety guaranteed +- **Comprehensive linting** — clippy with pedantic warnings +- **Strong typing** — all API types generated from protobuf +- **Test coverage** — unit tests and integration tests with wiremock + +## Examples + +The SDK includes working examples in the [`examples/`](https://github.com/tkhq/rust-sdk/tree/main/examples) directory: + +| Example | Description | +|---------|-------------| +| [`whoami`](https://github.com/tkhq/rust-sdk/tree/main/examples/whoami) | Basic API call to verify credentials | +| [`wallet`](https://github.com/tkhq/rust-sdk/tree/main/examples/wallet) | Wallet creation, signing, and export | +| [`proofs`](https://github.com/tkhq/rust-sdk/tree/main/examples/proofs) | Attestation verification | +| [`sub_organization`](https://github.com/tkhq/rust-sdk/tree/main/examples/sub_organization) | Sub-org creation | + +## Resources + +- [GitHub Repository](https://github.com/tkhq/rust-sdk) +- [crates.io - turnkey_client](https://crates.io/crates/turnkey_client) +- [crates.io - turnkey_api_key_stamper](https://crates.io/crates/turnkey_api_key_stamper) +- [docs.rs Documentation](https://docs.rs/turnkey_client) +- [API Reference](/api-reference/overview)