Skip to content

Commit a9ad9d4

Browse files
committed
add paymaster phrases demo app
1 parent 0571781 commit a9ad9d4

34 files changed

+24400
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["next/core-web-vitals", "next/typescript"]
3+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
.yarn/install-state.gz
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# local env files
29+
.env*
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# You can remove this file if you don't want to use Yarn package manager.
2+
nodeLinker: node-modules
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Lingos - A phrase completion game
2+
3+
An onchain phrase completion game that tests your knowledge of phrases, idioms, and proverbs from around the world. Challenge yourself to recognize and complete common expressions while earning points for correct answers.
4+
5+
🎮 [Play Now](https://play-lingos.vercel.app/)
6+
7+
## Overview
8+
9+
Lingos combines traditional language learning with modern web technologies and blockchain integration. Players can:
10+
11+
- Test their knowledge of international phrases and expressions
12+
- Connect their [Base Smart Wallet](https://docs.base.org/identity/smart-wallet/quickstart) for gasless experience
13+
14+
## Technologies Used
15+
16+
### Frontend
17+
18+
- **Next.js 14** - React framework for production
19+
- **OnchainKit** - Onchain Frontend Library
20+
21+
### Blockchain Integration
22+
23+
- **wagmi** - React Hooks for Ethereum
24+
- **viem** - TypeScript Interface for Ethereum
25+
26+
## Getting Started
27+
28+
### Prerequisites
29+
30+
- Node.js 18+ installed
31+
- npm, yarn, or pnpm package manager
32+
- A modern web browser
33+
- (Optional) Web3 wallet for full features
34+
35+
### Local Development
36+
37+
1. Clone the repository:
38+
39+
```bash
40+
cd demos/paymaster/onchain-game-lingos
41+
```
42+
43+
2. Install dependencies:
44+
45+
```bash
46+
npm install
47+
```
48+
49+
3. Start the development server:
50+
51+
```bash
52+
npm run dev
53+
```
54+
55+
4. Open [http://localhost:3000](http://localhost:3000) in your browser
56+
57+
### Environment Variables
58+
59+
Create a `.env.local` file with:
60+
61+
```env
62+
NEXT_PUBLIC_ONCHAINKIT_API_KEY=your_api_key
63+
NEXT_PUBLIC_PAYMASTER_PROXY_SERVER_URL=your_proxy_url
64+
```
65+
66+
## License
67+
68+
This project is licensed under the MIT License - see the LICENSE file for details.
69+
70+
## Acknowledgments
71+
72+
- Built with [onchainkit](https://onchainkit.xyz)
73+
- Deployed on [Vercel](https://vercel.com)
74+
- Powered by [Next.js](https://nextjs.org)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { Redis } from '@upstash/redis';
3+
import { decodeAbiParameters } from 'viem';
4+
5+
// Initialize Upstash Redis client
6+
const redis = new Redis({
7+
url: process.env.UPSTASH_REDIS_REST_URL!,
8+
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
9+
});
10+
11+
export async function POST(req: NextRequest) {
12+
try {
13+
const body = await req.json();
14+
console.log('Received webhook payload:', JSON.stringify(body, null, 2));
15+
16+
// Validate the webhook payload structure
17+
if (
18+
!body ||
19+
!body.event ||
20+
!body.event.data ||
21+
!body.event.data.block ||
22+
!Array.isArray(body.event.data.block.logs) ||
23+
body.event.data.block.logs.length === 0
24+
) {
25+
console.error('Invalid webhook payload structure:', body);
26+
return NextResponse.json(
27+
{ error: 'Invalid webhook payload structure' },
28+
{ status: 400 }
29+
);
30+
}
31+
32+
const log = body.event.data.block.logs[0];
33+
34+
// Validate log structure
35+
if (!log.data || !log.topics || log.topics.length < 2) {
36+
console.error('Invalid log structure:', log);
37+
return NextResponse.json(
38+
{ error: 'Invalid log structure' },
39+
{ status: 400 }
40+
);
41+
}
42+
43+
// 1) Decode the single uint256 "score" from log.data (hex)
44+
const [scoreBigInt] = decodeAbiParameters(
45+
[{ type: 'uint256', name: 'score' }],
46+
log.data as `0x${string}`
47+
);
48+
const score = Number(scoreBigInt);
49+
50+
// 2) Extract player address from topics[1]
51+
// topics[1] is zero-padded 32 bytes: strip leading zeros
52+
const topic1 = log.topics[1] as string;
53+
const player = `0x${topic1.slice(26)}`;
54+
55+
console.log('Processing score update:', {
56+
player,
57+
score,
58+
transactionHash: log.transaction.hash,
59+
});
60+
61+
// 3) Upsert into a Redis sorted set called "leaderboard"
62+
const result = await redis.zadd('leaderboard', { score, member: player });
63+
console.log('Redis write result:', result);
64+
65+
// Verify the write by reading back the data
66+
const leaderboard = await redis.zrange('leaderboard', 0, -1, {
67+
withScores: true,
68+
});
69+
console.log('Current leaderboard state:', leaderboard);
70+
71+
return NextResponse.json({
72+
ok: true,
73+
result,
74+
processed: {
75+
player,
76+
score,
77+
transactionHash: log.transaction.hash,
78+
},
79+
});
80+
} catch (err) {
81+
console.error('❌ /api/events/highscore error:', err);
82+
return NextResponse.json(
83+
{
84+
error: 'Internal server error',
85+
details: err instanceof Error ? err.message : 'Unknown error',
86+
},
87+
{ status: 500 }
88+
);
89+
}
90+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { NextResponse } from 'next/server';
2+
import lingosData from '@/data/lingos.json';
3+
4+
export const dynamic = 'force-dynamic';
5+
export const revalidate = 0;
6+
7+
export async function GET() {
8+
try {
9+
const lingos = lingosData.lingos;
10+
console.log(`Total lingos available: ${lingos.length}`);
11+
12+
const randomIndex = Math.floor(Math.random() * lingos.length);
13+
console.log(`Selected random index: ${randomIndex}`);
14+
15+
const randomLingo = lingos[randomIndex];
16+
console.log('Serving lingo:', {
17+
id: randomLingo.id,
18+
prompt: randomLingo.prompt,
19+
answer: randomLingo.answer,
20+
timestamp: new Date().toISOString(),
21+
});
22+
23+
// Return response with no-cache headers
24+
return new NextResponse(JSON.stringify(randomLingo), {
25+
status: 200,
26+
headers: {
27+
'Content-Type': 'application/json',
28+
'Cache-Control':
29+
'no-store, no-cache, must-revalidate, proxy-revalidate',
30+
Pragma: 'no-cache',
31+
Expires: '0',
32+
},
33+
});
34+
} catch (error) {
35+
console.error('Error serving lingo:', error);
36+
return NextResponse.json(
37+
{ error: 'Failed to serve lingo' },
38+
{ status: 500 }
39+
);
40+
}
41+
}

0 commit comments

Comments
 (0)