Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ Refer to each example's README for the exact commands.

- [otp](./otp/): Example of using one-time password (OTP) flows for authentication.

- [private_keys](./private_keys/): Shows how to manage standalone private keys.
- [import_private_key](./private_keys/import_private_key/): Example of importing a raw private key.

- [signing](./signing/): Demonstrates how to sign messages and transactions using the SDK.
- [sign_raw_payload](./signing/sign_raw_payload/): Example of signing a raw payload.
- [sign_transaction](./signing/sign_transaction/): Example of signing a blockchain transaction.
Expand Down
41 changes: 41 additions & 0 deletions examples/private_keys/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Examples: Private Keys

A Turnkey private key is a single, standalone key pair, as opposed to a [wallet](../wallets/), which is a hierarchical deterministic (HD) tree of accounts derived from one seed. Use a private key when you want to bring an existing raw key into Turnkey rather than deriving addresses from a mnemonic.

**Private Key** → one key pair, defined by a curve. Supported curves:
- `CURVE_SECP256K1` (Ethereum and other EVM chains)
- `CURVE_ED25519` (Solana)

> [!CAUTION]
>
> **SECURITY: Your private keys grant full, irrevocable access to your funds.**
> Treat them with the same care as a root password, store them offline and never log them, and never share them.
> If you have any reason to believe they were exposed, **rotate them immediately**.

---

## `import_private_key`

Imports an existing raw private key into Turnkey. The key is encrypted client-side before being sent — Turnkey's enclave decrypts it and stores it. The plaintext private key is never transmitted.

Supports either a hex-encoded Secp256k1 (Ethereum) key or a base58-encoded Ed25519 (Solana) key. Set exactly one.

### 1/ Setup

