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
8 changes: 8 additions & 0 deletions examples/kms/privy/.claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(npm install)",
"Bash(npx tsc --noEmit)"
]
}
}
4 changes: 4 additions & 0 deletions examples/kms/privy/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
PRIVY_APP_ID=
JAW_API_KEY=
PRIVY_APP_SECRET=

76 changes: 76 additions & 0 deletions examples/kms/privy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# JAW + Privy Server Wallets

A server-side example that uses [Privy Server Wallets](https://docs.privy.io/guide/server-wallets) as the key management layer for JAW smart accounts — no browser or passkey required.

## What This Demonstrates

| Feature | Description |
|---------|-------------|
| Privy server wallet creation | Create and manage wallets via Privy's server-side API |
| JAW smart account from Privy wallet | Wrap a Privy server 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 Privy server wallet (`@privy-io/server-auth`)
2. **Convert** the Privy wallet into a viem `LocalAccount` (`@privy-io/server-auth/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 |
|---|---|
| `PRIVY_APP_ID` | Your Privy app ID from [console.privy.io](https://dashboard.privy.io) |
| `PRIVY_APP_SECRET` | Your Privy app secret |
| `JAW_API_KEY` | Your API key from [dashboard.jaw.id](https://dashboard.jaw.id) |

> **Note:** Enable **Server Wallets** in your Privy dashboard under the **Server Wallets** tab.

3. Run the script:
```bash
npm run dev
```

## Expected Output

```
--- Privy + JAW Server Wallet ---

[1] Create new wallet
[2] Load existing wallet

Choice: 1

Creating new server wallet...
Wallet ID: <privy-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

**Privy Server Wallets** — Privy manages the private key on its infrastructure. Your server interacts with the wallet through their API, and you never handle raw key material.

**`createViemAccount`** — Bridges a Privy server 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, Privy, or another KMS) and creates a smart account.
118 changes: 118 additions & 0 deletions examples/kms/privy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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 { initPrivy, createServerWallet, loadWallet, getViemAccount } from './src/privy.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 privy = initPrivy(env.privyAppId, env.privyAppSecret);

console.log('\n--- Privy + JAW Server Wallet ---\n');

const choice = await ask(' [1] Create new wallet\n [2] Load existing wallet\n\n Choice: ');

let walletId: string;
let walletAddress: string;

if (choice.trim() === '2') {
const id = await ask(' Enter wallet ID: ');
const wallet = await loadWallet(privy, id.trim());
walletId = wallet.id;
walletAddress = wallet.address;
console.log(`\n Loaded wallet ${walletId}`);
} else {
console.log('\n Creating new server wallet...');
const wallet = await createServerWallet(privy);
walletId = wallet.id;
walletAddress = wallet.address;
console.log(` Wallet ID: ${walletId}`);
}

console.log(` Address: ${walletAddress}`);

console.log('\n Creating viem account...');
const localAccount = await getViemAccount(privy, walletId, 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();
});
19 changes: 19 additions & 0 deletions examples/kms/privy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@jaw-examples/kms-privy",
"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",
"@privy-io/server-auth": "^1.18.4",
"viem": "^2.23.2"
},
"devDependencies": {
"tsx": "^4.19.4",
"typescript": "^5.7.3"
}
}
17 changes: 17 additions & 0 deletions examples/kms/privy/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const CHAIN_ID = 84532; // Base Sepolia

export function getEnv() {
const privyAppId = process.env.PRIVY_APP_ID;
const privyAppSecret = process.env.PRIVY_APP_SECRET;
const jawApiKey = process.env.JAW_API_KEY;

if (!privyAppId || !privyAppSecret || !jawApiKey) {
console.error('Missing required environment variables:');
if (!privyAppId) console.error(' NEXT_PUBLIC_PRIVY_APP_ID');
if (!privyAppSecret) console.error(' PRIVY_APP_SECRET');
if (!jawApiKey) console.error(' NEXT_PUBLIC_API_KEY');
process.exit(1);
}

return { privyAppId, privyAppSecret, jawApiKey };
}
14 changes: 14 additions & 0 deletions examples/kms/privy/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;
}
26 changes: 26 additions & 0 deletions examples/kms/privy/src/privy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { PrivyClient } from '@privy-io/server-auth';
import { createViemAccount } from '@privy-io/server-auth/viem';

export function initPrivy(appId: string, appSecret: string) {
return new PrivyClient(appId, appSecret);
}

export async function createServerWallet(privy: PrivyClient) {
const wallet = await privy.walletApi.create({ chainType: 'ethereum' });
return wallet;
}

export async function loadWallet(privy: PrivyClient, walletId: string) {
const wallet = await privy.walletApi.getWallet({ id: walletId });
return wallet;
}

export async function getViemAccount(privy: PrivyClient, walletId: string, address: string) {
const localAccount = await createViemAccount({
walletId,
address: address as `0x${string}`,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
privy: privy as any,
});
return localAccount;
}
12 changes: 12 additions & 0 deletions examples/kms/privy/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"]
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jaw-examples",
"private": true,
"workspaces": ["examples/*"],
"workspaces": ["examples/*","examples/kms/*"],
"scripts": {
"dev": "nx dev",
"build": "nx run-many -t build"
Expand Down
Loading