Skip to content
Open
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
29 changes: 29 additions & 0 deletions app/api/secret-data/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';
import { buildX402Offer, verifyX402Payment } from '@/lib/x402-helpers';
import { type Address } from 'viem';

export async function GET(request: Request) {
const headers = new Headers(request.headers);
const paymentProof = headers.get('x-payment') || undefined;
const recipient = (process.env.X402_RECIPIENT_ADDRESS || process.env.NEXT_PUBLIC_CMT_FAUCET_ADDRESS_MAINNET) as Address | undefined;

if (!recipient) {
return NextResponse.json({ error: 'recipient_not_configured' }, { status: 500 });
}

if (paymentProof) {
const ok = await verifyX402Payment(paymentProof, {
expectedRecipient: recipient,
expectedTokenSymbol: 'cUSD',
expectedAmount: '1.00',
});
if (ok) {
return NextResponse.json({ secret: 'contenido protegido' }, { status: 200 });
}
return NextResponse.json({ error: 'payment_invalid' }, { status: 402 });
}

const offer = buildX402Offer({ amount: '1.00', tokenSymbol: 'cUSD', recipient });
return NextResponse.json({ payment: offer }, { status: 402 });
}

23 changes: 23 additions & 0 deletions app/api/x402/cmt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextResponse } from 'next/server';
import { buildX402Offer, verifyX402Payment } from '@/lib/x402-helpers';
import { type Address } from 'viem';

export async function GET(request: Request) {
const headers = new Headers(request.headers);
const proof = headers.get('x-payment') || undefined;
const recipient = (process.env.X402_RECIPIENT_ADDRESS || '0xc5CE44D994C00F2FeA2079408e8b6c18b6D2F156') as Address | undefined;
if (!recipient) return NextResponse.json({ error: 'recipient_not_configured' }, { status: 500 });

if (proof) {
const ok = await verifyX402Payment(proof, {
expectedRecipient: recipient,
expectedTokenSymbol: 'X402',
expectedAmount: '1.00',
});
if (ok) return NextResponse.json({ payload: 'cmt_access_granted' }, { status: 200 });
return NextResponse.json({ error: 'payment_invalid' }, { status: 402 });
}

const offer = buildX402Offer({ amount: '1.00', tokenSymbol: 'X402', recipient });
return NextResponse.json({ payment: offer }, { status: 402 });
}
25 changes: 25 additions & 0 deletions app/api/x402/facilitator/settle/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextResponse } from 'next/server';
import { settleAuthorization } from '@/lib/x402/facilitator';

export async function POST(request: Request) {
try {
const body = await request.json();
const { txHash } = await settleAuthorization({
tokenAddress: body.tokenAddress,
eip712Name: body.eip712Name,
eip712Version: body.eip712Version,
from: body.from,
to: body.to,
value: BigInt(body.value),
validAfter: BigInt(body.validAfter),
validBefore: BigInt(body.validBefore),
nonce: body.nonce,
signature: body.signature,
chainId: body.chainId,
});
return NextResponse.json({ txHash }, { status: 200 });
} catch (e: any) {
return NextResponse.json({ error: e?.message || 'settle_failed' }, { status: 400 });
}
}

8 changes: 8 additions & 0 deletions app/api/x402/facilitator/status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { NextResponse } from 'next/server';
import { facilitatorStatus } from '@/lib/x402/facilitator';

export async function GET() {
const s = await facilitatorStatus();
return NextResponse.json(s, { status: s.ok ? 200 : 500 });
}

17 changes: 17 additions & 0 deletions app/api/x402/facilitator/verify/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NextResponse } from 'next/server';
import { verifyX402Payment } from '@/lib/x402-helpers';

export async function POST(request: Request) {
try {
const body = await request.json();
const ok = await verifyX402Payment(body.proof, {
expectedRecipient: body.recipient,
expectedTokenSymbol: body.tokenSymbol,
expectedAmount: body.amount,
});
return NextResponse.json({ ok }, { status: ok ? 200 : 402 });
} catch (e: any) {
return NextResponse.json({ error: e?.message || 'verify_failed' }, { status: 400 });
}
}