Follow the [Quickstart](https://docs.turnkey.com/getting-started/quickstart) to get your API key and organization ID.

Copy `.env.example` to `.env` and fill in the values:

```bash
cp examples/private_keys/import_private_key/.env.example examples/private_keys/import_private_key/.env
```

Set exactly one of `TURNKEY_ETHEREUM_PRIVATE_KEY` (hex-encoded) or `TURNKEY_SOLANA_PRIVATE_KEY` (base58-encoded).

### 2/ Running

```bash
set -a && source examples/private_keys/import_private_key/.env && set +a && go run ./examples/private_keys/import_private_key
```

Prints the new private key ID and derived addresses on success.
7 changes: 7 additions & 0 deletions examples/private_keys/import_private_key/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
TURNKEY_API_PRIVATE_KEY='YOUR_API_PRIVATE_KEY'
TURNKEY_ORGANIZATION_ID='YOUR_ORGANIZATION_ID'
# Set exactly one of the following.
# Hex-encoded Secp256k1 (Ethereum) private key:
TURNKEY_ETHEREUM_PRIVATE_KEY='YOUR_HEX_ENCODED_PRIVATE_KEY'
# Base58-encoded Ed25519 (Solana) private key:
# TURNKEY_SOLANA_PRIVATE_KEY='YOUR_BASE58_ENCODED_PRIVATE_KEY'
138 changes: 138 additions & 0 deletions examples/private_keys/import_private_key/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Package main demonstrates importing a raw private key (Secp256k1 or Ed25519).
//
// Set exactly one of TURNKEY_ETHEREUM_PRIVATE_KEY (hex-encoded) or
// TURNKEY_SOLANA_PRIVATE_KEY (base58-encoded). The key is encrypted client-side
// before being sent — Turnkey's enclave decrypts it and stores it. The plaintext
// private key is never transmitted.
package main

import (
"context"
"errors"
"fmt"
"log"
"os"
"time"

"github.com/tkhq/go-sdk/crypto"
turnkey "github.com/tkhq/go-sdk/v2"
)

func main() {
apiPrivateKey := mustEnv("TURNKEY_API_PRIVATE_KEY")
organizationID := mustEnv("TURNKEY_ORGANIZATION_ID")

sel, err := selectKey()
if err != nil {
log.Fatal(err)
}

stamper, err := turnkey.NewAPIKeyStamper(apiPrivateKey)
if err != nil {
log.Fatal("failed to create stamper:", err)
}

client, err := turnkey.NewClient(stamper, organizationID)
if err != nil {
log.Fatal("failed to create Turnkey client:", err)
}

ctx := context.Background()

whoami, err := client.GetWhoami(ctx, turnkey.GetWhoamiRequest{})
if err != nil {
log.Fatal("failed to get whoami:", err)
}

initResult, err := client.InitImportPrivateKey(ctx, turnkey.InitImportPrivateKeyRequest{
UserID: whoami.UserID,
})
if err != nil {
fatalRequestError(err, "init import private key")
}

encryptedBundle, err := crypto.EncryptPrivateKeyToBundle(sel.privateKey, sel.keyFormat, initResult.ImportBundle, organizationID, whoami.UserID)
if err != nil {
log.Fatal("failed to encrypt private key:", err)
}

privateKeyName := fmt.Sprintf("Imported Private Key %d", time.Now().UnixMilli())

importResult, err := client.ImportPrivateKey(ctx, turnkey.ImportPrivateKeyRequest{
UserID: whoami.UserID,
PrivateKeyName: privateKeyName,
EncryptedBundle: encryptedBundle,
Curve: sel.curve,
AddressFormats: []turnkey.AddressFormat{sel.addressFormat},
})
if err != nil {
fatalRequestError(err, "import private key")
}

fmt.Printf("Private Key ID: %s\n", importResult.PrivateKeyID)
printAddresses(importResult.Addresses)
}

// keySelection is the private key to import plus its matching curve, address,
// and key format.
type keySelection struct {
privateKey string
keyFormat string
curve turnkey.Curve
addressFormat turnkey.AddressFormat
}

// selectKey reads the key from the environment and returns it with its matching
// curve, address, and key format. Exactly one of TURNKEY_ETHEREUM_PRIVATE_KEY
// (hex) or TURNKEY_SOLANA_PRIVATE_KEY (base58) must be set.
func selectKey() (keySelection, error) {
ethereumKey := os.Getenv("TURNKEY_ETHEREUM_PRIVATE_KEY")
solanaKey := os.Getenv("TURNKEY_SOLANA_PRIVATE_KEY")

switch {
case ethereumKey != "" && solanaKey != "":
return keySelection{}, errors.New("set only one of TURNKEY_ETHEREUM_PRIVATE_KEY or TURNKEY_SOLANA_PRIVATE_KEY")
case ethereumKey != "":
return keySelection{
privateKey: ethereumKey,
keyFormat: crypto.KeyFormatHexadecimal,
curve: turnkey.CurveSecp256K1,
addressFormat: turnkey.AddressFormatEthereum,
}, nil
case solanaKey != "":
return keySelection{
privateKey: solanaKey,
keyFormat: crypto.KeyFormatSolana,
curve: turnkey.CurveEd25519,
addressFormat: turnkey.AddressFormatSolana,
}, nil
default:
return keySelection{}, errors.New("set one of TURNKEY_ETHEREUM_PRIVATE_KEY or TURNKEY_SOLANA_PRIVATE_KEY")
}
}

// printAddresses prints each imported address, skipping any without a value.
func printAddresses(addresses []turnkey.Immutableactivityv1Address) {
for _, addr := range addresses {
if addr.Address == nil {
continue
}
fmt.Printf("Address: %s\n", *addr.Address)
}
}

func mustEnv(key string) string {
v := os.Getenv(key)
if v == "" {
log.Fatalf("%s is required", key)
}
return v
}

func fatalRequestError(err error, action string) {
var reqErr *turnkey.RequestError
if errors.As(err, &reqErr) {
log.Fatalf("failed to %s (status=%d): %s", action, reqErr.StatusCode, reqErr.Body)
}
log.Fatalf("failed to %s: %v", action, err)
}
Loading