Skip to content
Closed
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
41 changes: 28 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,36 @@ on:
branches: [main]

jobs:
discover:
runs-on: ubuntu-latest
outputs:
examples: ${{ steps.find.outputs.examples }}
steps:
- uses: actions/checkout@v4

- name: Discover buildable examples
id: find
run: |
# Every example dir with a package.json that defines a `build` script
# (covers nested examples like eip7702/* and kms/*; skips node-only ones).
examples=$(
find examples -mindepth 2 -maxdepth 3 -name package.json -not -path '*/node_modules/*' |
while read -r pkg; do
if jq -e '.scripts.build' "$pkg" >/dev/null 2>&1; then
dirname "$pkg"
fi
done | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))'
)
echo "examples=$examples" >> "$GITHUB_OUTPUT"
echo "Discovered examples: $examples"

build:
needs: discover
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
example:
- nextjs-quickstart
- nextjs-send-transaction
- nextjs-sign-message
- nextjs-gas-sponsorship
- nextjs-ens-profiles
- nextjs-siwe
- nextjs-permissions
- nextjs-subscription
- vanilla-quickstart
- node-server-charge
example: ${{ fromJson(needs.discover.outputs.examples) }}

steps:
- uses: actions/checkout@v4
Expand All @@ -31,11 +46,11 @@ jobs:
bun-version: latest

- name: Install dependencies
working-directory: examples/${{ matrix.example }}
working-directory: ${{ matrix.example }}
run: bun install

