From 67cd1c2ecec6262acc21294d657c772f35b76bfa Mon Sep 17 00:00:00 2001 From: LeoFranklin015 Date: Wed, 25 Feb 2026 15:00:51 +0530 Subject: [PATCH 1/4] feat(kms-privy): add new example for Privy wallet integration with JAW --- examples/kms/privy/.claude/settings.json | 8 ++ examples/kms/privy/index.ts | 118 +++++++++++++++++++++++ examples/kms/privy/package.json | 18 ++++ examples/kms/privy/src/config.ts | 17 ++++ examples/kms/privy/src/jaw.ts | 14 +++ examples/kms/privy/src/privy.ts | 26 +++++ examples/kms/privy/tsconfig.json | 12 +++ 7 files changed, 213 insertions(+) create mode 100644 examples/kms/privy/.claude/settings.json create mode 100644 examples/kms/privy/index.ts create mode 100644 examples/kms/privy/package.json create mode 100644 examples/kms/privy/src/config.ts create mode 100644 examples/kms/privy/src/jaw.ts create mode 100644 examples/kms/privy/src/privy.ts create mode 100644 examples/kms/privy/tsconfig.json diff --git a/examples/kms/privy/.claude/settings.json b/examples/kms/privy/.claude/settings.json new file mode 100644 index 0000000..4870a07 --- /dev/null +++ b/examples/kms/privy/.claude/settings.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(npm install)", + "Bash(npx tsc --noEmit)" + ] + } +} diff --git a/examples/kms/privy/index.ts b/examples/kms/privy/index.ts new file mode 100644 index 0000000..8021562 --- /dev/null +++ b/examples/kms/privy/index.ts @@ -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((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(); +}); diff --git a/examples/kms/privy/package.json b/examples/kms/privy/package.json new file mode 100644 index 0000000..a30c8f1 --- /dev/null +++ b/examples/kms/privy/package.json @@ -0,0 +1,18 @@ +{ + "name": "@jaw-examples/kms-privy", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "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" + } +} diff --git a/examples/kms/privy/src/config.ts b/examples/kms/privy/src/config.ts new file mode 100644 index 0000000..db32b92 --- /dev/null +++ b/examples/kms/privy/src/config.ts @@ -0,0 +1,17 @@ +export const CHAIN_ID = 84532; // Base Sepolia + +export function getEnv() { + const privyAppId = process.env.NEXT_PUBLIC_PRIVY_APP_ID; + const privyAppSecret = process.env.PRIVY_APP_SECRET; + const jawApiKey = process.env.NEXT_PUBLIC_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 }; +} diff --git a/examples/kms/privy/src/jaw.ts b/examples/kms/privy/src/jaw.ts new file mode 100644 index 0000000..5aa24eb --- /dev/null +++ b/examples/kms/privy/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/privy/src/privy.ts b/examples/kms/privy/src/privy.ts new file mode 100644 index 0000000..ce76077 --- /dev/null +++ b/examples/kms/privy/src/privy.ts @@ -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; +} diff --git a/examples/kms/privy/tsconfig.json b/examples/kms/privy/tsconfig.json new file mode 100644 index 0000000..5f968d2 --- /dev/null +++ b/examples/kms/privy/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["*.ts"] +} From 087234e56da13409ec9218920e3734ee0a1938d7 Mon Sep 17 00:00:00 2001 From: LeoFranklin015 Date: Wed, 25 Feb 2026 15:01:01 +0530 Subject: [PATCH 2/4] feat(kms-privy): update workspace configuration and add environment variables for Privy integration --- examples/kms/privy/.env.example | 4 ++++ examples/kms/privy/package.json | 1 + examples/kms/privy/src/config.ts | 4 ++-- package.json | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 examples/kms/privy/.env.example diff --git a/examples/kms/privy/.env.example b/examples/kms/privy/.env.example new file mode 100644 index 0000000..907a4ae --- /dev/null +++ b/examples/kms/privy/.env.example @@ -0,0 +1,4 @@ +PRIVY_APP_ID= +JAW_API_KEY= +PRIVY_APP_SECRET= + diff --git a/examples/kms/privy/package.json b/examples/kms/privy/package.json index a30c8f1..55d6e27 100644 --- a/examples/kms/privy/package.json +++ b/examples/kms/privy/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "scripts": { + "dev": "tsx --env-file=.env.local index.ts", "start": "tsx --env-file=.env.local index.ts" }, "dependencies": { diff --git a/examples/kms/privy/src/config.ts b/examples/kms/privy/src/config.ts index db32b92..a0e2b16 100644 --- a/examples/kms/privy/src/config.ts +++ b/examples/kms/privy/src/config.ts @@ -1,9 +1,9 @@ export const CHAIN_ID = 84532; // Base Sepolia export function getEnv() { - const privyAppId = process.env.NEXT_PUBLIC_PRIVY_APP_ID; + const privyAppId = process.env.PRIVY_APP_ID; const privyAppSecret = process.env.PRIVY_APP_SECRET; - const jawApiKey = process.env.NEXT_PUBLIC_API_KEY; + const jawApiKey = process.env.JAW_API_KEY; if (!privyAppId || !privyAppSecret || !jawApiKey) { console.error('Missing required environment variables:'); diff --git a/package.json b/package.json index f11bd10..280e867 100644 --- a/package.json +++ b/package.json @@ -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" From 0c97b9b15a2222b81569e2980d96cf0015b979aa Mon Sep 17 00:00:00 2001 From: LeoFranklin015 Date: Wed, 25 Feb 2026 15:05:53 +0530 Subject: [PATCH 3/4] chore: added readme --- examples/kms/privy/README.md | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 examples/kms/privy/README.md diff --git a/examples/kms/privy/README.md b/examples/kms/privy/README.md new file mode 100644 index 0000000..f73cc33 --- /dev/null +++ b/examples/kms/privy/README.md @@ -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://console.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: + 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. From 57373cccca0625cf9302077a429da0b7182a595b Mon Sep 17 00:00:00 2001 From: LeoFranklin015 Date: Wed, 25 Feb 2026 15:07:53 +0530 Subject: [PATCH 4/4] fix: update Privy app ID link in README --- examples/kms/privy/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/kms/privy/README.md b/examples/kms/privy/README.md index f73cc33..e06757e 100644 --- a/examples/kms/privy/README.md +++ b/examples/kms/privy/README.md @@ -32,7 +32,7 @@ A server-side example that uses [Privy Server Wallets](https://docs.privy.io/gui | Variable | Description | |---|---| - | `PRIVY_APP_ID` | Your Privy app ID from [console.privy.io](https://console.privy.io) | + | `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) |