Skip to content
Merged
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ There are multiple different apps and elements in this repo. Please read careful
- Indentation: tabs, width 4 (`.editorconfig`).
- Prettier: single quotes, semicolons, 120 col width.
- Asset files per token: `logo.svg`, `logo-32.png`, `logo-128.png`.
- Addresses: lowercase for EVM chains; case‑sensitive for Solana (`1151111081099710`).
- Addresses: lowercase for EVM chains.
- Directory names: numeric `chainId` (or `btcm`).

## Testing Guidelines
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ Environment Variables: None
- 81457: Blast
- 98866: Plume
- 1380012617: Rari
- 1151111081099710: Solana
- btcm: Bitcoin

## Contributing
Expand All @@ -89,7 +88,7 @@ $ rsvg-convert -h 32 logo.svg > logo-32.png && rsvg-convert -h 128 logo.svg > lo
```

Once ready, create a new directory with the chain ID, or use the existing one,
and create a new directory for the token address (in lower case for EVM chains, or case sensitive for Solana) you are adding.
and create a new directory for the token address in lowercase.

Fill-in the details when creating the pull-request, and we'll merge it shortly!

Expand Down
2 changes: 1 addition & 1 deletion app/image-tools/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
- Indentation: tabs, width 4 (`.editorconfig`).
- Prettier: single quotes, semicolons, 120‑column width.
- Token asset files: exactly `logo.svg`, `logo-32.png`, `logo-128.png`.
- Addresses: EVM lowercase; Solana case‑sensitive (e.g., `1151111081099710`).
- Addresses: EVM lowercase.
- Directories: numeric `chainId`; addresses under the chain folder.

## Testing Guidelines
Expand Down
45 changes: 23 additions & 22 deletions app/image-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,35 @@ A lightweight SPA + Vercel Functions app for uploading token/chain assets and op

## Environment Variables (Dev/Prod)

- Client (exposed to browser)
- `VITE_GITHUB_CLIENT_ID` — GitHub OAuth App client ID.
- `VITE_API_BASE_URL` — optional; default same-origin. Set only if the API lives on another origin.
- `VITE_RPC_URI_FOR_<chainId>` — optional RPC URLs used by `/api/erc20-name`.
- Server (Vercel Functions)
- `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` — same GitHub OAuth App used by the client.
- `APP_BASE_URL` — optional; default request origin. Only set if SPA and API are on different origins.
- `REPO_OWNER` (default `yearn`), `REPO_NAME` (default `tokenAssets`).
- `ALLOW_REPO_OVERRIDE` — set to `true` only if you intentionally want to target a non-yearn repo when deploying from a fork.
- GitHub OAuth App callback must match the current domain: `https://<domain>/api/auth/github/callback` (or `http://localhost:3000/...` for `vercel dev`).
- Client (exposed to browser)
- `VITE_GITHUB_CLIENT_ID` — GitHub OAuth App client ID.
- `VITE_API_BASE_URL` — optional; default same-origin. Set only if the API lives on another origin.
- `VITE_RPC_URI_FOR_<chainId>` — optional RPC URLs used by `/api/erc20-name`.
- Server (Vercel Functions)
- `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` — same GitHub OAuth App used by the client.
- `APP_BASE_URL` — optional; default request origin. Only set if SPA and API are on different origins. For the
public deployment, leave unset or set to `https://token-assets.yearn.fi`; do not point this at a Vercel alias.
- `REPO_OWNER` (default `yearn`), `REPO_NAME` (default `tokenAssets`).
- `ALLOW_REPO_OVERRIDE` — set to `true` only if you intentionally want to target a non-yearn repo when deploying from a fork.
- GitHub OAuth App callback must allow the current domain: `https://<domain>/api/auth/github/callback` (or `http://localhost:3000/...` for `vercel dev`).

## Commands

