feat: initialize smirk-website with connect flow

- Next.js 16 with TypeScript and Tailwind CSS
- Single-asset authentication (user picks favorite coin to sign)
- Logo with glow animation
- Coin picker UI with all 5 assets
- API client for backend auth endpoints
- TypeScript types for window.smirk extension API
This commit is contained in:
jwinterm
2026-01-25 08:18:14 -05:00
commit 20a27d3fd2
26 changed files with 7516 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
# 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*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+130
View File
@@ -0,0 +1,130 @@
# Smirk Website
User dashboard for Smirk wallet users. Requires the Smirk browser extension to access.
## Overview
This website serves as a hub for Smirk wallet users to:
- View their account UUID and balances
- See tipping history (sent and received)
- Manage connected accounts (Telegram, Discord, etc.)
- Future: Chat/messaging between users, direct tipping, etc.
**Important**: This site is ONLY accessible to users with the Smirk extension installed. Authentication happens via cryptographic signatures from the user's wallet keys.
## Architecture
### Deployment Options
The website is designed to be deployable either:
1. **Alongside the backend** - Served from the same server as smirk-backend (current plan)
2. **Separately** - As a standalone deployment (e.g., Vercel, separate server)
Keep this flexibility in mind when developing - avoid tight coupling to backend internals.
### Extension Communication
The Smirk extension injects a `window.smirk` API object into web pages. The website uses this to:
1. **Detect extension** - Check if `window.smirk` exists
2. **Connect** - Request user's public keys (with user approval)
3. **Sign** - Request signatures for authentication challenges (BTC/LTC: ECDSA, XMR/WOW/Grin: Ed25519)
```
┌─────────────┐ window.smirk ┌─────────────────┐
│ Website │ ◄──────────────────► │ Smirk Extension │
└──────┬──────┘ └────────┬────────┘
│ │
│ POST /auth/verify │ User approves
│ { publicKeys, signatures } │ in popup
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ Backend │ │ Extension UI │
└─────────────┘ └─────────────────┘
```
### Authentication Flow
1. User clicks "Connect with Smirk"
2. Website calls `window.smirk.connect()`
3. Extension shows approval popup
4. User approves → Extension returns public keys for all 5 assets
5. Website calls `POST /api/v1/auth/website/challenge` with origin
6. Backend returns challenge message (with nonce + timestamp + origin)
7. Website calls `window.smirk.signMessage(challenge)`
8. Extension shows signing popup with the message
9. User approves → Extension returns signatures from all 5 keys
10. Website sends `{ challenge_id, signatures }` to `POST /api/v1/auth/website/verify`
11. Backend verifies signatures, looks up user by BTC pubkey_hash, issues JWT session
12. Website stores JWT, user is now authenticated
**Future**: Allow single-asset auth (user picks their favorite coin to sign with).
### Tech Stack
- **Framework**: Next.js 14+ (App Router)
- **Styling**: Tailwind CSS
- **State**: React Context / Zustand (TBD)
- **Auth**: JWT sessions verified against backend
## Development
```bash
# Install dependencies
npm install
# Run development server
npm run dev
# Build for production
npm run build
# Start production server
npm start
```
## Environment Variables
```bash
# Backend API URL
NEXT_PUBLIC_API_URL=http://localhost:3000
# For production
NEXT_PUBLIC_API_URL=https://api.smirk.cash
```
## Project Structure
```
src/
├── app/ # Next.js App Router pages
│ ├── page.tsx # Landing / Connect page
│ ├── dashboard/ # Protected dashboard routes
│ │ ├── page.tsx # Main dashboard (balances, UUID)
│ │ ├── history/ # Tip history
│ │ └── settings/ # Account settings
│ └── layout.tsx # Root layout
├── components/ # React components
│ ├── ConnectButton.tsx # "Connect with Smirk" button
│ ├── Dashboard/ # Dashboard components
│ └── ui/ # Shared UI components
├── lib/
│ ├── smirk.ts # window.smirk API wrapper
│ ├── auth.ts # Authentication utilities
│ └── api.ts # Backend API client
└── types/
└── smirk.d.ts # TypeScript types for window.smirk
```
## Security Considerations
- Challenge messages include timestamps to prevent replay attacks
- Signatures are verified server-side against registered public keys
- JWT tokens have reasonable expiration
- HTTPS required in production
- CSP headers to prevent XSS
## Related Projects
- [smirk-extension](../smirk-extension) - Browser extension (injects `window.smirk`)
- [smirk-backend](../smirk-backend) - Rust backend API
+147
View File
@@ -0,0 +1,147 @@
# Smirk Website TODO
## Phase 1: Foundation (Critical Path)
### Extension API (DONE - 2026-01-22)
- [x] **Inject `window.smirk` API** - Content script injects API object into pages
- [x] **`window.smirk.connect()`** - Returns public keys for all 5 assets
- [x] **`window.smirk.signMessage(message)`** - Signs message with all 5 asset keys
- [x] **`window.smirk.isConnected()`** - Check if site is connected
- [x] **`window.smirk.disconnect()`** - Revoke site access
- [x] **`window.smirk.getPublicKeys()`** - Get public keys if already connected
- [x] **Approval popup UI** - Show user what site is requesting, allow approve/deny
- [x] **Origin allowlist** - Track which origins user has approved (persisted to storage)
### Backend Auth Endpoints (DONE - 2026-01-24)
- [x] `POST /api/v1/auth/website/challenge` - Generate challenge with nonce + origin
- [x] `POST /api/v1/auth/website/verify` - Verify signatures, issue JWT session token
- [x] `GET /api/v1/auth/me` - Get current user info from JWT
- [ ] JWT middleware for protected routes (frontend side)
### Website Basics
- [ ] Initialize Next.js project with App Router
- [ ] Tailwind CSS setup
- [ ] Basic layout (header, footer)
- [ ] Landing page with "Connect with Smirk" button
- [ ] Extension detection (`window.smirk` check)
- [ ] "Install Smirk" fallback if extension not detected
## Phase 2: Core Dashboard
### Authentication Flow
- [ ] Connect button triggers `window.smirk.connect()`
- [ ] Challenge generation and signing
- [ ] Backend verification and JWT storage
- [ ] Protected route middleware
- [ ] Logout functionality
### Dashboard - Basic
- [ ] Display user UUID
- [ ] Display balances for all 5 assets
- [ ] Auto-refresh balances
- [ ] Loading states and error handling
### Dashboard - Tip History
- [ ] List of tips sent
- [ ] List of tips received
- [ ] Pagination
- [ ] Filter by asset type
- [ ] Click to view tip details
## Phase 3: Account Management
### Connected Accounts
- [ ] Link Telegram account
- [ ] Link Discord account
- [ ] Link other platforms (Twitter/X, etc.)
- [ ] Manage/unlink accounts
- [ ] Display linked accounts on dashboard
### Settings
- [ ] Notification preferences
- [ ] Privacy settings
- [ ] Export account data
## Phase 4: Social Features (Future)
### Direct Tipping
- [ ] Send tip to another Smirk user by UUID
- [ ] Send tip via username/handle
- [ ] Tip confirmation flow
- [ ] Tip notifications
### Messaging (Future)
- [ ] Direct messages between users
- [ ] Encrypted messaging (E2E?)
- [ ] Message notifications
### Discovery (Future)
- [ ] Public profiles (opt-in)
- [ ] Search users
- [ ] Leaderboards?
## Technical Debt / Polish
- [ ] Comprehensive error handling
- [ ] Loading skeletons
- [ ] Mobile responsive design
- [ ] Dark/light theme (sync with extension setting?)
- [ ] Accessibility (a11y)
- [ ] SEO optimization
- [ ] Analytics integration
- [ ] Rate limiting on frontend
- [ ] E2E tests with Playwright
## Deployment
- [ ] Docker configuration
- [ ] Nginx reverse proxy setup (alongside backend)
- [ ] Environment configuration
- [ ] CI/CD pipeline
- [ ] Monitoring and alerting
---
## Notes
### Extension Work Required (add to smirk-extension/TODO.md)
The website cannot function without these extension features:
1. **Content script injection** - Must inject `window.smirk` on all pages (or configurable origins)
2. **Message passing** - Content script ↔ Background script communication
3. **Approval UI** - Popup for connect/sign requests
4. **Security** - Origin tracking, user approval persistence
### Signing Algorithm Notes
For message signing (all implemented in extension):
- **BTC/LTC**: ECDSA signature (secp256k1) with Bitcoin message signing format
- **XMR/WOW**: Ed25519 signature using private spend key
- **Grin**: Ed25519 signature using slatepack key
Backend must implement verification for all signature types.
### Challenge Message Format
```
Sign this message to authenticate with smirk.cash
Timestamp: 2026-01-22T12:00:00Z
Nonce: abc123...
Origin: https://smirk.cash
```
Challenge should expire after ~5 minutes to prevent replay attacks.
### Single-Asset Authentication (Future Enhancement)
**Current implementation**: Backend requires signatures from all 5 asset keys.
**Better approach**: Let users pick their favorite coin to sign with. Benefits:
1. **Simpler UX** - User chooses one coin, signs once
2. **User data** - "What's your favorite coin?" gives us insight into our user base
3. **Proof of concept** - Demonstrates wallet-based auth for ANY of these 5 coins
4. **Still secure** - We already know all 5 pubkeys; signing with ANY one proves wallet ownership
This positions Smirk as infrastructure for wallet-based authentication across multiple cryptocurrencies. Other websites could use the same pattern - "Sign in with Bitcoin/Litecoin/Monero/Wownero/Grin".
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+6538
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "smirk-website",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"next": "16.1.4",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.4",
"tailwindcss": "^4",
"typescript": "^5"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+7
View File
@@ -0,0 +1,7 @@
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
<g transform="translate(0.00630876,-0.00301984)">
<path fill="#f7931a" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
<path fill="#FFF" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>␍
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->␍
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"␍
viewBox="0 0 191 191" style="enable-background:new 0 0 191 191;" xml:space="preserve">␍
<style type="text/css">␍
.st0{fill:#FEF102;}␍
</style>␍
<circle cx="95.5" cy="95.5" r="95.5"/>␍
<circle class="st0" cx="95.3" cy="95.5" r="85.3"/>␍
<path d="M135.7,63.5c-2-4.6-3.9-13.6-9.1-15.6c-6.7-2.6-10.9,14.3-11.9,18.6h-1c-1.7-7.3-4-17.7-12-20c-3.7,15.6,3.9,32.5,11,46␍
c7.6-3.6,11.3-15,12-23h1l8,24c6.4-1.8,8.7-8.2,11-14c5-12.4,9.2-27.7,6-41C141.1,41.2,138.2,55,135.7,63.5 M39.7,95.5␍
c8.6-4.1,12.7-17.1,14-26h1c1,4.3,3.7,15.4,9.1,16.1c6.8,0.9,10.4-14.3,10.9-19.1h1c2.2,7.5,4.5,17.3,12,21␍
c2.8-11.7-0.5-24.1-4.8-35c-1.1-2.8-3.3-10.4-7.2-10.4c-6.3,0-9.9,16.7-11,21.4h-1l-8-24C41.6,46.2,34,82,39.7,95.5 M24.7,107.5␍
c10.4,45.6,58.6,71.4,102,52.1c15.8-7,28.3-19.5,35.5-35.1c1.9-4,5.5-11.4,3.4-16.7c-2.1-5.3-22.8,3.4-27.9,5.6␍
c-0.7,0.3-1.2,1-1.4,1.8c-0.3,2.2,1.2,4.3,3.4,4.6c0.1,0,0.2,0,0.3,0l8.7,0.7c-15.7,30.4-58.1,43.8-88,19.5␍
C54,134.6,48,128.2,44,120.5c-1.9-3.7-3.7-8.4-7-11C33.8,107.1,29,106.3,24.7,107.5L24.7,107.5z"/>␍
</svg>␍

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 82.6 82.6"><title>litecoin-ltc-logo</title><circle cx="41.3" cy="41.3" r="36.83" style="fill:#fff"/><path d="M41.3,0A41.3,41.3,0,1,0,82.6,41.3h0A41.18,41.18,0,0,0,41.54,0ZM42,42.7,37.7,57.2h23a1.16,1.16,0,0,1,1.2,1.12v.38l-2,6.9a1.49,1.49,0,0,1-1.5,1.1H23.2l5.9-20.1-6.6,2L24,44l6.6-2,8.3-28.2a1.51,1.51,0,0,1,1.5-1.1h8.9a1.16,1.16,0,0,1,1.2,1.12v.38L43.5,38l6.6-2-1.4,4.8Z" style="fill:#345d9d"/></svg>

After

Width:  |  Height:  |  Size: 489 B

+1
View File
@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3756.09 3756.49"><title>monero</title><path d="M4128,2249.81C4128,3287,3287.26,4127.86,2250,4127.86S372,3287,372,2249.81,1212.76,371.75,2250,371.75,4128,1212.54,4128,2249.81Z" transform="translate(-371.96 -371.75)" style="fill:#fff"/><path id="_149931032" data-name=" 149931032" d="M2250,371.75c-1036.89,0-1879.12,842.06-1877.8,1878,0.26,207.26,33.31,406.63,95.34,593.12h561.88V1263L2250,2483.57,3470.52,1263v1579.9h562c62.12-186.48,95-385.85,95.37-593.12C4129.66,1212.76,3287,372,2250,372Z" transform="translate(-371.96 -371.75)" style="fill:#f26822"/><path id="_149931160" data-name=" 149931160" d="M1969.3,2764.17l-532.67-532.7v994.14H1029.38l-384.29.07c329.63,540.8,925.35,902.56,1604.91,902.56S3525.31,3766.4,3855,3225.6H3063.25V2231.47l-532.7,532.7-280.61,280.61-280.62-280.61h0Z" transform="translate(-371.96 -371.75)" style="fill:#4d4d4d"/></svg>

After

Width:  |  Height:  |  Size: 940 B

+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="svg898" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FF2AD4;}
.st1{fill:#FFFFFF;}
.st2{fill:#FFCC00;}
</style>
<path id="path6" class="st0" d="M61.5,19.5C54.6,3.3,35.8-4.3,19.5,2.5s-23.9,25.7-17,41.9s25.7,23.9,41.9,17
c7.7-3.2,13.8-9.3,17-17C64.8,36.5,64.8,27.5,61.5,19.5z"/>
<path class="st1" d="M7.4,26.3h8.2v15.6v6.4l4.6-4.4l11.9-11.2l11.8,11.2l4.6,4.4v-6.4l0-15.6h8.1c0.4,1.8,0.6,3.7,0.6,5.6
c0,13.9-11.3,25.3-25.2,25.3S6.7,45.8,6.7,31.9C6.7,30,6.9,28.1,7.4,26.3 M5.3,23.5C0.7,38.3,8.9,54,23.7,58.6s30.5-3.7,35-18.4
c0.8-2.7,1.3-5.5,1.3-8.3c0-2.8-0.4-5.6-1.3-8.4H45.8l-0.1,18.4L32.1,28.8L18.3,41.9V23.5H5.3z"/>
<path class="st2" d="M18.3,20.8H21v2.8v12l9.1-8.7l1.9-1.8l1.9,1.8l9.1,8.6v-12v-2.7h14.7C51.5,6.6,34.9,0.2,20.8,6.4
c-6.4,2.8-11.6,8-14.4,14.4H18.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+80
View File
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="164.43007mm"
height="142.10118mm"
viewBox="0 0 164.43007 142.10118"
version="1.1"
id="svg5"
xml:space="preserve"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="logo_w_text.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.64693624"
inkscape:cx="70.331506"
inkscape:cy="386.43685"
inkscape:window-width="2124"
inkscape:window-height="1236"
inkscape:window-x="5342"
inkscape:window-y="533"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><defs
id="defs2" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-20.986536,-70.520522)"><circle
style="fill:#000000;fill-opacity:1;stroke-width:1.80275"
id="path464-3"
cx="102.98067"
cy="138.87482"
r="43.459656" /><circle
style="fill:#fbeb0a;fill-opacity:1;stroke-width:1.64883"
id="path464"
cx="102.98067"
cy="138.87482"
r="39.748943" /><path
style="fill:#000000;stroke-width:0.0417246"
d="m 120.01899,137.4727 c -0.0945,-0.29259 -0.2164,-0.65403 -0.27088,-0.80319 -0.0545,-0.14917 -0.19792,-0.58102 -0.31877,-0.95967 -0.12086,-0.37865 -0.27093,-0.82928 -0.33351,-1.00139 -0.0626,-0.17211 -0.23747,-0.68846 -0.38866,-1.14743 -0.15119,-0.45897 -0.29479,-0.89082 -0.3191,-0.95966 -0.0243,-0.0689 -0.42743,-1.27827 -0.89581,-2.68762 -0.46838,-1.40934 -0.87592,-2.57787 -0.90564,-2.59672 -0.0626,-0.0397 -0.22419,-0.0447 -0.35045,-0.0108 -0.0786,0.0211 -0.0905,0.0636 -0.1133,0.40624 -0.0677,1.01625 -0.60455,3.35207 -0.98049,4.26614 -0.0465,0.11305 -0.13455,0.33698 -0.19567,0.49762 -0.0611,0.16065 -0.14237,0.36439 -0.18056,0.45278 -0.12503,0.28937 -0.72141,1.46505 -0.77567,1.52914 -0.0292,0.0344 -0.0638,0.0932 -0.077,0.13058 -0.0828,0.23492 -0.95323,1.34099 -1.38649,1.76186 -0.59149,0.57457 -1.54112,1.23781 -1.69761,1.18564 -0.0605,-0.0202 -0.34,-0.54095 -0.91305,-1.70116 -0.25503,-0.51635 -0.54936,-1.10779 -0.65407,-1.31433 -0.2116,-0.41736 -0.75251,-1.59963 -1.04039,-2.27399 -1.10804,-2.59566 -1.93449,-5.12296 -2.2854,-6.98887 -0.27842,-1.48039 -0.29453,-1.58262 -0.35511,-2.25313 -0.0249,-0.27538 -0.0636,-0.69784 -0.086,-0.9388 -0.0773,-0.83042 -0.0321,-2.58144 0.0888,-3.44228 0.064,-0.45545 0.14032,-0.89444 0.2258,-1.29843 l 0.0717,-0.33875 0.2768,0.0994 c 1.40822,0.50567 2.52323,1.6172 3.38422,3.37366 0.34701,0.70791 0.4934,1.07202 0.73814,1.83588 0.10294,0.32128 0.22151,0.68741 0.2635,0.81363 0.0922,0.27713 0.4122,1.52379 0.46592,1.81502 0.0211,0.11474 0.0546,0.24618 0.0744,0.29207 0.0335,0.0778 0.11267,0.43593 0.1384,0.62587 0.009,0.0674 0.0473,0.0855 0.19869,0.0943 0.10306,0.006 0.21112,-0.005 0.24014,-0.0237 0.029,-0.019 0.15573,-0.44523 0.28159,-0.94716 0.25536,-1.01838 0.53763,-1.97468 0.68961,-2.33626 0.0193,-0.0459 0.0813,-0.22427 0.13779,-0.39638 0.10254,-0.31235 0.21499,-0.60507 0.50491,-1.31432 0.19027,-0.46547 0.72955,-1.50338 0.94929,-1.827 0.90699,-1.33582 1.89503,-1.84911 2.81309,-1.46141 0.56161,0.23718 1.0452,0.62619 1.5287,1.22974 0.40354,0.50373 0.9589,1.55483 1.33022,2.51763 0.0443,0.11475 0.0961,0.24618 0.11529,0.29208 0.11971,0.28683 0.21289,0.5529 0.21289,0.6079 0,0.0351 0.017,0.0817 0.0379,0.1035 0.0208,0.0218 0.0569,0.10535 0.0802,0.18568 0.0233,0.0803 0.0925,0.27746 0.15369,0.4381 0.0612,0.16064 0.12746,0.33902 0.14718,0.39639 0.0197,0.0574 0.0851,0.23574 0.1452,0.39638 0.0601,0.16064 0.1473,0.40004 0.19368,0.53199 0.0464,0.13195 0.0975,0.23991 0.11373,0.23991 0.0263,0 0.16332,-0.44975 0.25411,-0.83449 0.19513,-0.82686 0.88265,-3.07602 1.24992,-4.08901 0.7226,-1.99306 1.54671,-3.45449 2.59206,-4.5966 0.57405,-0.62719 1.58121,-1.30472 2.23989,-1.5068 0.17324,-0.0531 0.30229,-0.11599 0.30251,-0.14731 4.2e-4,-0.03 0.0177,-0.0439 0.0387,-0.0309 0.0966,0.0597 0.32141,1.32134 0.44654,2.50557 0.0996,0.94246 0.0993,3.15009 -4.6e-4,4.15159 -0.13983,1.40308 -0.33341,2.6784 -0.56684,3.73435 -0.1224,0.55371 -0.45045,1.89447 -0.52578,2.14882 -0.0306,0.10326 -0.093,0.31919 -0.13869,0.47983 -0.14939,0.52507 -0.58236,1.90751 -0.77101,2.46175 -0.22786,0.66949 -1.08777,2.98214 -1.19461,3.21279 -0.0212,0.0459 -0.0745,0.17733 -0.11837,0.29207 -0.13031,0.34107 -0.38022,0.94611 -0.47394,1.14743 -0.50114,1.07653 -0.95016,1.92046 -1.18055,2.21885 -0.0459,0.0594 -0.11161,0.15105 -0.14603,0.20357 -0.20336,0.31029 -0.68405,0.83229 -1.07364,1.1659 -0.19363,0.16581 -0.68606,0.4799 -1.03346,0.65918 -0.18569,0.0958 -0.6564,0.25876 -0.74755,0.25876 -0.0385,0 -0.11808,-0.18366 -0.23058,-0.53199 z"
id="path2693" /><path
style="fill:#000000;stroke-width:0.0417246"
d="m 87.065159,138.61502 c -1.362193,-0.15528 -2.017187,-0.29489 -3.150208,-0.67145 -0.857456,-0.28498 -1.32444,-0.48763 -2.018161,-0.87579 -0.468539,-0.26217 -1.218073,-0.75071 -1.465844,-0.95543 -1.239235,-1.02392 -1.606164,-1.41976 -2.28127,-2.461 -0.332413,-0.5127 -0.855826,-1.49505 -1.098983,-2.00434 -0.48597,-1.01786 -2.260387,-3.28883 0.252915,-3.08358 1.064567,0.0869 0.790069,0.0665 1.643984,0.11631 1.169025,-0.041 2.079099,0.47579 3.256652,1.42374 0.63205,0.50882 0.777018,0.6279 1.022253,0.83974 0.137691,0.11895 0.363004,0.29902 0.500695,0.40016 0.137691,0.10114 0.259736,0.19462 0.27121,0.20774 0.01147,0.0131 0.12413,0.091 0.250348,0.17319 1.069322,0.69591 2.222996,1.08669 3.609178,1.22253 0.312633,0.0306 1.226151,-0.0407 1.668984,-0.13037 1.182633,-0.23943 2.096969,-0.82627 2.466782,-1.58326 0.15691,-0.32119 0.161715,-0.34412 0.156859,-0.74858 -0.01386,-1.15472 -0.627503,-1.9424 -2.026692,-2.6015 -0.716785,-0.33765 -1.352605,-0.52703 -2.808353,-0.83648 -1.060418,-0.22541 -1.204318,-0.25811 -1.857519,-0.42203 -1.732248,-0.43473 -2.892563,-1.03611 -3.880705,-2.01132 -1.123982,-1.10927 -1.75776,-2.53076 -1.823072,-4.08891 -0.04504,-1.0746 0.135353,-1.90695 0.631073,-2.9118 0.308714,-0.62578 0.672205,-1.1293 1.201924,-1.66495 1.000331,-1.01152 1.932007,-1.598 3.287409,-2.06935 1.980139,-0.68861 4.14237,-0.78864 6.175242,-0.28567 0.814303,0.20147 1.325837,0.38346 2.029618,0.72209 1.442947,0.69427 1.770424,1.06037 2.479495,2.24093 0.31689,0.5276 0.789865,1.23468 1.248716,2.53013 0.158242,0.44675 0.769587,0.87729 -0.23814,0.90412 -0.394823,0.0105 -0.871505,0.11809 -1.510723,0.11552 -0.646307,-0.003 -0.893613,0.1645 -1.785234,-0.24348 -2.601623,-1.19041 -3.225024,-1.36986 -4.581172,-1.31877 -0.741168,0.0279 -1.303652,0.15049 -1.83643,0.40016 -1.116747,0.52332 -1.53891,1.18206 -1.167581,1.82188 0.185348,0.31936 0.668601,0.69356 1.208552,0.93582 0.365349,0.16392 1.191748,0.42723 1.816322,0.57873 2.09589,0.50838 2.461512,0.61539 3.44228,1.00753 0.506742,0.20261 1.428738,0.67537 1.930047,0.98964 0.911031,0.57115 1.921841,1.47923 2.463002,2.2127 0.213745,0.2897 0.676491,1.05061 0.676491,1.11238 0,0.0103 0.05452,0.13733 0.121148,0.28236 0.715941,1.55831 0.80087,3.48618 0.22943,5.20806 -0.133998,0.40376 -0.554861,1.31854 -0.66018,1.43494 -0.02478,0.0274 -0.06383,0.08 -0.08678,0.11698 -0.367256,0.59123 -1.054236,1.36436 -1.543811,1.73742 -0.137691,0.10493 -0.259736,0.20167 -0.27121,0.215 -0.06236,0.0724 -0.6204,0.4392 -1.004052,0.6599 -1.256396,0.72279 -2.520835,1.11622 -4.252003,1.32299 -0.46963,0.0561 -2.29979,0.0801 -2.692483,0.0354 z"
id="path2691"
sodipodi:nodetypes="ssssssscsssssssssssssssssssssssssssssssssssssssssss" /><path
style="fill:#000000;stroke-width:0.0417246"
d="m 93.337178,169.65921 c -0.76646,-0.088 -1.50511,-0.24803 -2.312166,-0.50108 -0.748203,-0.2346 -0.863846,-0.28043 -1.923052,-0.76218 -0.529418,-0.24079 -1.57368,-0.82832 -1.877607,-1.05639 -0.05737,-0.0431 -0.282684,-0.20296 -0.500695,-0.35534 -0.218011,-0.15239 -0.405772,-0.28855 -0.417246,-0.30258 -0.01147,-0.014 -0.226627,-0.19373 -0.478119,-0.39933 -1.62797,-1.33092 -2.940886,-2.96468 -3.938462,-4.90094 -0.764824,-1.4845 -1.197619,-2.79186 -1.443434,-4.36022 -0.154371,-0.98493 -0.195184,-1.2107 -0.223083,-1.23408 -0.01625,-0.0137 -0.273646,-0.11439 -0.571977,-0.22393 -2.253126,-0.82732 -4.099378,-1.69381 -5.148308,-2.41622 -0.441374,-0.30398 -0.930166,-0.87019 -1.157932,-1.34135 -0.209542,-0.43346 -0.265448,-0.68312 -0.265227,-1.18442 3.76e-4,-0.85924 0.208275,-1.48514 0.652751,-1.96523 0.300709,-0.3248 0.624748,-0.48669 1.090073,-0.54459 0.674127,-0.0839 1.314707,0.0678 2.387754,0.56542 2.172633,1.00755 3.318277,1.4807 3.585237,1.4807 0.09936,0 0.119918,-0.0355 0.272166,-0.46941 0.185132,-0.52766 0.745508,-1.66447 1.023244,-2.07579 0.948991,-1.40545 2.057782,-2.39489 3.464274,-3.0914 0.875099,-0.43334 1.82355,-0.72712 2.941585,-0.91112 2.19197,-0.36076 4.370677,-0.0914 5.549372,0.68615 0.734998,0.48483 1.018948,1.08214 0.968904,2.03813 -0.03901,0.74524 -0.251871,1.29117 -0.705292,1.8089 -0.32672,0.37305 -0.538205,0.53403 -1.016169,0.77348 -0.682047,0.34169 -1.343228,0.53304 -2.418512,0.6999 -1.652239,0.2564 -3.038894,0.92409 -3.683524,1.77366 -0.34145,0.45001 -0.450453,0.64615 -0.382446,0.68818 0.10613,0.0656 2.812496,0.70531 3.857775,0.91188 2.367522,0.46789 5.199956,0.84323 7.885522,1.04497 1.515236,0.11382 2.906296,0.18024 2.944566,0.14058 0.0177,-0.0184 0.12537,-0.23995 0.23918,-0.49238 0.11381,-0.25244 0.34059,-0.75415 0.50395,-1.11493 0.16336,-0.36077 0.29702,-0.6658 0.29702,-0.67784 0,-0.012 0.21171,-0.44912 0.47047,-0.9713 0.96757,-1.95259 1.83302,-3.19165 3.15775,-4.52092 1.61576,-1.6213 3.28473,-2.59743 5.65551,-3.30772 1.83795,-0.55065 3.88891,-0.6959 5.88316,-0.41663 1.4763,0.20673 2.95533,0.74276 4.20608,1.52437 1.32952,0.83082 2.60006,2.13702 3.53002,3.6291 0.26177,0.41999 0.83052,1.53098 1.07005,2.09022 0.0663,0.15491 0.14385,0.28164 0.17223,0.28164 0.0284,0 0.23543,-0.0668 0.46012,-0.14854 1.0933,-0.39756 1.5125,-0.53306 2.70337,-0.8738 0.97705,-0.27957 1.60849,-0.39565 2.17624,-0.40008 0.55975,-0.004 0.80323,0.0804 1.06566,0.37083 0.22032,0.24388 0.26332,0.41051 0.26057,1.00987 -0.005,1.12244 -0.38777,2.17846 -1.12091,3.09358 -0.99457,1.24145 -2.4285,2.19232 -4.26512,2.82831 l -0.63183,0.21879 -0.0292,0.27806 c -0.15655,1.49271 -0.77176,3.49852 -1.51221,4.93035 -0.78144,1.5111 -1.5622,2.56765 -2.8058,3.79694 -1.07045,1.05813 -2.53858,2.07773 -3.91251,2.71721 -0.24687,0.11491 -0.47948,0.22524 -0.51692,0.24519 -0.20796,0.11086 -1.07752,0.44526 -1.67037,0.64235 -0.99801,0.33179 -1.24198,0.37908 -1.95419,0.37878 -0.59384,-4.2e-4 -0.61104,-0.003 -0.93208,-0.14058 -0.57697,-0.24755 -0.93556,-0.70721 -1.14261,-1.46465 -0.10966,-0.40119 -0.0984,-1.36526 0.0209,-1.80147 0.2779,-1.01523 0.88293,-1.89256 1.68055,-2.43693 0.17083,-0.11658 0.56407,-0.33061 0.87388,-0.4756 2.40279,-1.12459 3.60325,-1.9321 4.52963,-3.04692 0.3547,-0.42687 0.5135,-0.66697 0.87016,-1.31569 0.251,-0.45652 0.35239,-0.70666 0.30546,-0.75359 -0.0104,-0.0104 -0.2516,0.0372 -0.53595,0.10582 -1.11164,0.26828 -2.69812,0.56485 -4.68947,0.87664 -1.01966,0.15964 -2.47916,0.33106 -4.79833,0.56356 -1.01716,0.10197 -2.86083,0.23278 -3.72711,0.26445 l -0.80639,0.0295 -0.11708,0.29207 c -0.50443,1.25832 -1.44463,3.0481 -2.16993,4.13074 -0.9131,1.36294 -1.47667,2.03663 -2.40063,2.86968 -1.17483,1.05923 -2.311402,1.73636 -3.650919,2.17509 -1.230834,0.40313 -2.305805,0.56954 -3.63004,0.56196 -0.424548,-0.003 -0.997142,-0.0303 -1.272431,-0.0619 z m 1.564503,-6.61275 c 1.305332,-0.25767 2.306514,-0.83931 3.213691,-1.86701 0.24896,-0.28204 1.042219,-1.42974 1.042219,-1.50791 0,-0.0187 -0.154903,-0.0466 -0.344228,-0.0621 -2.69734,-0.22101 -5.826446,-0.6005 -7.437412,-0.90201 -2.216672,-0.41487 -2.779443,-0.53404 -4.040843,-0.85563 -0.40953,-0.10441 -0.75555,-0.17888 -0.768934,-0.1655 -0.07478,0.0748 0.516777,1.41235 0.888168,2.00822 0.585611,0.93958 1.451126,1.76358 2.461248,2.3432 0.902853,0.51807 2.31243,0.95625 3.383458,1.05179 0.45564,0.0406 1.292576,0.0182 1.602633,-0.043 z m 14.791389,-9.11392 c 2.29082,-0.15389 5.41631,-0.56373 7.63966,-1.0018 0.75055,-0.14789 3.44432,-0.7627 3.47467,-0.79304 0.0285,-0.0285 -0.24099,-0.51764 -0.44738,-0.81196 -0.27456,-0.39156 -0.78801,-0.87828 -1.2532,-1.18796 -0.58018,-0.38625 -1.0986,-0.60205 -1.92419,-0.80097 -0.37291,-0.0899 -0.56847,-0.10873 -1.31432,-0.12688 -0.75931,-0.0185 -0.94302,-0.01 -1.37691,0.0645 -0.87255,0.14962 -1.42508,0.3315 -2.14853,0.70725 -1.06181,0.55149 -1.8515,1.26563 -2.54758,2.30383 -0.27136,0.40473 -0.79556,1.39751 -0.85601,1.62118 -0.0258,0.0956 -0.0209,0.0977 0.17584,0.0747 0.11134,-0.013 0.37141,-0.035 0.57795,-0.0489 z"
id="path302" /><text
xml:space="preserve"
style="font-size:32.4556px;line-height:31px;font-family:'Baekmuk Headline';-inkscape-font-specification:'Baekmuk Headline';letter-spacing:-1.10596px;fill:#000000;stroke-width:3.166"
x="18.909378"
y="210.4949"
id="text238"><tspan
sodipodi:role="line"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:32.4556px;font-family:'Syne Mono';-inkscape-font-specification:'Syne Mono Bold Italic';stroke-width:3.166"
x="18.909378"
y="210.4949"
id="tspan244">smirk.ca$h</tspan></text><text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:25.4px;font-family:'Syne Mono';-inkscape-font-specification:'Syne Mono Bold Italic';fill:#000000;stroke-width:3.166"
x="19.559481"
y="88.300522"
id="text408"><tspan
sodipodi:role="line"
id="tspan406"
style="font-size:25.4px;stroke-width:3.166"
x="19.559481"
y="88.300522">smirk wallet</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 14 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+69
View File
@@ -0,0 +1,69 @@
@import "tailwindcss";
:root {
--background: #0a0a0a;
--foreground: #ededed;
--smirk-yellow: #fbeb0a;
--smirk-yellow-dark: #d4c708;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-smirk: var(--smirk-yellow);
--font-mono: var(--font-geist-mono);
}
body {
background: var(--background);
color: var(--foreground);
font-family: var(--font-mono), monospace;
}
/* Logo animation */
@keyframes pulse-glow {
0%, 100% {
filter: drop-shadow(0 0 8px rgba(251, 235, 10, 0.4));
}
50% {
filter: drop-shadow(0 0 20px rgba(251, 235, 10, 0.8));
}
}
.logo-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
/* Coin button styles */
.coin-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1rem;
border-radius: 0.75rem;
border: 2px solid #3f3f46;
transition: all 0.2s;
cursor: pointer;
}
.coin-btn:hover {
border-color: #fbeb0a;
background: rgba(39, 39, 42, 0.5);
}
.coin-btn.selected {
border-color: #fbeb0a;
background: rgba(39, 39, 42, 0.5);
}
.coin-btn img {
width: 3rem;
height: 3rem;
}
.coin-btn span {
font-size: 0.875rem;
font-weight: 500;
text-transform: uppercase;
}
+27
View File
@@ -0,0 +1,27 @@
import type { Metadata } from "next";
import { Geist_Mono } from "next/font/google";
import "./globals.css";
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Smirk Wallet",
description: "Sign in with your Smirk wallet",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistMono.variable} font-mono antialiased`}>
{children}
</body>
</html>
);
}
+234
View File
@@ -0,0 +1,234 @@
'use client';
import { useState, useEffect } from 'react';
import Image from 'next/image';
import { getChallenge, verifySignature, type AuthResponse } from '@/lib/api';
type Asset = 'btc' | 'ltc' | 'xmr' | 'wow' | 'grin';
const COINS: { id: Asset; name: string; icon: string }[] = [
{ id: 'btc', name: 'Bitcoin', icon: '/coins/bitcoin.svg' },
{ id: 'ltc', name: 'Litecoin', icon: '/coins/litecoin.svg' },
{ id: 'xmr', name: 'Monero', icon: '/coins/monero.svg' },
{ id: 'wow', name: 'Wownero', icon: '/coins/wownero.svg' },
{ id: 'grin', name: 'Grin', icon: '/coins/grin.svg' },
];
type Step = 'initial' | 'choose-coin' | 'signing' | 'logged-in';
export default function Home() {
const [step, setStep] = useState<Step>('initial');
const [selectedCoin, setSelectedCoin] = useState<Asset | null>(null);
const [error, setError] = useState<string | null>(null);
const [user, setUser] = useState<AuthResponse['user'] | null>(null);
const [hasExtension, setHasExtension] = useState<boolean | null>(null);
// Check for extension on mount
useEffect(() => {
const check = () => setHasExtension(!!window.smirk);
// Check immediately and after a delay (extension might inject late)
check();
const timeout = setTimeout(check, 500);
return () => clearTimeout(timeout);
}, []);
const handleConnect = async () => {
if (!window.smirk) {
setError('Smirk extension not found');
return;
}
try {
setError(null);
// Connect to extension (get public keys)
await window.smirk.connect();
setStep('choose-coin');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to connect');
}
};
const handleCoinSelect = async (coin: Asset) => {
if (!window.smirk) return;
setSelectedCoin(coin);
setStep('signing');
setError(null);
try {
// Get challenge from backend
const challenge = await getChallenge(window.location.origin);
// Sign with the selected coin
const signatures = await window.smirk.signMessage(challenge.challenge);
const sig = signatures[coin];
// Verify with backend
const auth = await verifySignature(challenge.challenge_id, {
asset: coin,
signature: sig.signature,
public_key: sig.public_key,
});
// Store token and show logged in
localStorage.setItem('smirk_token', auth.access_token);
localStorage.setItem('smirk_refresh', auth.refresh_token);
setUser(auth.user);
setStep('logged-in');
} catch (err) {
setError(err instanceof Error ? err.message : 'Authentication failed');
setStep('choose-coin');
}
};
const handleLogout = () => {
localStorage.removeItem('smirk_token');
localStorage.removeItem('smirk_refresh');
setUser(null);
setSelectedCoin(null);
setStep('initial');
};
// Check for existing session on mount
useEffect(() => {
const token = localStorage.getItem('smirk_token');
if (token) {
// TODO: Validate token with /auth/me
// For now, just clear it and require re-auth
}
}, []);
return (
<div className="min-h-screen flex flex-col items-center justify-center p-8">
{/* Logo */}
<div className="mb-12 logo-glow">
<Image
src="/logo_w_text.svg"
alt="Smirk Wallet"
width={200}
height={180}
priority
/>
</div>
{/* Initial state - Connect button */}
{step === 'initial' && (
<div className="flex flex-col items-center gap-6">
{hasExtension === false && (
<p className="text-zinc-400 text-sm mb-4">
Install the{' '}
<a
href="https://chrome.google.com/webstore/detail/smirk-wallet"
target="_blank"
rel="noopener noreferrer"
className="text-[#fbeb0a] hover:underline"
>
Smirk extension
</a>{' '}
to continue
</p>
)}
<button
onClick={handleConnect}
disabled={hasExtension === false}
className="px-8 py-4 bg-[#fbeb0a] text-black font-bold rounded-xl
hover:bg-[#d4c708] transition-colors disabled:opacity-50
disabled:cursor-not-allowed text-lg"
>
Connect with Smirk
</button>
</div>
)}
{/* Choose coin step */}
{step === 'choose-coin' && (
<div className="flex flex-col items-center gap-6">
<h2 className="text-xl font-semibold mb-2">Choose your coin to sign in</h2>
<p className="text-zinc-400 text-sm mb-4">
Pick your favorite cryptocurrency to authenticate
</p>
<div className="grid grid-cols-5 gap-4">
{COINS.map((coin) => (
<button
key={coin.id}
onClick={() => handleCoinSelect(coin.id)}
className={`coin-btn ${selectedCoin === coin.id ? 'selected' : ''}`}
>
<Image src={coin.icon} alt={coin.name} width={48} height={48} />
<span>{coin.id}</span>
</button>
))}
</div>
</div>
)}
{/* Signing step */}
{step === 'signing' && (
<div className="flex flex-col items-center gap-4">
<div className="animate-spin w-8 h-8 border-2 border-[#fbeb0a] border-t-transparent rounded-full" />
<p className="text-zinc-400">
Signing with {selectedCoin?.toUpperCase()}...
</p>
<p className="text-zinc-500 text-sm">Check your extension</p>
</div>
)}
{/* Logged in state */}
{step === 'logged-in' && user && (
<div className="flex flex-col items-center gap-6 text-center">
<div className="w-16 h-16 bg-[#fbeb0a] rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-black"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<div>
<h2 className="text-xl font-semibold mb-2">Logged in!</h2>
<p className="text-zinc-400 text-sm">
Signed in with {selectedCoin?.toUpperCase()}
</p>
</div>
<div className="bg-zinc-900 rounded-xl p-6 w-full max-w-md">
<p className="text-zinc-500 text-xs mb-1">User ID</p>
<p className="font-mono text-sm break-all">{user.id}</p>
{user.telegram_username && (
<>
<p className="text-zinc-500 text-xs mt-4 mb-1">Telegram</p>
<p className="text-sm">@{user.telegram_username}</p>
</>
)}
</div>
<button
onClick={handleLogout}
className="px-6 py-2 border border-zinc-700 rounded-lg
hover:border-zinc-500 transition-colors text-sm"
>
Disconnect
</button>
</div>
)}
{/* Error display */}
{error && (
<div className="mt-6 p-4 bg-red-900/20 border border-red-500/50 rounded-lg">
<p className="text-red-400 text-sm">{error}</p>
</div>
)}
</div>
);
}
+75
View File
@@ -0,0 +1,75 @@
// API client for Smirk backend
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
export interface ChallengeResponse {
challenge: string;
challenge_id: string;
expires_at: string;
}
export interface AuthResponse {
access_token: string;
refresh_token: string;
expires_in: number;
user: {
id: string;
telegram_id: number | null;
telegram_username: string | null;
};
}
export interface MeResponse {
id: string;
username: string | null;
telegram_id: number | null;
telegram_username: string | null;
}
export async function getChallenge(origin: string): Promise<ChallengeResponse> {
const res = await fetch(`${API_URL}/api/v1/auth/website/challenge`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ origin }),
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: 'Request failed' }));
throw new Error(error.message || 'Failed to get challenge');
}
return res.json();
}
export async function verifySignature(
challengeId: string,
signature: { asset: string; signature: string; public_key: string }
): Promise<AuthResponse> {
const res = await fetch(`${API_URL}/api/v1/auth/website/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
challenge_id: challengeId,
signature,
}),
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: 'Request failed' }));
throw new Error(error.message || 'Failed to verify signature');
}
return res.json();
}
export async function getMe(token: string): Promise<MeResponse> {
const res = await fetch(`${API_URL}/api/v1/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
throw new Error('Failed to get user info');
}
return res.json();
}
+35
View File
@@ -0,0 +1,35 @@
// TypeScript types for the Smirk extension's injected window.smirk API
interface SmirkPublicKeys {
btc: string;
ltc: string;
xmr: string;
wow: string;
grin: string;
}
interface SmirkSignature {
asset: string;
signature: string;
public_key: string;
}
interface SmirkSignatures {
btc: SmirkSignature;
ltc: SmirkSignature;
xmr: SmirkSignature;
wow: SmirkSignature;
grin: SmirkSignature;
}
interface SmirkAPI {
connect(): Promise<SmirkPublicKeys>;
disconnect(): Promise<void>;
isConnected(): Promise<boolean>;
getPublicKeys(): Promise<SmirkPublicKeys | null>;
signMessage(message: string): Promise<SmirkSignatures>;
}
interface Window {
smirk?: SmirkAPI;
}
+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}