diff --git a/examples/README.md b/examples/README.md index 2441cd6..5628b91 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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. diff --git a/examples/private_keys/README.md b/examples/private_keys/README.md new file mode 100644 index 0000000..637d1ba --- /dev/null +++ b/examples/private_keys/README.md @@ -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. diff --git a/examples/private_keys/import_private_key/.env.example b/examples/private_keys/import_private_key/.env.example new file mode 100644 index 0000000..266b70d --- /dev/null +++ b/examples/private_keys/import_private_key/.env.example @@ -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' diff --git a/examples/private_keys/import_private_key/main.go b/examples/private_keys/import_private_key/main.go new file mode 100644 index 0000000..f9ba0b4 --- /dev/null +++ b/examples/private_keys/import_private_key/main.go @@ -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) +}