- name: Build
working-directory: examples/${{ matrix.example }}
working-directory: ${{ matrix.example }}
run: bun run build
env:
NEXT_PUBLIC_JAW_API_KEY: test_key
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Official examples for the [JAW SDK](https://docs.jaw.id) — identity-first smar
| --- | --- |
| [nextjs-core](./examples/nextjs-core) | Same features as `nextjs-wagmi` using the raw JAW EIP-1193 provider (no wagmi) |
| [nextjs-headless-mode](./examples/nextjs-headless-mode) | Headless `Account` API — create, login, import passkeys, sign, transact, and manage permissions |
| [nextjs-coinbase-onramp](./examples/nextjs-coinbase-onramp) | Buy USDC on Base into a passkey account via Coinbase guest checkout — phone OTP, payment iframe, and live order status |
| [node-quickstart](./examples/node-quickstart) | Server-side smart account: sign messages, send and batch transactions in Node.js |

### EIP-7702 — Upgrade EOA to smart account
Expand Down Expand Up @@ -49,7 +50,7 @@ Official examples for the [JAW SDK](https://docs.jaw.id) — identity-first smar
npx nx dev nextjs-wagmi
```

Replace `nextjs-wagmi` with any example name: `nextjs-core`, `nextjs-headless-mode`, `node-quickstart`, `eip7702-node-quickstart`, `eip7702-turnkey`, `eip7702-privy-nextjs`, `kms-turnkey`, or `kms-privy`.
Replace `nextjs-wagmi` with any example name: `nextjs-core`, `nextjs-headless-mode`, `nextjs-coinbase-onramp`, `node-quickstart`, `eip7702-node-quickstart`, `eip7702-turnkey`, `eip7702-privy-nextjs`, `kms-turnkey`, or `kms-privy`.

## Documentation

Expand Down
565 changes: 270 additions & 295 deletions bun.lock

Large diffs are not rendered by default.

15 changes: 10 additions & 5 deletions examples/eip7702/node-quickstart/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,19 @@ async function main() {

console.log('Step 1 — Create EIP-7702 account');

const signer = privateKeyToAccount(PRIVATE_KEY);
// Guaranteed defined by the env guard above; TS doesn't carry that narrowing
// of module-level consts into this nested function, so assert it here.
const signer = privateKeyToAccount(PRIVATE_KEY!);
const account = await Account.fromLocalAccount(
{
chainId: CHAIN_ID,
apiKey: JAW_API_KEY,
apiKey: JAW_API_KEY!,
...(PAYMASTER_URL ? { paymasterUrl: PAYMASTER_URL } : {}),
},
signer,
// The example's direct viem and @jaw.id/core's viem can resolve to two
// different versions in this workspace; the LocalAccount shape is identical
// but nominally distinct, so bridge it to the parameter type here.
signer as Parameters<typeof Account.fromLocalAccount>[1],
{ eip7702: true },
);

Expand Down Expand Up @@ -84,7 +89,7 @@ async function main() {
process.stdout.write(' waiting : ');

for (;;) {
const status = account.getCallStatus(id);
const status = await account.getCallStatus(id);
if (status && status.status !== 100) {
const label = status.status === 200 ? 'confirmed' : `failed (code ${status.status})`;
console.log(label);
Expand All @@ -110,7 +115,7 @@ async function main() {
process.stdout.write(' waiting : ');

for (;;) {
const status2 = account.getCallStatus(id2);
const status2 = await account.getCallStatus(id2);
if (status2 && status2.status !== 100) {
const label = status2.status === 200 ? 'confirmed' : `failed (code ${status2.status})`;
console.log(label);
Expand Down
1 change: 1 addition & 0 deletions examples/eip7702/node-quickstart/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022", "DOM"],
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
Expand Down
4 changes: 4 additions & 0 deletions examples/eip7702/privy-nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export const metadata: Metadata = {
description: 'Upgrade a Privy embedded wallet to a JAW smart account via EIP-7702',
};

// The Privy provider initializes with a runtime app ID and can't be prerendered
// at build time, so render this route dynamically (at request time).
export const dynamic = 'force-dynamic';

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
Expand Down
13 changes: 12 additions & 1 deletion examples/eip7702/privy-nextjs/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
webpack: (config) => {
// @privy-io/react-auth references an optional Solana mini-app connector that
// this EVM-only example doesn't use and isn't installed. Stub it so the
// bundler doesn't fail to resolve it.
config.resolve.alias = {
...config.resolve.alias,
"@farcaster/mini-app-solana": false,
};
return config;
},
};

export default nextConfig;
13 changes: 13 additions & 0 deletions examples/nextjs-coinbase-onramp/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# --- Browser (JAW passkey account) ---
# Publishable API key from https://dashboard.jaw.id
NEXT_PUBLIC_JAW_API_KEY=

# --- Server only (Coinbase on-ramp proxy) ---
# These are NEVER exposed to the browser. The Next.js route handlers in
# app/api/onramp/* attach the x-api-key when forwarding to the proxy.
#
# Base path of the JustaName proxy on-ramp API, including the /proxy/v2/onramp prefix.
# For a proxy booted locally, use: http://localhost:3013/proxy/v2/onramp
ONRAMP_PROXY_BASE_URL=https://api.justaname.id/proxy/v2/onramp
# The x-api-key the proxy expects on start / validate-otp / orders.
ONRAMP_API_KEY=
43 changes: 43 additions & 0 deletions examples/nextjs-coinbase-onramp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*
# ...but the template IS committed, like the other examples
!.env.example

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
79 changes: 79 additions & 0 deletions examples/nextjs-coinbase-onramp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# JAW Coinbase On-Ramp

A Next.js app that buys **USDC on Base** into a passkey smart account, using the
Coinbase guest-checkout on-ramp behind the JustaName proxy. No Coinbase account,
no seed phrase — the user pays with Apple Pay / Google Pay and the USDC lands
straight in their `@jaw.id/core` smart account.

## What This Demonstrates

| Page | Feature |
| --- | --- |
| `/` | Create / login / import a passkey smart account (`@jaw.id/core`) |
| `/buy` | Phone-OTP → Coinbase payment iframe → live order status |

The proxy `x-api-key` never reaches the browser: the UI calls our own Next.js
route handlers (`app/api/onramp/*`), which forward to the proxy server-side.

```
Browser ──► /api/onramp/start ──► proxy /proxy/v2/onramp/start
──► /api/onramp/validate-otp ──► proxy /proxy/v2/onramp/validate-otp
──► /api/onramp/orders/[id] ──► proxy /proxy/v2/onramp/orders/:id
```

## Flow

1. **Connect** a passkey account on `/`. Its address is the on-ramp destination.
2. On `/buy`, enter US phone (E.164 `+1…`), email, and amount ($2–$500).
3. `POST /start` → proxy sends the OTP. With the proxy's `twilio` provider this
is a real SMS; with the `mock` provider (local/sandbox) no SMS is sent and the
code is whatever `ONRAMP_MOCK_OTP_CODE` is on the proxy (default `000000`).
4. Enter the code → `POST /validate-otp` → proxy creates the Coinbase order and
returns an `embeddable.url`.
5. The app embeds that URL in an `<iframe allow="payment">` for Apple/Google Pay.
6. The app polls `GET /orders/:id` every 4s until `COMPLETED` / `FAILED` /
`EXPIRED`. In production the Coinbase webhook to the proxy drives the final
status; locally the polled value reflects Coinbase's live order status
directly (Coinbase can't reach a `localhost` webhook).

## Setup

1. Copy the environment file:
```bash
cp examples/nextjs-coinbase-onramp/.env.example examples/nextjs-coinbase-onramp/.env.local
```

2. Fill in your values:

| Variable | Scope | Description |
| --- | --- | --- |
| `NEXT_PUBLIC_JAW_API_KEY` | browser | Publishable key from [dashboard.jaw.id](https://dashboard.jaw.id) |
| `ONRAMP_PROXY_BASE_URL` | server | Proxy base incl. `/proxy/v2/onramp` |
| `ONRAMP_API_KEY` | server | `x-api-key` the proxy expects |

3. From the repo root, run:
```bash
npx nx dev nextjs-coinbase-onramp
```

Open [http://localhost:3000](http://localhost:3000).

## Constraints (enforced by Coinbase guest checkout)

- **US only** — phone must be `+1XXXXXXXXXX` (E.164).
- **USDC on Base** only.
- **$2–$500** per purchase (guest weekly cap).
- **OTP required** — Coinbase needs a verified phone for guest orders.

## Notes

- The smart account is created on Arbitrum Sepolia, but its counterfactual
address is identical across EVM chains, so the same address receives USDC on
Base.
- For local testing without real payments, run the proxy with `ONRAMP_SANDBOX=true`
— the returned iframe URL points at the Coinbase sandbox payment sheet.

## Documentation

- [JAW Docs](https://docs.jaw.id)
- [Dashboard](https://dashboard.jaw.id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NextRequest, NextResponse } from "next/server";

const BASE = process.env.ONRAMP_PROXY_BASE_URL!;
const KEY = process.env.ONRAMP_API_KEY!;

export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const upstream = await fetch(`${BASE}/orders/${encodeURIComponent(id)}`, {
headers: { "x-api-key": KEY },
});
const data = await upstream.json();
return NextResponse.json(data, { status: upstream.status });
}
15 changes: 15 additions & 0 deletions examples/nextjs-coinbase-onramp/app/api/onramp/start/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextRequest, NextResponse } from "next/server";

const BASE = process.env.ONRAMP_PROXY_BASE_URL!;
const KEY = process.env.ONRAMP_API_KEY!;

export async function POST(req: NextRequest) {
const body = await req.json();
const upstream = await fetch(`${BASE}/start`, {
method: "POST",
headers: { "content-type": "application/json", "x-api-key": KEY },
body: JSON.stringify(body),
});
const data = await upstream.json();
return NextResponse.json(data, { status: upstream.status });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NextRequest, NextResponse } from "next/server";

const BASE = process.env.ONRAMP_PROXY_BASE_URL!;
const KEY = process.env.ONRAMP_API_KEY!;

export async function POST(req: NextRequest) {
const body = await req.json();
// Forward the end-user's IP — Coinbase uses it for the US region check.
const clientIp = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
const upstream = await fetch(`${BASE}/validate-otp`, {
method: "POST",
headers: { "content-type": "application/json", "x-api-key": KEY },
body: JSON.stringify(clientIp ? { ...body, clientIp } : body),
});
const data = await upstream.json();
return NextResponse.json(data, { status: upstream.status });
}
33 changes: 33 additions & 0 deletions examples/nextjs-coinbase-onramp/app/buy/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import { useAccount } from "@/app/providers";
import { OnrampWidget } from "@/components/onramp-widget";

export default function BuyPage() {
const { account } = useAccount();

if (!account) {
return (
<div>
<h2 className="text-2xl font-bold">Buy Crypto</h2>
<p className="mt-2 text-sm text-gray-400">
Connect an account first — the on-ramp needs a destination address.
</p>
</div>
);
}

return (
<div className="max-w-xl">
<div className="mb-6">
<h2 className="text-2xl font-bold">Buy Crypto</h2>
<p className="mt-1 text-sm text-gray-400">
Buy USDC on Base with Apple Pay / Google Pay via Coinbase guest
checkout. USDC lands in your passkey account.
</p>
</div>

<OnrampWidget destinationAddress={account.address} />
</div>
);
}
18 changes: 18 additions & 0 deletions examples/nextjs-coinbase-onramp/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@import "tailwindcss";

:root {
--background: #030712;
--foreground: #f9fafb;
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}

body {
background: var(--background);
color: var(--foreground);
}
Loading
Loading