- TypeScript 68.5%
- JavaScript 29.1%
- CSS 1.9%
- Python 0.3%
- HTML 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Use the wallet kit account menu for copying the address and disconnecting. Retain admin session cleanup after disconnect and align browser coverage with the stock menu. Validation: frontend lint, typecheck, configuration tests and explicit release build; 36 backend unit tests, deliberate duplicate-key and permission guard failures, 60 native PocketBase integration tests, and six browser wallet/session cases. |
||
| .forgejo/workflows | ||
| .githooks | ||
| backend | ||
| frontend | ||
| shared | ||
| .gitignore | ||
| .ignore | ||
| AGENTS.md | ||
| config.example.json | ||
| LICENSE | ||
| README.md | ||
ICU Smart Contract GUI
A browser interface for EVM contracts: import an address and ABI, call functions, track transactions, and share contracts or profiles through wallet-signed links. Browser data stays in IndexedDB; the backend stores shared content and private administration state. Writes require the wallet to match the selected chain.
Build and run
Use Node.js 22 and stock PocketBase 0.40.4 with bundled JavaScript hooks and the Preact frontend.
(cd frontend && npm ci && npm run build:preview)
(cd backend && npm ci && npm run build -- --preview)
Serve backend/dist/pb_public, backend/dist/pb_hooks and
backend/dist/pb_migrations with PocketBase; keep its data directory outside
release artifacts. Frontend development uses npm run dev in frontend/, port
5173. Preview builds show -dev and cannot deploy to production.
Frontend installation applies an integrity-checked depth limit to braces 3.0.3;
builds reject missing or unexpected patch bytes. If installation skips lifecycle
scripts, run node frontend/scripts/harden-braces.mjs before building.
For a release, set VITE_RELEASE_VERSION=v<package version> when running
npm run build:release in frontend/, then run
npm run build -- --output dist-release in backend/. Both package versions must match.
Configuration
The backend generates /config.js from environment variables or string-valued
settings in pb_hooks/.environment/settings.json. Keep the file mode 0600 and
its directory mode 0700; upload as settings.pending for bootstrap activation.
Nonempty environment values take precedence. Never publish administrative keys.
| Variable | Purpose |
|---|---|
ICU_ENVIRONMENT |
production, staging or development. |
ICU_PUBLIC_ORIGINS |
App origins allowed for wallet administration. |
ALLOWED_ORIGINS |
Browser CORS allowlist. |
ICU_TRUSTED_PROXY_CIDRS |
Explicit peers trusted to forward visitor IPs. |
ICU_ADMIN_ADDRESSES |
Admin wallets; empty disables the admin API. |
ICU_ADMIN_SESSION_KEY |
Private hexadecimal HMAC key, at least 32 bytes; alternatively use mode-0600 admin-session.key in the data directory. |
ICU_WRITE_MAINTENANCE |
1 pauses mutations while reads remain available. |
ICU_TOKEN_ICON_SERVICE_URL |
Optional HTTPS icon-resolver origin; unset uses provider fallbacks. |
ICU_TOKEN_ICON_SERVICE_KEY |
Private icon-resolver consumer key. |
ICU_TOKEN_ICONS_ENABLED |
Set false to disable icon lookups. |
ICU_TOKEN_ICON_DEXSCREENER_RPM |
Server-wide fallback requests per minute; default 20. |
ICU_TOKEN_ICON_GECKOTERMINAL_RPM |
Server-wide fallback requests per minute; default 1. |
ICU_WC_PROJECT_ID |
Optional WalletConnect project ID. |
ICU_MIRROR_PUBLIC_URL |
Optional read-only mirror URL on r2.dev. |
ICU_MIRROR_SIGNER |
Public address used to verify mirror snapshots. |
ICU_MIRROR_ENDPOINT, ICU_MIRROR_BUCKET |
Mirror S3 endpoint and dedicated bucket. |
ICU_MIRROR_ACCESS_KEY_ID, ICU_MIRROR_SECRET_ACCESS_KEY |
Private bucket-scoped object read/write credentials. |
ICU_MIRROR_SIGNING_KEY |
Private 32-byte hexadecimal snapshot key; separate from wallet and admin keys. |
ICU_CLOUDFLARE_EDGE_SECRET |
Private hexadecimal credential, at least 32 bytes, matching the hostname-scoped X-ICU-Edge-Auth header rule; required for production publishing. |
Branding and the Chainlist endpoint are fixed in source. Static-only hosting can
provide config.example.json as /config.js; an empty
frontend.apiBase disables backend publishing without disabling local features.
Administration and permalinks
The optional /admin dashboard uses allowlisted wallet sign-in. It provides
sharing statistics, searchable records, payload inspection, confirmed deletion,
permanent bans, permalink management and persisted publishing settings. Admin
access requires a configured backend and an approved ICU_PUBLIC_ORIGINS entry.
Admin sessions use bearer tokens held only in browser memory. Reloading requires
a fresh wallet signature; no cookies or persistent login storage are used.
Frontend and API may use unrelated domains; public APIs use the
ALLOWED_ORIGINS allowlist.
Wallet sign-in does not use PocketBase user accounts. Migrations lock registration,
record access and authentication for the unused users collection; PocketBase
superuser administration remains separate.
Settings default to a 60-second signature timeout, 5 shares per wallet and 30 POST attempts per IP per minute, and daily new-link limits of 100 per wallet and 1000 per IP. Changes preserve usage counts. Existing explicit limits are imported once; their old environment variables no longer control the app.
IP, subnet, country and city bans block publishing. Owner bans also hide hosted records and release custom names; signed owner deletion remains available. Country/city data comes from authenticated Cloudflare location headers. Enable Add visitor location headers on a proxied hostname, preserve real IPv6 addresses, and verify the forwarding chain. Unknown locations are allowed; direct-origin publishing is rejected when edge authentication is configured.
Owners can claim available /c/name and /p/name permalinks by signing with
their wallets. Names use 1–64 lowercase letters, digits or hyphens; 13- and 22-character
ID-shaped names are reserved. Each record has one name, with separate contract
and profile namespaces. Opaque IDs survive alias changes. Unbanning restores
eligible names only if nobody else claimed them, even if that claimant later
released them. Admin transfers cancel the former record's automatic restoration.
Read-only outage fallback
An optional R2 mirror exports public contracts and profiles in the background. Configure all mirror variables together and allow the frontend origins in the bucket's GET/HEAD CORS policy. Production and staging use separate buckets and credentials. Only the public URL and verification address reach the frontend.
The exporter publishes signed snapshots before advancing their signed head. Failed uploads retain the previous head and retry; deletions and owner bans advance the next snapshot. Retired generations are removed after ten minutes. The mirror exposes known-reference reads without a public share inventory.
Network failures, timeouts and server errors can load the last verified copy. Authoritative client errors, including missing or blocked records and throttling, do not trigger fallback. Saved content shows its snapshot time and a retry action; backend mutations remain unavailable and are never replayed automatically. Sharing links use the frontend's current origin. Browser-local editing and exports remain available independently of the backend.
Public API
- Read:
GET /read/{contracts|profiles}/{id-or-name}. - Share:
POST /share/contractor/share/profile. - Delete/rotate: signed
POST /delete/{table}/{nid}or/regen/{table}/{nid}. - Permalinks:
GETor signedPOST /permalink/{table}/{nid}. - Icons:
POST /api/scgui/token-icon/batchwith{chainId, addresses}(max 30); optionalfailedImagesmaps requested addresses to failed automatic image URLs. Single-token reads useGET /api/scgui/token-icon/{chain}/{address}. Responses includeimageUrl,symbol,cached,retryAfter,source,state, andversion. The edit form’s Fetch Icon button selects a URL for saving. Entries with no saved image may display an automatically resolved icon; saved images always take priority. Provider fallback supports Ethereum and PulseChain. Cached metadata expires after 24 hours; temporary failures use a cooldown. - Health/release:
GET /api/healthand/api/scgui/release.
Icon GET, HEAD and batch requests share an IP request budget; respect retryAfter
when throttled.
Signed writes use Content-Type: application/json and contain exactly
{payload, signed}; sign the literal payload string. Duplicate keys, unknown
fields, stale signatures and URL mismatches are
rejected. Delete/rotate payloads use schema icu-sc-gui/share-mutation/v1 with
operation, nid, table, sharedByAddress, timestamp. Permalink payloads
use icu-sc-gui/permalink/v1, operation claim or remove, and add slug.
Exports use schemaVersion: 2 and full-signature favoriteFunctions. Legacy
imports remain supported. Public reads omit owner addresses; quotas and replay
records persist. Shared and administrative SQL tables are not public collections.
Deployment and verification
The frontend is hosted on Cloudflare Pages at scgui.pages.dev and sc.gui.icu;
the API stays on PocketHost at sc-api.gui.icu. Admin access requires the frontend
origin to be approved in ICU_PUBLIC_ORIGINS. Browser-local data is separate per origin.
frontend/deployment.json defines the public topology. The Pages preparation
script exports only public settings and installs response headers; no Worker or
Pages Functions are required.
Repositories with WEB_DEPLOY_ENABLED=true run main-push validation on the
ubuntu-latest build runner; enable this variable only on Remote Git. Matching
version-tag pushes promote one artifact through PocketHost staging/production,
then upload Pages assets. Enable the local publication check with
git config core.hooksPath .githooks; it rejects gaps between version tags,
tags that disagree with package versions, and main pushes without a matching
tag on the same commit. Tagged CI repeats the version check.
CI needs repository secrets CLOUDFLARE_API_TOKEN,
POCKETHOST_CONNECTION_JSON, POCKETHOST_PRODUCTION_SETTINGS,
POCKETHOST_STAGING_SETTINGS, POCKETHOST_SFTP_KEY and POCKETHOST_KNOWN_HOSTS.
Optional POCKETHOST_PRODUCTION_MIRROR_SETTINGS and
POCKETHOST_STAGING_MIRROR_SETTINGS contain only the seven mirror variables;
they overlay existing deployment settings without changing unrelated values.
Provider output is withheld, keys use private temporary files, and build/test
subprocesses receive no deployment credentials.
backend/scripts/deploy.mjs takes --target staging|production,
--connection <protected JSON file> and --artifact <directory>. Connections define host,
port, user, optional identityFile, and distinct target instances, origins
and settings files. Use --dry-run for checksums; production requires
--confirm-production v<package version> and protected edge configuration.
Promote one immutable artifact through staging. Activation verifies hooks before
publishing HTML; preserve previous modules, assets, data and session keys.
Back up before migration and pause source writes. If replay records are absent,
wait 300 seconds plus the signature timeout. backend/scripts/migrate.py exports
private SQLite data and generates guarded imports for empty or identical targets.
On fresh instances, place the import after the base migration and before
1791158400_moderation.js. Never reset storage after a failed operation.
Frontend checks: lint, typecheck, test:config, build:preview. Backend:
test, test:guards, test:integration (PocketBase on PATH or
POCKETBASE_BINARY). Browser checks use test:a11y with A11Y_BASE_URL and
BROWSERLESS_URL / BROWSERLESS_TOKEN; source fixtures need SCGUI_SOURCE_TESTS=1.
License
MIT — see LICENSE.