Skip to content

Commit b7fdda4

Browse files
authored
Cloudflare deploy of Mainnet (#10)
* init cf deploy and better mobile support * add more mobile support * improve a11y * finish up cf deploy
1 parent 79a908c commit b7fdda4

29 files changed

+603
-101
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ yarn-debug.log*
66
yarn-error.log*
77
pnpm-debug.log*
88
.env
9+
.env.testnet
910

1011
node_modules
1112
dist
@@ -29,4 +30,7 @@ workers
2930
tsconfig.app.tsbuildinfo
3031
tsconfig.node.tsbuildinfo
3132

32-
.windsurfrules
33+
.windsurfrules
34+
35+
.wrangler/
36+
.npmrc

bun.lock

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"hono": "^4.7.6",
2424
"http-proxy": "^1.18.1",
2525
"json-bigint": "^1.0.0",
26+
"lru-cache": "^11.1.0",
2627
"react": "^19.0.0",
2728
"react-dom": "^19.0.0",
2829
"react-router": "^7.4.0",
@@ -727,7 +728,7 @@
727728

728729
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
729730

730-
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
731+
"lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="],
731732

732733
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
733734

@@ -1007,6 +1008,8 @@
10071008

10081009
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
10091010

1011+
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
1012+
10101013
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
10111014

10121015
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width" />
56
<link rel="icon" type="image/svg+xml" href="/fine_icon.svg" />
67
<title>fine-tx</title>
78
</head>

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "fine-tx",
33
"private": true,
4-
"version": "0.0.0",
4+
"version": "0.6.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
@@ -32,6 +32,7 @@
3232
"hono": "^4.7.6",
3333
"http-proxy": "^1.18.1",
3434
"json-bigint": "^1.0.0",
35+
"lru-cache": "^11.1.0",
3536
"react": "^19.0.0",
3637
"react-dom": "^19.0.0",
3738
"react-router": "^7.4.0",

proxy-cf.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Cloudflare Worker proxy for fine-tx
2+
3+
import { Hono } from 'hono';
4+
import { proxy } from 'hono/proxy';
5+
import { logger } from 'hono/logger';
6+
import convict from 'convict';
7+
import { LRUCache } from 'lru-cache';
8+
9+
const config = convict({
10+
port: {
11+
doc: 'The port to bind the proxy server to',
12+
format: 'port',
13+
default: 5173,
14+
env: 'PORT',
15+
},
16+
betterfrostUrl: {
17+
doc: 'URL for the Betterfrost API',
18+
format: String,
19+
default: 'http://0.0.0.0:3001',
20+
env: 'VITE_BETTERFROST_URL',
21+
},
22+
blockfrostProjectId: {
23+
doc: 'Project ID for Blockfrost API (optional, if not using Betterfrost)',
24+
format: String,
25+
default: '',
26+
env: 'VITE_BLOCKFROST_PROJECT_ID',
27+
},
28+
cacheSize: {
29+
doc: 'Size of the cache in bytes',
30+
format: Number,
31+
default: 0,
32+
env: 'PROXY_CACHE_SIZE',
33+
},
34+
registryUrl: {
35+
doc: 'URL for the token registry service',
36+
format: String,
37+
default: 'https://public.liqwid.finance/v4',
38+
env: 'VITE_REGISTRY_URL',
39+
},
40+
});
41+
42+
config.validate({ allowed: 'strict' });
43+
44+
const app = new Hono();
45+
46+
app.use(logger());
47+
48+
const cache = new LRUCache<string, Blob>({
49+
max: config.get('cacheSize'),
50+
ttl: 60 * 30 * 1000,
51+
});
52+
53+
function key(url: string, options: RequestInit) {
54+
return `${url}-${options.body}`;
55+
}
56+
57+
async function betterfrostProxy(
58+
url: string,
59+
options: RequestInit,
60+
): Promise<Response> {
61+
const k = key(url, options);
62+
const cachedResponse = cache.get(k);
63+
if (cachedResponse) {
64+
console.log(`--> CACHE HIT ${k}`);
65+
return new Response(cachedResponse);
66+
}
67+
68+
console.log(`--> CACHE MISS ${k}`);
69+
70+
const response = await proxy(url, {
71+
...options,
72+
});
73+
74+
const blob = await response.blob();
75+
76+
console.log(`--> CACHE SET ${k} (${blob.size} bytes)`);
77+
78+
const res = new Response(blob);
79+
80+
cache.set(k, blob);
81+
82+
return res;
83+
}
84+
85+
// Proxy routes
86+
app.all('/betterfrost/*', async (c) => {
87+
const targetUrl = c.req.path.replace('/betterfrost', '');
88+
89+
const extraHeaders = config.get('blockfrostProjectId')
90+
? ({
91+
project_id: config.get('blockfrostProjectId') ?? '',
92+
} as Record<string, string>)
93+
: {};
94+
95+
const response = await betterfrostProxy(
96+
`${config.get('betterfrostUrl')}${targetUrl}`,
97+
{
98+
method: c.req.method,
99+
body: c.req.raw.body,
100+
headers: {
101+
...c.req.raw.headers,
102+
'User-Agent': c.req.raw.headers['User-Agent'],
103+
...extraHeaders,
104+
},
105+
},
106+
);
107+
108+
return response;
109+
});
110+
111+
app.all('/ogmios/*', async (c) => {
112+
return c.json({ error: 'OGMIOS_URL not set. Not available!' });
113+
});
114+
115+
app.get('/registry-proxy/:path', async (c) => {
116+
return proxy(`${config.get('registryUrl')}/${c.req.param('path')}`);
117+
});
118+
119+
console.log(`
120+
Proxy started on http://0.0.0.0:${config.get('port')} 🚀
121+
122+
Requests for /betterfrost => ${config.get('betterfrostUrl')}
123+
Requests for /ogmios => Not available
124+
Requests for /registry-proxy => ${config.get('registryUrl')}
125+
`);
126+
127+
// Start the server
128+
export default {
129+
port: config.get('port'),
130+
fetch: app.fetch,
131+
};