57 changes: 57 additions & 0 deletions app/api/x402/usdc/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { NextResponse } from 'next/server';
import { settleAuthorization } from '@/lib/x402/facilitator';
import { type Address } from 'viem';

function decodePaymentHeader(header: string | null) {
if (!header) return null;
try {
const maybeJson = header.startsWith('{') ? header : Buffer.from(header, 'base64').toString('utf-8');
const obj = JSON.parse(maybeJson);
return obj;
} catch {
return null;
}
}

export async function GET(request: Request) {
const headers = new Headers(request.headers);
const paymentHeader = headers.get('x-payment');
const recipient = process.env.X402_RECIPIENT_ADDRESS as Address | undefined;
if (!recipient) return NextResponse.json({ error: 'recipient_not_configured' }, { status: 500 });

if (paymentHeader) {
const payload = decodePaymentHeader(paymentHeader);
if (!payload) return NextResponse.json({ error: 'payment_payload_invalid' }, { status: 402 });

try {
const { txHash } = await settleAuthorization({
tokenAddress: payload.domain?.verifyingContract || payload.tokenAddress,
eip712Name: payload.eip712?.name || 'USD Coin',
eip712Version: payload.eip712?.version || '2',
from: payload.message?.from,
to: payload.message?.to ?? recipient,
value: BigInt(payload.message?.value || '0'),
validAfter: BigInt(payload.message?.validAfter || '0'),
validBefore: BigInt(payload.message?.validBefore || Math.floor(Date.now() / 1000 + 900)),
nonce: payload.message?.nonce,
signature: payload.signature,
chainId: payload.domain?.chainId ?? 84532,
});
return NextResponse.json({ ok: true, txHash }, { status: 200 });
} catch (e: any) {
return NextResponse.json({ error: e?.message || 'settle_failed' }, { status: 402 });
}
}

const chainId = 84532; // Base Sepolia for testing
return NextResponse.json({
payment: {
network: `base:${chainId}`,
token: 'USDC',
amount: '0.01',
recipient,
eip712: { name: 'USD Coin', version: '2' },
chainId,
},
}, { status: 402 });
}
61 changes: 61 additions & 0 deletions app/api/x402/x402/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { NextResponse } from 'next/server';
import { type Address } from 'viem';
import { tokens } from '@/config/tokens';
import { settleAuthorization } from '@/lib/x402/facilitator';

function decodePaymentHeader(header: string | null) {
if (!header) return null;
try {
const maybeJson = header.startsWith('{') ? header : Buffer.from(header, 'base64').toString('utf-8');
const obj = JSON.parse(maybeJson);
return obj;
} catch {
return null;
}
}

export async function GET(request: Request) {
const headers = new Headers(request.headers);
const paymentHeader = headers.get('x-payment');
const recipient = ('0xc5CE44D994C00F2FeA2079408e8b6c18b6D2F156') as Address | undefined;
if (!recipient) return NextResponse.json({ error: 'recipient_not_configured_' }, { status: 500 });

const x402Token = tokens.find(t => t.symbol === 'X402');
if (!x402Token) return NextResponse.json({ error: 'token_not_configured' }, { status: 500 });

if (paymentHeader) {
const payload = decodePaymentHeader(paymentHeader);
if (!payload) return NextResponse.json({ error: 'payment_payload_invalid' }, { status: 402 });

try {
const { txHash } = await settleAuthorization({
tokenAddress: (payload.domain?.verifyingContract || x402Token.address) as Address,
eip712Name: payload.eip712?.name || 'X402 Token',
eip712Version: payload.eip712?.version || '2',
from: payload.message?.from,
to: payload.message?.to ?? recipient,
value: BigInt(payload.message?.value || '0'),
validAfter: BigInt(payload.message?.validAfter || '0'),
validBefore: BigInt(payload.message?.validBefore || Math.floor(Date.now() / 1000 + 900)),
nonce: payload.message?.nonce,
signature: payload.signature,
chainId: payload.domain?.chainId ?? 42220,
});
return NextResponse.json({ ok: true, txHash }, { status: 200 });
} catch (e: any) {
return NextResponse.json({ error: e?.message || 'settle_failed' }, { status: 402 });
}
}

return NextResponse.json({
payment: {
network: 'celo:42220',
token: 'X402',
amount: '1.00',
recipient,
eip712: { name: 'X402 Token', version: '2' },
chainId: 42220,
tokenAddress: x402Token.address,
},
}, { status: 402 });
}
100 changes: 100 additions & 0 deletions app/x402-cmt/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use client';