- `bun dev` — Vite dev server for the SPA (http://localhost:5173).
- `vercel dev` — Runs API routes and serves the SPA locally (recommended for full flow).
- `bun build` / `bun preview` — Build and preview the SPA.
- `bun typecheck` — TypeScript type checks (acts as lightweight lint).
- `bun lint` — Alias to type checks.
- `bun dev` — Vite dev server for the SPA (http://localhost:5173).
- `vercel dev` — Runs API routes and serves the SPA locally (recommended for full flow).
- `bun build` / `bun preview` — Build and preview the SPA.
- `bun typecheck` — TypeScript type checks (acts as lightweight lint).
- `bun lint` — Alias to type checks.

## App Flow (What Calls What)

1) Open the site — SPA loads; no API calls by default.
2) Sign in with GitHub — Browser goes to GitHub OAuth; upon approval GitHub redirects to `/api/auth/github/callback` (Edge). The function exchanges the code for a token and redirects to `/auth/github/success` where the token is stored.
3) Enter chain/address — Client may call `POST /api/erc20-name` (Edge) to resolve ERC‑20 name.
4) Drop SVG — Client generates PNG previews (32×32, 128×128) via Canvas.
5) Submit PR — Client posts multipart form to `POST /api/upload` (Edge) with `svg`, `png32`, and `png128`. The function validates sizes and opens a PR via GitHub API.
1. Open the site — SPA loads; no API calls by default.
2. Sign in with GitHub — Browser goes to GitHub OAuth; upon approval GitHub redirects to `/api/auth/github/callback` (Edge). The function exchanges the code for a token and redirects to `/auth/github/success` where the token is stored.
3. Enter chain/address — Client may call `POST /api/erc20-name` (Edge) to resolve ERC‑20 name.
4. Drop SVG — Client generates PNG previews (32×32, 128×128) via Canvas.
5. Submit PR — Client posts multipart form to `POST /api/upload` (Edge) with `svg`, `png32`, and `png128`. The function validates sizes and opens a PR via GitHub API.

## Notes

- PNGs are generated client‑side and validated on the server.
- Keep SVGs simple/optimized; ensure PNGs are exactly 32×32 and 128×128.
- PNGs are generated client‑side and validated on the server.
- Keep SVGs simple/optimized; ensure PNGs are exactly 32×32 and 128×128.
33 changes: 19 additions & 14 deletions app/image-tools/api/auth/github/callback.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,47 @@
export const config = { runtime: 'edge' };
export const config = {runtime: 'edge'};

export default async function (req: Request): Promise<Response> {
try {
const url = new URL(req.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state') || '';
const redirectUri = new URL('/api/auth/github/callback', url.origin).toString();
if (!code) {
return new Response(JSON.stringify({ error: 'Missing code' }), {
return new Response(JSON.stringify({error: 'Missing code'}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
headers: {'Content-Type': 'application/json'}
});
}

const clientId = process.env.GITHUB_CLIENT_ID || process.env.VITE_GITHUB_CLIENT_ID;
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
if (!clientId || !clientSecret) {
return new Response(JSON.stringify({ error: 'Missing GitHub OAuth env vars' }), {
return new Response(JSON.stringify({error: 'Missing GitHub OAuth env vars'}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
headers: {'Content-Type': 'application/json'}
});
}

const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code })
headers: {Accept: 'application/json', 'Content-Type': 'application/json'},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
code,
redirect_uri: redirectUri
})
});
if (!tokenRes.ok) {
const text = await tokenRes.text();
return new Response(text, { status: 502 });
return new Response(text, {status: 502});
}
const tokenJson = (await tokenRes.json()) as { access_token?: string };
const tokenJson = (await tokenRes.json()) as {access_token?: string};
const accessToken = tokenJson.access_token;
if (!accessToken) {
return new Response(JSON.stringify({ error: 'No access_token in response' }), {
return new Response(JSON.stringify({error: 'No access_token in response'}), {
status: 502,
headers: { 'Content-Type': 'application/json' }
headers: {'Content-Type': 'application/json'}
});
}

Expand All @@ -45,10 +51,9 @@ export default async function (req: Request): Promise<Response> {
redirect.searchParams.set('state', state);
return Response.redirect(redirect.toString(), 302);
} catch (e: any) {
return new Response(JSON.stringify({ error: e?.message || 'OAuth callback failed' }), {
return new Response(JSON.stringify({error: e?.message || 'OAuth callback failed'}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
headers: {'Content-Type': 'application/json'}
});
}
}

Loading
Loading