src/App.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@ import { BlueprintPage } from './pages/blueprint';
1717
import { DatumProvider } from './context/Providers';
1818
import { HomePage } from './pages/home';
1919

20-
const queryClient = new QueryClient();
20+
const queryClient = new QueryClient({
21+
defaultOptions: {
22+
queries: {
23+
// @ts-expect-error Suspense is not a valid option, according to the types, but
24+
// it is valid in practice.
25+
suspense: true,
26+
},
27+
},
28+
});
2129

2230
const persister = createSyncStoragePersister({
2331
storage: window.localStorage,

src/components/ActionButtons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function ActionButton({
4040
return (
4141
<button
4242
onClick={onClick}
43-
className={`inline-flex items-center justify-center gap-1.5 p-1 text-xs focus:outline-none transition-colors duration-200 ${className}`}
43+
className={`inline-flex items-center justify-center gap-1.5 p-1 focus:outline-none transition-colors duration-200 ${className}`}
4444
title={title}
4545
aria-label={ariaLabel || title}
4646
>

src/components/AnimatedSearchInput.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ export function AnimatedSearchInput({
4545
type={type}
4646
id={id}
4747
name={name}
48+
role="searchbox"
4849
value={value}
4950
onChange={onChange}
5051
onFocus={() => setIsInputFocused(true)}
5152
onBlur={() => setIsInputFocused(false)}
5253
placeholder={placeholder}
54+
aria-description="Search"
5355
title={title}
5456
className={`h-10 w-full
5557
${
@@ -72,6 +74,8 @@ export function AnimatedSearchInput({
7274
? 'opacity-100 max-w-[80px] ml-0'
7375
: 'opacity-0 max-w-0 ml-[-2px] overflow-hidden'
7476
}`}
77+
onFocus={() => setIsInputFocused(true)}
78+
onBlur={() => setIsInputFocused(false)}
7579
>
7680
{buttonText}
7781
</button>

src/components/Button.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
export type IconButtonProps = {
22
onClick: () => void;
3-
disabled: boolean;
3+
disabled?: boolean;
44
ariaLabel: string;
55
children: React.ReactNode;
66
};
77

88
export const IconButton = ({
99
onClick,
10-
disabled,
10+
disabled = false,
1111
ariaLabel,
1212
children,
1313
}: IconButtonProps) => {

src/components/Datum.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useLiveQuery } from 'dexie-react-hooks';
22
import { db, renderToJSON, renderToYaml } from '../cbor/plutus_json';
3-
import { Fragment, useContext, useMemo, useState } from 'react';
3+
import { Fragment, useContext, useId, useMemo, useState } from 'react';
44
import * as cbor2 from 'cbor2';
55
import { parseRawDatum } from '../cbor/raw_datum';
66
import { createParsingContext } from '../cbor/plutus_json';
@@ -38,6 +38,7 @@ function ExternalLinkButton({
3838
}
3939

4040
export const ViewDatum = ({ datum }: { datum: string }) => {
41+
const datumSelectId = useId();
4142
const blueprints = useLiveQuery(async () => {
4243
return db.plutusJson.toArray();
4344
}, []);
@@ -136,17 +137,35 @@ export const ViewDatum = ({ datum }: { datum: string }) => {
136137
<div className="flex flex-col">
137138
{/* Toolbar with buttons always visible at the top */}
138139
<div className="flex justify-between items-center p-1 border-b border-gray-800">
140+
<label htmlFor={datumSelectId} className="sr-only">
141+
View mode selector
142+
</label>
139143
<select
144+
id={datumSelectId}
145+
aria-label="View mode selector"
146+
aria-description="Select the format to view the datum in"
140147
value={datumContext?.viewMode || 'enriched_yaml'}
141148
onChange={handleViewModeChange}
142149
className="text-xs text-white border-r border-gray-700 px-2 py-1 focus:outline-none bg-transparent"
143150
>
144-
<option value="hex">Hex</option>
145-
<option value="json">JSON</option>
146-
<option value="diag">Diagnostic</option>
147-
<option value="raw_datum">Raw Datum</option>
148-
<option value="enriched_datum">Enriched Datum</option>
149-
<option value="enriched_yaml">Enriched Datum (YAML)</option>
151+
<option label="Hex" value="hex">
152+
Hex
153+
</option>
154+
<option label="JSON" value="json">
155+
JSON
156+
</option>
157+
<option label="Diagnostic" value="diag">
158+
Diagnostic
159+
</option>
160+
<option label="Raw Datum" value="raw_datum">
161+
Raw Datum
162+
</option>
163+
<option label="Enriched Datum" value="enriched_datum">
164+
Enriched Datum
165+
</option>
166+
<option label="Enriched Datum (YAML)" value="enriched_yaml">
167+
Enriched Datum (YAML)
168+
</option>
150169
</select>
151170
<div className="flex gap-1">
152171
<button

0 commit comments

Comments
 (0)