import { useEffect, useState } from 'react';
import { Wallet, RefreshCw, CheckCircle2, ExternalLink } from 'lucide-react';
import { usePrivy } from '@privy-io/react-auth';
import { usePrivyCmtFetch } from '@/hooks/usePrivyCmtFetch';

export default function X402CmtPage() {
const { ready, authenticated, login } = usePrivy();
const fetchWithPayment = usePrivyCmtFetch();
const [status, setStatus] = useState<'idle'|'paying'|'success'|'error'>('idle');
const [message, setMessage] = useState<string>('');
const [txHash, setTxHash] = useState<string | undefined>(undefined);
useEffect(() => {
if (ready && !authenticated) {
login();
}
}, [ready, authenticated, login]);

const runTest = async () => {
setStatus('paying');
setMessage('');
setTxHash(undefined);
try {
const { response, txHash } = await fetchWithPayment('/api/x402/cmt');
setTxHash(txHash);
if (!response.ok) {
const b = await response.json().catch(() => ({}));
setStatus('error');
setMessage(b?.error || `HTTP ${response.status}`);
return;
}
const data = await response.json().catch(() => ({}));
setStatus('success');
setMessage(typeof data?.payload === 'string' ? data.payload : 'ok');
} catch (e: any) {
setStatus('error');
setMessage(e?.message || 'unknown error');
}
};

if (!ready) {
return (
<div className="min-h-screen flex items-center justify-center bg-celo-bg">
<div className="text-center">
<RefreshCw className="w-16 h-16 mx-auto mb-4 text-celo-yellow animate-spin" />
<h2 className="text-2xl font-bold text-celo-fg mb-2">Cargando...</h2>
<p className="text-celo-muted">Inicializando Privy</p>
</div>
</div>
);
}

if (!authenticated) {
return (
<div className="min-h-screen flex items-center justify-center bg-celo-bg">
<div className="text-center">
<Wallet className="w-16 h-16 mx-auto mb-4 text-celo-yellow" />
<h2 className="text-2xl font-bold text-celo-fg mb-2">Conecta tu Wallet</h2>
<p className="text-celo-muted mb-6">Necesitas conectar tu wallet para pagar 1 CMT</p>
<button onClick={() => login()} className="px-6 py-3 bg-celoLegacy-yellow text-black font-semibold rounded-xl hover:opacity-90 transition">Conectar</button>
</div>
</div>
);
}



return (
<div className="min-h-screen bg-celo-bg">
<div className="max-w-xl mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-celo-fg mb-6">Pagar 1 X402</h1>
<p className="text-celo-muted mb-6">Solicita contenido protegido en X402. Si el servidor responde 402, se paga 1 X402 con tu wallet y se reintenta automáticamente.</p>
<button
onClick={runTest}
disabled={status==='paying'}
className="px-6 py-3 bg-celoLegacy-yellow text-black rounded-xl font-semibold hover:opacity-90 transition disabled:opacity-50"
>
{status==='paying' ? 'Pagando...' : 'Solicitar Payload CMT'}
</button>

{status==='success' && (
<div className="mt-6 p-4 border border-green-200 bg-green-50 rounded-xl">
<div className="flex items-center gap-2 text-green-700 font-semibold">
<CheckCircle2 className="w-5 h-5" /> Pago verificado
</div>
<div className="mt-2 text-sm text-green-800">Payload: {message}</div>
{txHash && (
<a href={`https://celoscan.io/tx/${txHash}`} target="_blank" rel="noopener noreferrer" className="text-xs text-green-700 underline mt-2 inline-block">Ver TX</a>
)}
</div>
)}

{status==='error' && (
<div className="mt-6 p-4 border border-red-200 bg-red-50 rounded-xl text-red-700">Error: {message}</div>
)}
</div>
</div>
);
}
Loading
Loading