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
4 changes: 4 additions & 0 deletions examples/kms/turnkey/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
TURNKEY_ORGANIZATION_ID=
TURNKEY_API_PUBLIC_KEY=
TURNKEY_API_PRIVATE_KEY=
JAW_API_KEY=
77 changes: 77 additions & 0 deletions examples/kms/turnkey/README.md
Original file line number Diff line number Diff line change
@@ -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: <turnkey-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.
131 changes: 131 additions & 0 deletions examples/kms/turnkey/index.ts
Original file line number Diff line number Diff line change
@@ -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<string>((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();
});
20 changes: 20 additions & 0 deletions examples/kms/turnkey/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
19 changes: 19 additions & 0 deletions examples/kms/turnkey/src/config.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
14 changes: 14 additions & 0 deletions examples/kms/turnkey/src/jaw.ts
Original file line number Diff line number Diff line change
@@ -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;
}
61 changes: 61 additions & 0 deletions examples/kms/turnkey/src/turnkey.ts
Original file line number Diff line number Diff line change
@@ -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;
}
12 changes: 12 additions & 0 deletions examples/kms/turnkey/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["*.ts"]
}
Loading