From dca71a50462fc1e83029e87b78b429a6f58e97fa Mon Sep 17 00:00:00 2001 From: LeoFranklin015 Date: Thu, 26 Feb 2026 18:07:54 +0530 Subject: [PATCH] feat: added turnkey example --- examples/kms/turnkey/.env.example | 4 + examples/kms/turnkey/README.md | 77 ++++++++++++++++ examples/kms/turnkey/index.ts | 131 ++++++++++++++++++++++++++++ examples/kms/turnkey/package.json | 20 +++++ examples/kms/turnkey/src/config.ts | 19 ++++ examples/kms/turnkey/src/jaw.ts | 14 +++ examples/kms/turnkey/src/turnkey.ts | 61 +++++++++++++ examples/kms/turnkey/tsconfig.json | 12 +++ 8 files changed, 338 insertions(+) create mode 100644 examples/kms/turnkey/.env.example create mode 100644 examples/kms/turnkey/README.md create mode 100644 examples/kms/turnkey/index.ts create mode 100644 examples/kms/turnkey/package.json create mode 100644 examples/kms/turnkey/src/config.ts create mode 100644 examples/kms/turnkey/src/jaw.ts create mode 100644 examples/kms/turnkey/src/turnkey.ts create mode 100644 examples/kms/turnkey/tsconfig.json diff --git a/examples/kms/turnkey/.env.example b/examples/kms/turnkey/.env.example new file mode 100644 index 0000000..847ac89 --- /dev/null +++ b/examples/kms/turnkey/.env.example @@ -0,0 +1,4 @@ +TURNKEY_ORGANIZATION_ID= +TURNKEY_API_PUBLIC_KEY= +TURNKEY_API_PRIVATE_KEY= +JAW_API_KEY= diff --git a/examples/kms/turnkey/README.md b/examples/kms/turnkey/README.md new file mode 100644 index 0000000..16536b7 --- /dev/null +++ b/examples/kms/turnkey/README.md @@ -0,0 +1,77 @@ +# JAW + Turnkey Server Wallets + +A server-side example that uses [Turnkey](https://www.turnkey.com/) as the key management layer for JAW smart accounts — no browser or passkey required. + +## What This Demonstrates + +| Feature | Description | +|---------|-------------| +| Turnkey wallet creation | Create and manage HD wallets via Turnkey's server-side API | +| JAW smart account from Turnkey wallet | Wrap a Turnkey wallet into a JAW smart account | +| Message signing | Sign arbitrary messages off-chain | +| Send transactions | Send ETH transfers through the JAW smart account | + +## How It Works + +1. **Create or load** a Turnkey wallet (`@turnkey/sdk-server`) +2. **Convert** the Turnkey wallet into a viem `LocalAccount` (`@turnkey/viem`) +3. **Wrap** the local account into a JAW smart account (`Account.fromLocalAccount`) +4. **Interact** — sign messages, send transactions via an interactive CLI menu + +## Setup + +1. Install dependencies: + ```bash + npm install + ``` + +2. Copy the environment file and fill in your values: + ```bash + cp .env.example .env.local + ``` + + | Variable | Description | + |---|---| + | `TURNKEY_ORGANIZATION_ID` | Your organization ID from [dashboard.turnkey.com](https://dashboard.turnkey.com) | + | `TURNKEY_API_PUBLIC_KEY` | Your API public key | + | `TURNKEY_API_PRIVATE_KEY` | Your API private key | + | `JAW_API_KEY` | Your API key from [dashboard.jaw.id](https://dashboard.jaw.id) | + + > **Note:** Create an API key pair in your Turnkey dashboard under **API Keys**. + +3. Run the script: + ```bash + npm run dev + ``` + +## Expected Output + +``` +--- Turnkey + JAW Server Wallet --- + + [1] Create new wallet + [2] Load existing wallet + + Choice: 1 + + Creating new Turnkey wallet... + Wallet ID: + Address: 0xabc... + Creating viem account... + Creating JAW smart account... + Smart Account: 0xdef... + Chain ID: 84532 + + [1] Sign a message + [2] Send transaction + [3] Account info + [4] Exit +``` + +## Key Concepts + +**Turnkey** — A key management infrastructure provider. Turnkey manages cryptographic keys on secure hardware, and your server interacts with wallets through their API. You never handle raw key material. + +**`createAccount`** — From `@turnkey/viem`, bridges a Turnkey wallet into a viem-compatible `LocalAccount`, which JAW can then wrap into a smart account. + +**`Account.fromLocalAccount`** — The JAW server-side entry point. Takes any viem `LocalAccount` (whether from a private key, Turnkey, or another KMS) and creates a smart account. diff --git a/examples/kms/turnkey/index.ts b/examples/kms/turnkey/index.ts new file mode 100644 index 0000000..c6a96b9 --- /dev/null +++ b/examples/kms/turnkey/index.ts @@ -0,0 +1,131 @@ +import * as readline from 'readline'; +import { parseEther } from 'viem'; +import type { Account } from '@jaw.id/core'; + +import { CHAIN_ID, getEnv } from './src/config.js'; +import { initTurnkey, createWallet, getWallets, getWalletAccounts, getViemAccount } from './src/turnkey.js'; +import { createJawAccount } from './src/jaw.js'; + +const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); +const ask = (q: string) => new Promise((res) => rl.question(q, res)); + +async function setup() { + const env = getEnv(); + const turnkey = initTurnkey(env.turnkeyOrgId, env.turnkeyApiPublicKey, env.turnkeyApiPrivateKey); + + console.log('\n--- Turnkey + JAW Server Wallet ---\n'); + + const choice = await ask(' [1] Create new wallet\n [2] Load existing wallet\n\n Choice: '); + + let walletAddress: string; + + if (choice.trim() === '2') { + const wallets = await getWallets(turnkey); + + if (wallets.length === 0) { + console.log('\n No wallets found. Creating a new one...'); + const wallet = await createWallet(turnkey); + walletAddress = wallet.address; + console.log(` Wallet ID: ${wallet.walletId}`); + } else { + console.log('\n Available wallets:'); + for (let i = 0; i < wallets.length; i++) { + console.log(` [${i + 1}] ${wallets[i].walletName} (${wallets[i].walletId})`); + } + const walletChoice = await ask('\n Select wallet number: '); + const idx = parseInt(walletChoice.trim(), 10) - 1; + const selectedWallet = wallets[idx] || wallets[0]; + + const accounts = await getWalletAccounts(turnkey, selectedWallet.walletId); + walletAddress = accounts[0].address; + console.log(`\n Loaded wallet: ${selectedWallet.walletName}`); + } + } else { + console.log('\n Creating new Turnkey wallet...'); + const wallet = await createWallet(turnkey); + walletAddress = wallet.address; + console.log(` Wallet ID: ${wallet.walletId}`); + } + + console.log(` Address: ${walletAddress}`); + + console.log('\n Creating viem account...'); + const localAccount = await getViemAccount(turnkey, env.turnkeyOrgId, walletAddress); + + console.log(' Creating JAW smart account...'); + const account = await createJawAccount(CHAIN_ID, env.jawApiKey, localAccount); + console.log(` Smart Account: ${account.address}`); + console.log(` Chain ID: ${account.chainId}\n`); + + return account; +} + +async function signMessage(account: Account) { + const msg = await ask(' Message to sign: '); + const signature = await account.signMessage(msg); + console.log(` Signature: ${signature}\n`); +} + +async function sendTransaction(account: Account) { + const to = await ask(' Recipient address (blank = self): '); + const amountStr = await ask(' Amount in ETH: '); + const recipient = to.trim() || account.address; + const amount = parseEther(amountStr.trim()); + + console.log(` Sending ${amountStr.trim()} ETH to ${recipient}...`); + try { + const txHash = await account.sendTransaction([ + { to: recipient as `0x${string}`, value: amount, data: '0x' }, + ]); + console.log(` Tx hash: ${txHash}`); + console.log(` Explorer: https://sepolia.basescan.org/tx/${txHash}\n`); + } catch (err) { + console.log(` Failed: ${err instanceof Error ? err.message : 'Unknown error'}`); + console.log(` Fund the account: ${account.address}`); + console.log(` Faucet: https://www.alchemy.com/faucets/base-sepolia\n`); + } +} + +function showInfo(account: Account) { + console.log(` Address: ${account.address}`); + console.log(` Chain ID: ${account.chainId}\n`); +} + +async function menu(account: Account) { + while (true) { + console.log(' [1] Sign a message'); + console.log(' [2] Send transaction'); + console.log(' [3] Account info'); + console.log(' [4] Exit\n'); + + const choice = await ask(' > '); + + switch (choice.trim()) { + case '1': + await signMessage(account); + break; + case '2': + await sendTransaction(account); + break; + case '3': + showInfo(account); + break; + case '4': + console.log(' Bye.'); + rl.close(); + return; + default: + console.log(' Invalid choice.\n'); + } + } +} + +async function main() { + const account = await setup(); + await menu(account); +} + +main().catch((err) => { + console.error(err); + rl.close(); +}); diff --git a/examples/kms/turnkey/package.json b/examples/kms/turnkey/package.json new file mode 100644 index 0000000..b2f7a67 --- /dev/null +++ b/examples/kms/turnkey/package.json @@ -0,0 +1,20 @@ +{ + "name": "@jaw-examples/kms-turnkey", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx --env-file=.env.local index.ts", + "start": "tsx --env-file=.env.local index.ts" + }, + "dependencies": { + "@jaw.id/core": "latest", + "@turnkey/sdk-server": "^5.1.0", + "@turnkey/viem": "^0.14.24", + "viem": "^2.23.2" + }, + "devDependencies": { + "tsx": "^4.19.4", + "typescript": "^5.7.3" + } +} diff --git a/examples/kms/turnkey/src/config.ts b/examples/kms/turnkey/src/config.ts new file mode 100644 index 0000000..bdd0750 --- /dev/null +++ b/examples/kms/turnkey/src/config.ts @@ -0,0 +1,19 @@ +export const CHAIN_ID = 84532; // Base Sepolia + +export function getEnv() { + const turnkeyOrgId = process.env.TURNKEY_ORGANIZATION_ID; + const turnkeyApiPublicKey = process.env.TURNKEY_API_PUBLIC_KEY; + const turnkeyApiPrivateKey = process.env.TURNKEY_API_PRIVATE_KEY; + const jawApiKey = process.env.JAW_API_KEY; + + if (!turnkeyOrgId || !turnkeyApiPublicKey || !turnkeyApiPrivateKey || !jawApiKey) { + console.error('Missing required environment variables:'); + if (!turnkeyOrgId) console.error(' TURNKEY_ORGANIZATION_ID'); + if (!turnkeyApiPublicKey) console.error(' TURNKEY_API_PUBLIC_KEY'); + if (!turnkeyApiPrivateKey) console.error(' TURNKEY_API_PRIVATE_KEY'); + if (!jawApiKey) console.error(' JAW_API_KEY'); + process.exit(1); + } + + return { turnkeyOrgId, turnkeyApiPublicKey, turnkeyApiPrivateKey, jawApiKey }; +} diff --git a/examples/kms/turnkey/src/jaw.ts b/examples/kms/turnkey/src/jaw.ts new file mode 100644 index 0000000..5aa24eb --- /dev/null +++ b/examples/kms/turnkey/src/jaw.ts @@ -0,0 +1,14 @@ +import { Account } from '@jaw.id/core'; +import type { LocalAccount } from 'viem'; + +export async function createJawAccount( + chainId: number, + apiKey: string, + localAccount: LocalAccount, +) { + const account = await Account.fromLocalAccount( + { chainId, apiKey }, + localAccount, + ); + return account; +} diff --git a/examples/kms/turnkey/src/turnkey.ts b/examples/kms/turnkey/src/turnkey.ts new file mode 100644 index 0000000..3439d4b --- /dev/null +++ b/examples/kms/turnkey/src/turnkey.ts @@ -0,0 +1,61 @@ +import { Turnkey } from '@turnkey/sdk-server'; +import { createAccount } from '@turnkey/viem'; + +export function initTurnkey( + organizationId: string, + apiPublicKey: string, + apiPrivateKey: string, +) { + return new Turnkey({ + defaultOrganizationId: organizationId, + apiBaseUrl: 'https://api.turnkey.com', + apiPublicKey, + apiPrivateKey, + }); +} + +export async function createWallet(turnkey: Turnkey) { + const apiClient = turnkey.apiClient(); + const response = await apiClient.createWallet({ + walletName: `jaw-wallet-${Date.now()}`, + accounts: [ + { + curve: 'CURVE_SECP256K1', + pathFormat: 'PATH_FORMAT_BIP32', + path: "m/44'/60'/0'/0/0", + addressFormat: 'ADDRESS_FORMAT_ETHEREUM', + }, + ], + }); + + const walletId = response.walletId; + const address = response.addresses[0]; + + return { walletId, address }; +} + +export async function getWallets(turnkey: Turnkey) { + const apiClient = turnkey.apiClient(); + const response = await apiClient.getWallets(); + return response.wallets; +} + +export async function getWalletAccounts(turnkey: Turnkey, walletId: string) { + const apiClient = turnkey.apiClient(); + const response = await apiClient.getWalletAccounts({ walletId }); + return response.accounts; +} + +export async function getViemAccount( + turnkey: Turnkey, + organizationId: string, + address: string, +) { + const localAccount = await createAccount({ + client: turnkey.apiClient(), + organizationId, + signWith: address, + ethereumAddress: address as `0x${string}`, + }); + return localAccount; +} diff --git a/examples/kms/turnkey/tsconfig.json b/examples/kms/turnkey/tsconfig.json new file mode 100644 index 0000000..5f968d2 --- /dev/null +++ b/examples/kms/turnkey/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["*.ts"] +}