-
-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathsealed-drop.js
More file actions
133 lines (112 loc) · 5.78 KB
/
Copy pathsealed-drop.js
File metadata and controls
133 lines (112 loc) · 5.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/**
* GET /api/og/sealed-drop?id=<dropId>
*
* Dynamic Open Graph share card for a sealed wallet drop — the image that
* previews wherever a /drop/:id link is posted (X, Telegram, Discord, iMessage,
* Farcaster). Mirrors api/og/three-token-badge.js: a 1200×630 SVG returned with
* image/svg+xml + a CDN cache header, no satori/canvas deps in the bundle.
*
* The card shows ONLY public, secret-free fields from the drop's public
* projection: the (vanity) address, the funded amount + asset, the theme, who
* it's from, and the sealed/E2E badge. It NEVER renders the sealed envelope, the
* claim token, or any secret. A missing/unknown id still renders a valid,
* branded card (a generic "sealed gift") rather than 5xx — crawlers must always
* get an image.
*/
import { cors, wrap } from '../_lib/http.js';
import { getDrop } from '../_lib/sealed-drop-store.js';
const CACHE = 'public, max-age=60, s-maxage=600, stale-while-revalidate=120';
// three.ws brand gradient.
const C1 = '#8b5cf6';
const C2 = '#06b6d4';
// Per-theme accent + glyph, coin-agnostic.
const THEME_STYLE = {
default: { accent: '#8b5cf6', glyph: '🎁', label: 'A sealed gift' },
birthday: { accent: '#ff5db1', glyph: '🎂', label: 'Happy birthday' },
congrats: { accent: '#ffb020', glyph: '🎉', label: 'Congratulations' },
thanks: { accent: '#4ade80', glyph: '🙏', label: 'Thank you' },
welcome: { accent: '#38bdf8', glyph: '👋', label: 'Welcome' },
tip: { accent: '#fbbf24', glyph: '⚡', label: 'A tip for you' },
};
function x(s) {
return String(s ?? '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function shortAddr(addr) {
const s = String(addr || '');
return s.length > 16 ? `${s.slice(0, 8)}…${s.slice(-6)}` : s;
}
function assetLabel(asset) {
if (asset === 'THREE') return '$THREE';
return String(asset || '');
}
function buildCard({ status, amount, asset, address, theme, senderLabel, vanity }) {
const t = THEME_STYLE[theme] || THEME_STYLE.default;
const claimed = status === 'claimed';
const reclaimed = status === 'reclaimed';
const statusLabel = claimed ? 'CLAIMED' : reclaimed ? 'RECLAIMED' : 'SEALED · UNCLAIMED';
const statusColor = claimed ? '#4ade80' : reclaimed ? '#888' : t.accent;
const amountStr = amount != null ? `${amount} ${assetLabel(asset)}` : 'Sealed wallet';
const addr = shortAddr(address);
const from = senderLabel ? `from ${senderLabel}` : 'A pre-funded wallet, sealed end-to-end';
const vanityNote = vanity?.prefix || vanity?.suffix
? `vanity ${vanity.prefix ? vanity.prefix + '…' : ''}${vanity.suffix ? '…' + vanity.suffix : ''}`
: 'end-to-end encrypted';
return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-label="${x(t.label)} — a sealed wallet drop on three.ws">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#0a0a0f"/><stop offset="1" stop-color="#05050a"/>
</linearGradient>
<linearGradient id="brand" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="${C1}"/><stop offset="1" stop-color="${C2}"/>
</linearGradient>
<radialGradient id="glow" cx="0.8" cy="0.2" r="0.9">
<stop offset="0" stop-color="${t.accent}" stop-opacity="0.22"/>
<stop offset="1" stop-color="${t.accent}" stop-opacity="0"/>
</radialGradient>
</defs>
<rect width="1200" height="630" fill="url(#bg)"/>
<rect width="1200" height="630" fill="url(#glow)"/>
<rect x="0" y="0" width="1200" height="6" fill="url(#brand)"/>
<text x="64" y="86" font-family="'Space Grotesk',Inter,system-ui,sans-serif" font-size="30" font-weight="700" fill="#fff">three.ws</text>
<text x="64" y="86" font-family="Inter,system-ui,sans-serif" font-size="20" font-weight="600" fill="#777" dx="150">SEALED DROP</text>
<text x="1136" y="86" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="22" font-weight="800" letter-spacing="1" fill="${statusColor}">${x(statusLabel)}</text>
<text x="64" y="240" font-family="Inter,system-ui,sans-serif" font-size="44">${t.glyph}</text>
<text x="130" y="240" font-family="'Space Grotesk',Inter,system-ui,sans-serif" font-size="40" font-weight="800" fill="#fff">${x(t.label)}</text>
<text x="64" y="360" font-family="'Space Grotesk',Inter,system-ui,sans-serif" font-size="92" font-weight="800" fill="url(#brand)">${x(amountStr)}</text>
<text x="64" y="430" font-family="'Space Grotesk',monospace" font-size="30" font-weight="700" fill="#cbd5e1">${x(addr)}</text>
<text x="64" y="470" font-family="Inter,system-ui,sans-serif" font-size="22" fill="#94a3b8">${x(from)}</text>
<g>
<rect x="64" y="520" width="430" height="54" rx="27" fill="rgba(139,92,246,0.12)" stroke="${t.accent}" stroke-opacity="0.5"/>
<text x="92" y="555" font-family="Inter,system-ui,sans-serif" font-size="22" font-weight="700" fill="${t.accent}">🔒 ${x(vanityNote)}</text>
</g>
<text x="1136" y="600" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="22" font-weight="600" fill="#64748b">three.ws/drop</text>
</svg>`;
}
export default wrap(async (req, res) => {
if (cors(req, res, { methods: 'GET,OPTIONS', origins: '*' })) return;
const url = new URL(req.url, `http://${req.headers.host || 'three.ws'}`);
const id = (url.searchParams.get('id') || '').trim();
let drop = null;
if (/^[0-9a-f]{24}$/.test(id)) {
try {
drop = await getDrop(id);
} catch {
drop = null;
}
}
const svg = buildCard({
status: drop?.status || 'funded',
amount: drop?.amount ?? null,
asset: drop?.asset || null,
address: drop?.address || null,
theme: drop?.theme || 'default',
senderLabel: drop?.senderLabel || null,
vanity: drop?.vanity || null,
});
res.statusCode = 200;
res.setHeader('content-type', 'image/svg+xml; charset=utf-8');
res.setHeader('cache-control', CACHE);
res.end(svg);
});