chore: sync submodule state (parent ref update)

Made-with: Cursor
This commit is contained in:
defiQUG
2026-03-02 12:14:14 -08:00
parent b6a776e5d7
commit 25c96e210a
316 changed files with 29779 additions and 677 deletions

View File

@@ -9,7 +9,7 @@ on:
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * 1' # weekly Monday
- cron: '0 0 * * 1' # weekly Monday
jobs:
analyze:
@@ -37,4 +37,4 @@ jobs:
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
category: '/language:${{ matrix.language }}'

View File

@@ -1,7 +1,6 @@
# Build Snap companion site (pathPrefix /snap). Optional: set repository variable
# SNAP_VERIFY_BASE_URL (e.g. https://explorer.d-bis.org) to run verify-snap-site-vmid5000.sh after build.
# Build Snap companion site (pathPrefix /snap). Uploads artifact for deploy to your host.
# Optional: set secret GATSBY_SNAP_API_BASE_URL for production API in build.
name: Deploy Snap Site
name: Build Snap Site
on:
push:
@@ -9,8 +8,6 @@ on:
paths:
- 'packages/site/**'
- 'packages/snap/**'
- 'scripts/deploy-snap-site-to-vmid5000.sh'
- 'scripts/verify-snap-site-vmid5000.sh'
- '.github/workflows/deploy-snap-site.yml'
workflow_dispatch:
@@ -18,8 +15,8 @@ env:
GATSBY_PATH_PREFIX: /snap
jobs:
build-and-verify:
name: Build site and verify
build:
name: Build site
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -43,7 +40,3 @@ jobs:
name: snap-site-${{ github.sha }}
path: packages/site/public/
retention-days: 7
- name: Verify deployed Snap site (smoke)
if: ${{ vars.SNAP_VERIFY_BASE_URL != '' && vars.SNAP_VERIFY_BASE_URL != null }}
run: ./scripts/verify-snap-site-vmid5000.sh "${{ vars.SNAP_VERIFY_BASE_URL }}"

View File

@@ -4,19 +4,19 @@ Use this when submitting the Chain 138 Snap via the [MetaMask Snaps Directory In
## Pre-filled values
| Field | Value |
|-------|--------|
| **Snap name** | Chain 138 *(must match `proposedName` in snap.manifest.json)* |
| **Snap builder name and URL** | BIS Innovations — https://github.com/bis-innovations |
| **Snap website URL** | https://github.com/bis-innovations/chain138-snap#readme *(or your deployed companion site, e.g. https://explorer.d-bis.org/snap/)* |
| **GitHub repository** | https://github.com/bis-innovations/chain138-snap |
| **npm package** | https://www.npmjs.com/package/chain138-snap |
| **Snap version to allowlist** | 0.1.0 *(must match package.json and snap.manifest.json)* |
| **Snap auditor / audit report** | Leave blank *(no key-management APIs; audit not required)* |
| Field | Value |
| ------------------------------- | ----------------------------------------------------------------------------------------------- |
| **Snap name** | Chain 138 _(must match `proposedName` in snap.manifest.json)_ |
| **Snap builder name and URL** | BIS Innovations — https://github.com/bis-innovations |
| **Snap website URL** | https://github.com/bis-innovations/chain138-snap#readme _(or your deployed companion site URL)_ |
| **GitHub repository** | https://github.com/bis-innovations/chain138-snap |
| **npm package** | https://www.npmjs.com/package/chain138-snap |
| **Snap version to allowlist** | 0.1.2 _(must match package.json and snap.manifest.json)_ |
| **Snap auditor / audit report** | Leave blank _(no key-management APIs; audit not required)_ |
## Short description (12 sentences)
Chain 138 adds DeFi Oracle Meta Mainnet (and ALL Mainnet) support in MetaMask: network params, token list, market data, swap quotes, and CCIP bridge routes. Use with the token-aggregation API for full features.
Chain 138 adds DeFi Oracle Meta Mainnet (and ALL Mainnet) support in MetaMask: network params, token list, market data, swap quotes, and bridge routes (CCIP and Trustless). Use with the token-aggregation API for full features.
## Long description
@@ -25,15 +25,15 @@ Use line breaks and lists; no HTML. Example:
- **Networks:** Chain 138 (DeFi Oracle Meta Mainnet) and ALL Mainnet (651940); full EIP-3085 params from API.
- **Token list & market data:** Tokens and USD prices via token-aggregation (or optional JSON URLs).
- **Swap quotes:** In-Snap quotes for Chain 138 when quote API is configured.
- **Bridge routes:** CCIP WETH9/WETH10 routes to Ethereum Mainnet when bridge API is available.
- **Bridge routes:** CCIP (WETH9/WETH10) and Trustless (Lockbox) routes to Ethereum Mainnet when bridge API is available.
After installing, dApps must pass `apiBaseUrl` (your token-aggregation base URL) when invoking the Snap for market data, swap quote, and bridge routes. See the repo README and INTEGRATORS.md.
## Customer support
- **Escalation contact:** *(confidential; provide email or contact form)*
- **Escalation contact:** _(confidential; provide email or contact form)_
- **Public support:** GitHub Issues — https://github.com/bis-innovations/chain138-snap/issues
*(Add at least one other channel, e.g. docs link or support URL.)*
_(Add at least one other channel, e.g. docs link or support URL.)_
## Images

View File

@@ -18,4 +18,4 @@ Before submitting the Chain 138 Snap for MetaMask allowlisting, confirm:
- [x] No `console` logs, to-do comments, or unused permissions in Snap code.
- [x] Security: CI runs MetaMask Security Code Scanner (`.github/workflows/security-code-scanner.yml`). Optionally run [Snapper](https://docs.metamask.io/snaps/how-to/get-allowlisted/) locally before submission.
After publishing to npm, submit via the [MetaMask Snaps Directory Information form](https://docs.metamask.io/snaps/how-to/get-allowlisted/#1-submit-your-snap).
**Submitted** to the MetaMask Snaps Directory; pending review. For future versions, use the [MetaMask Snaps Directory Information Update form](https://docs.metamask.io/snaps/how-to/get-allowlisted/#5-update-your-snap).

View File

@@ -1,129 +0,0 @@
# Deploy Chain 138 Snap site to VMID 5000 (Production)
The Snap companion site can be published on the same host as the explorer (VMID 5000) at **https://explorer.d-bis.org/snap/** and is linked from the explorer navbar ("MetaMask Snap").
## Prerequisites
- Site built with **pathPrefix** `/snap` (so assets load under `/snap/`).
- Access to Proxmox host that runs VMID 5000 (e.g. `pct` or SSH to `PROXMOX_HOST_R630_02`).
## 1. Build for production (pathPrefix /snap)
From the Chain 138 Snap repo root:
```bash
GATSBY_PATH_PREFIX=/snap pnpm --filter site run build
```
This writes the static site into `packages/site/public/` with asset paths prefixed by `/snap/`.
## 2. Deploy to VMID 5000
**Option A build and deploy in one go:**
```bash
./scripts/deploy-snap-site-to-vmid5000.sh --build
```
**Production build (market/bridge/swap from live API):** set `GATSBY_SNAP_API_BASE_URL` when building:
```bash
GATSBY_SNAP_API_BASE_URL=https://your-token-aggregation-api.com ./scripts/deploy-snap-site-to-vmid5000.sh --build
```
**Option B deploy an existing build:**
```bash
./scripts/deploy-snap-site-to-vmid5000.sh
```
The script:
- Builds the site (only if `--build` is passed).
- Packs `packages/site/public/` into a tarball and deploys it to **/var/www/html/snap/** on VMID 5000.
- Works when run: **inside** the VM (direct), **on the Proxmox host** (`pct exec`), or **from a remote machine** (SSH to Proxmox, then `pct`). Set `PROXMOX_HOST_R630_02` (default `192.168.11.12`) when running remotely.
## 3. Nginx on VMID 5000
Nginx must serve the `/snap/` path so `/snap` and `/snap/` return 200. **One command from the Proxmox host** (or from a machine with SSH to the host):
```bash
cd explorer-monorepo
./scripts/apply-nginx-snap-vmid5000.sh
```
This runs `fix-nginx-serve-custom-frontend.sh` inside VMID 5000 (via `pct` or SSH). Alternatively, run the fix script **inside VMID 5000**:
- **`explorer-monorepo/scripts/fix-nginx-serve-custom-frontend.sh`** run inside the VM. It configures (among other things):
- `location = /snap` and `location /snap/` → alias `/var/www/html/snap/`, SPA fallback to `/snap/index.html`.
If you manage nginx by hand, add inside the HTTPS `server` block for explorer.d-bis.org:
```nginx
location /snap/ {
alias /var/www/html/snap/;
try_files $uri $uri/ /snap/index.html;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
```
Then reload nginx.
## 4. Explorer integration
The explorer frontend (`explorer-monorepo/frontend/public/index.html`) has a nav link **"MetaMask Snap"** pointing to `/snap/`. After deploying the explorer frontend to VMID 5000 (e.g. `explorer-monorepo/scripts/deploy-frontend-to-vmid5000.sh`), that link will open the Snap site at https://explorer.d-bis.org/snap/.
## 5. Production Snap and API URL
- **Snap**: For production, the Snap is typically installed from the npm package or a published Snap ID; the companion site at `/snap/` is the install/connect page.
- **API**: Set `GATSBY_SNAP_API_BASE_URL` when building the site (e.g. in `packages/site/.env.production`) to your token-aggregation API base URL so the Market data, Bridge, and Swap quote cards work. Rebuild and redeploy after changing it.
- **Token / bridge list URLs:** If you use GitHub (or other) JSON URLs for token list, bridge list, or networks, pin them to a tag or commit SHA for reproducible deploys. Validate: `./scripts/validate-token-lists.sh <URL1> [URL2] ...`.
## Verification checks
After deploy, the script runs:
- `/var/www/html/snap/index.html` exists in the VM
- Nginx config has `location /snap/`
- `http://localhost/snap/` returns 200
- Response body contains Snap app content (Connect|Snap|MetaMask)
**Standalone verify (Snap only):**
```bash
cd metamask-integration/chain138-snap
./scripts/verify-snap-site-vmid5000.sh [BASE_URL]
# BASE_URL defaults to https://explorer.d-bis.org
```
**All VMID 5000 checks (explorer + API + Snap):**
```bash
cd explorer-monorepo
./scripts/verify-vmid5000-all.sh [BASE_URL]
```
This runs: Blockscout port 4000, nginx `/api/` and `/snap/`, public `/api/v2/stats`, `/api/v2/blocks`, `/api/v2/transactions`, explorer root `/`, Snap site `/snap/` (200 + content), and nginx config for `/snap/`.
## Quick reference
| Step | Command |
|------|--------|
| Build site (pathPrefix /snap) | `GATSBY_PATH_PREFIX=/snap pnpm --filter site run build` |
| Deploy (with build) | `./scripts/deploy-snap-site-to-vmid5000.sh --build` |
| Deploy (existing build) | `./scripts/deploy-snap-site-to-vmid5000.sh` |
| Update nginx on VMID 5000 | From host: `explorer-monorepo/scripts/apply-nginx-snap-vmid5000.sh` (or run `fix-nginx-serve-custom-frontend.sh` inside the VM) |
| Deploy explorer (incl. Snap link) | `explorer-monorepo/scripts/deploy-frontend-to-vmid5000.sh` |
| Verify Snap only | `./scripts/verify-snap-site-vmid5000.sh` |
| Verify all (explorer + API + Snap) | `explorer-monorepo/scripts/verify-vmid5000-all.sh` |
**URLs:** https://explorer.d-bis.org/snap/ and http://192.168.11.140/snap/ (replace IP if your VM differs). **Version/health:** https://explorer.d-bis.org/snap/version.json (build version and buildTime).
## CI (GitHub Actions)
The workflow **`.github/workflows/deploy-snap-site.yml`** runs on push to `main` (when site/snap/scripts change): it builds the site and uploads the artifact. To **run the Snap site smoke verify** after deploy, set a **repository variable** in GitHub:
- **Name:** `SNAP_VERIFY_BASE_URL`
- **Value:** e.g. `https://explorer.d-bis.org` (no trailing slash)
Then the workflow will run `verify-snap-site-vmid5000.sh` against that URL. Optional: set secret **`GATSBY_SNAP_API_BASE_URL`** so the CI build uses your production API URL.

View File

@@ -12,22 +12,14 @@ The Snap and companion site call the token-aggregation API for networks, token l
1. **Prerequisites**
- Node.js 20+
- PostgreSQL 14+ with TimescaleDB
- RPC URLs for Chain 138 and 651940 (e.g. from `.env.example`)
- PostgreSQL 14+ with TimescaleDB (if your token-aggregation service uses it)
- RPC URLs for Chain 138 and 651940 (e.g. from your API `.env.example`)
2. **Setup**
```bash
cd smom-dbis-138/services/token-aggregation
cp .env.example .env
# Edit .env: set DATABASE_URL, CHAIN_138_RPC_URL, CHAIN_651940_RPC_URL
```
- Clone or use your token-aggregation service repo. Copy `.env.example` to `.env` and set `DATABASE_URL`, `CHAIN_138_RPC_URL`, `CHAIN_651940_RPC_URL` (or equivalent).
3. **Database**
- Apply migrations (see `QUICK_START.md` or `QUICK_START_COMPLETE.md` in that repo), e.g.:
```bash
psql $DATABASE_URL -f ../../explorer-monorepo/backend/database/migrations/0011_token_aggregation_schema.up.sql
# plus 0012 if used
```
- Apply migrations as required by your token-aggregation service (see that services QUICK_START or migration docs).
4. **Run**
```bash
@@ -41,10 +33,10 @@ The Snap and companion site call the token-aggregation API for networks, token l
### Option B: Docker
```bash
cd smom-dbis-138/services/token-aggregation
# From your token-aggregation service directory
# Ensure .env exists with DATABASE_URL etc.
docker-compose up -d
# API on http://localhost:3000
# API on http://localhost:3000 (or the port your service uses)
```
### Option C: Deployed / staging
@@ -64,8 +56,9 @@ Use your deployed token-aggregation base URL (e.g. `https://your-token-aggregati
The companion site passes `apiBaseUrl` to the Snap so that market data, bridge, and swap quote cards work.
1. **Create env file**
```bash
cd metamask-integration/chain138-snap/packages/site
cd packages/site
cp .env.production.dist .env
# or .env.production for production build
```
@@ -73,7 +66,7 @@ The companion site passes `apiBaseUrl` to the Snap so that market data, bridge,
2. **Set API base URL**
- Local token-aggregation: `GATSBY_SNAP_API_BASE_URL=http://localhost:3000`
- Deployed: `GATSBY_SNAP_API_BASE_URL=https://your-token-aggregation-api.com`
Do not add a trailing slash.
Do not add a trailing slash.
3. **Restart the site** if it is already running so the variable is picked up (Gatsby reads env at build/start).
@@ -81,10 +74,9 @@ The companion site passes `apiBaseUrl` to the Snap so that market data, bridge,
## 3. Run Snap + site
From the Chain 138 Snap monorepo root:
From the Chain 138 Snap repo root:
```bash
cd metamask-integration/chain138-snap
pnpm run start
```
@@ -104,7 +96,6 @@ Use the [E2E testing checklist (MetaMask Flask)](TESTING_INSTRUCTIONS.md#e2e-tes
To run the automated E2E tests (site loads and shows Snap Connect UI):
```bash
cd metamask-integration/chain138-snap
pnpm install
npx playwright install
pnpm run test:e2e
@@ -120,16 +111,14 @@ pnpm run test:e2e
## 6. Run E2E against deployed Snap site
To run Playwright (or manual checks) against the **deployed** Snap site (e.g. https://explorer.d-bis.org/snap/):
To run Playwright (or manual checks) against a **deployed** Snap companion site (e.g. https://yoursite.com/snap/):
1. **Playwright against a URL:** Set the base URL and run tests (if your Playwright config supports it), e.g.:
- Add a second config or override in `playwright.config.ts`: `baseURL: process.env.SNAP_BASE_URL || 'http://localhost:8000'`.
- Run: `SNAP_BASE_URL=https://explorer.d-bis.org/snap pnpm run test:e2e` (after adding `baseURL` to the config).
- Or run the manual E2E checklist in TESTING_INSTRUCTIONS.md while opening https://explorer.d-bis.org/snap/ in the browser.
- Add a config or override in `playwright.config.ts`: `baseURL: process.env.SNAP_BASE_URL || 'http://localhost:8000'`.
- Run: `SNAP_BASE_URL=https://yoursite.com/snap pnpm run test:e2e` (after adding `baseURL` to the config).
- Or run the manual E2E checklist in TESTING_INSTRUCTIONS.md while opening your deployed site URL in the browser.
2. **Environment:** The deployed site must be built with `GATSBY_SNAP_API_BASE_URL` set to the production token-aggregation URL for Market, Bridge, and Swap cards to work. If not set, those cards will show "Set GATSBY_SNAP_API_BASE_URL".
3. **CI:** You can run `scripts/verify-snap-site-vmid5000.sh https://explorer.d-bis.org` in CI as a smoke test after deploy (no MetaMask required). Set repo variable `SNAP_VERIFY_BASE_URL` to enable the verify step in `.github/workflows/deploy-snap-site.yml`.
2. **Environment:** The deployed site must be built with `GATSBY_SNAP_API_BASE_URL` set to your token-aggregation URL for Market, Bridge, and Swap cards to work. If not set, those cards will show "Set GATSBY_SNAP_API_BASE_URL".
---

View File

@@ -1,6 +1,6 @@
# Chain 138 Snap — Integrator guide
Use this Snap from your dApp to provide Chain 138 (and ALL Mainnet) network params, token list, market data, swap quotes, and bridge routes inside MetaMask.
Use this Snap from your dApp to provide Chain 138 (and ALL Mainnet) network params, token list, market data, swap quotes, and bridge routes (CCIP and Trustless) inside MetaMask.
## Production Snap ID
@@ -35,11 +35,11 @@ params: {
You can pass these instead of or in addition to `apiBaseUrl` for specific data:
| Param | Purpose | Used by RPCs |
|----------------|----------------------|--------------------------------------|
| `networksUrl` | JSON URL for networks| `get_networks`, `get_chain138_config` |
| `tokenListUrl` | JSON URL for tokens | `get_token_list`, `get_token_list_url`|
| `bridgeListUrl`| JSON URL for bridge | `get_bridge_routes`, `show_bridge_routes` |
| Param | Purpose | Used by RPCs |
| --------------- | --------------------- | ----------------------------------------- |
| `networksUrl` | JSON URL for networks | `get_networks`, `get_chain138_config` |
| `tokenListUrl` | JSON URL for tokens | `get_token_list`, `get_token_list_url` |
| `bridgeListUrl` | JSON URL for bridge | `get_bridge_routes`, `show_bridge_routes` |
## Companion site env

View File

@@ -2,6 +2,8 @@
Use this checklist to complete **manual** E2E verification. Covers Snap install, all RPC methods, and companion site cards.
**For thorough pre-publish testing** (logos/images, every asset, production-like test, recommendations): use **[docs/PRE_PUBLISH_TESTING.md](docs/PRE_PUBLISH_TESTING.md)**.
## Prerequisites
- [ ] MetaMask Flask installed: https://metamask.io/flask/
@@ -29,16 +31,24 @@ Use this checklist to complete **manual** E2E verification. Covers Snap install,
- [ ] `get_bridge_routes`, `show_bridge_routes` (apiBaseUrl or bridgeListUrl)
- [ ] `get_swap_quote`, `show_swap_quote` (apiBaseUrl, tokenIn, tokenOut, amountIn; optional chainId)
*(Use browser console and `wallet_invokeSnap` as in TESTING_INSTRUCTIONS.md.)*
_(Use browser console and `wallet_invokeSnap` as in TESTING_INSTRUCTIONS.md.)_
---
## 3. Companion site cards
- [ ] **Market data:** "Show market data" opens Snap dialog; "Fetch market summary" shows tokens/prices
- [ ] **Bridge:** "Show bridge routes" opens Snap dialog with CCIP routes
- [ ] **Bridge:** "Show bridge routes" opens Snap dialog with CCIP and Trustless routes
- [ ] **Swap quote:** Enter token In/Out addresses and amount (raw); "Get quote" shows amountOut; "Show quote in Snap" opens dialog
---
When all items are checked, manual E2E (snap-9 and snap-10) is complete.
## 4. Logos and images (pre-publish)
- [ ] Snap icon shows in MetaMask (Settings → Snaps → Chain 138).
- [ ] Token list from API: every token has `logoURI`; list has list-level `logoURI` (see PRE_PUBLISH_TESTING.md §4.3).
- [ ] Networks from API: each network has `iconUrls` and URLs resolve (see PRE_PUBLISH_TESTING.md §4.4).
---
When all items are checked, manual E2E (snap-9 and snap-10) is complete. Before publishing, complete the full [PRE_PUBLISH_TESTING.md](docs/PRE_PUBLISH_TESTING.md) sign-off.

View File

@@ -0,0 +1,18 @@
# Chain 138 Snap — Next steps
## Completed
- [x] **GitHub repo:** https://github.com/bis-innovations/chain138-snap (pushed; includes CodeQL workflow, npm README, publish script)
- [x] **npm:** [chain138-snap@0.1.2](https://www.npmjs.com/package/chain138-snap) published; Snap ID: `npm:chain138-snap`
- [x] **CodeQL:** `.github/workflows/codeql.yml` added; Security → Code scanning will run on push/PR and weekly
- [x] **Docs:** README on npm, INTEGRATORS.md, PUSH_AND_PUBLISH.md, ALLOWLIST_FORM_FIELDS.md, ALLOWLIST_SOURCE_AND_COMPLIANCE_CHECKLIST.md
- [x] **Documentation and FAQs:** All Snap-specific docs and FAQs live in this repo. See [docs/README.md](docs/README.md) for the index. Includes: CONTRIBUTING, FAQ, DEPLOY_COMPANION_SITE, RUNBOOK. Proxmox and other proprietary/internal references have been removed from docs and scripts.
## Remaining (manual)
1. ~~**Submit for allowlist**~~ **Done** — Submitted to MetaMask Snaps Directory; pending review/approval. After allowlisting, the Snap will be installable in standard MetaMask (non-Flask).
2. ~~**Dependabot alerts**~~ **Addressed** — Added pnpm `overrides` and Yarn `resolutions` for vulnerable transitive deps (cookie, glob, sharp, socket.io, ws, path-to-regexp). Bumped `sharp` override to ^0.34.5. Snap tests fixed with `@types/jest`. Re-run Dependabot/audit after pushing; merge any new Dependabot PRs for remaining bumps.
3. **Future releases**
Bump version in `packages/snap/package.json`, then from this repo root run `pnpm run publish:snap`. Push to GitHub. If this repo is used as a subtree elsewhere, use your usual subtree push (e.g. `git subtree push --prefix=chain138-snap chain138-snap main` or split + force push if the remote has diverged).

View File

@@ -4,9 +4,7 @@ The Snap repo is **https://github.com/bis-innovations/chain138-snap**.
## 1. Push to GitHub
This Snap lives inside the **metamask-integration** repo as `chain138-snap/`. To push updates to the dedicated Snap repo:
From the **metamask-integration** repo root (parent of `chain138-snap/`):
If this repo is used as a subtree (e.g. inside a parent monorepo as `chain138-snap/`), push from the parent repo root:
```bash
# One-time: add the Snap repo as a remote (if not already added)
@@ -18,13 +16,16 @@ git commit -m "your message"
git subtree push --prefix=chain138-snap chain138-snap main
```
The remote **chain138-snap** and branch **main** are already set up; the initial push has been done.
Use the remote name and branch that match your setup (e.g. `chain138-snap`, `main`).
## 2. Publish Snap package to npm
From the **chain138-snap** monorepo root:
```bash
# 0. (Recommended) Run thorough pre-publish testing
# See docs/PRE_PUBLISH_TESTING.md — build, logos/images, all RPC methods, companion site, Send page, production-like test.
# 1. Build (updates manifest shasum)
pnpm run build
@@ -33,9 +34,8 @@ pnpm run build
# "Publish" and "Bypass 2FA for publish" at https://www.npmjs.com/settings/~/tokens
# 3. Publish (uses NPM_ACCESS_TOKEN from .env if set)
# Run from the chain138-snap monorepo root (not from the parent proxmox repo):
# Run from the chain138-snap monorepo root:
pnpm run publish:snap
# If you're in the proxmox repo root: cd metamask-integration/chain138-snap && pnpm run publish:snap
# Or manually: cd packages/snap && npm login && npm publish --access public
```

View File

@@ -1,67 +1,99 @@
# Chain 138 Snap (MetaMask)
This Snap provides **Chain 138** (DeFi Oracle Meta Mainnet) and **ALL Mainnet** (651940) support in MetaMask: network params, token list, market data (prices), swap quotes, and CCIP bridge routes. It reads configuration from a **token-aggregation** (or compatible) API.
A MetaMask Snap that adds **both** supported blockchains inside MetaMask:
**Why we built it:** MetaMask already supports Chain 138 as a custom EVM network (add via RPC), but native **Swaps**, **Portfolio Bridge**, and **USD pricing** do not include Chain 138 (Consensys-controlled). No public Snap existed for swap/bridge/pricing on 138. This Snap gives in-wallet swap quotes, bridge routes, and market data by calling our APIs, so users get feature parity without waiting for Consensys. See [docs/04-configuration/CHAIN138_WALLET_ECOSYSTEM_AND_RATIONALE.md](../../docs/04-configuration/CHAIN138_WALLET_ECOSYSTEM_AND_RATIONALE.md#2-why-we-created-the-metamask-snap) for full rationale.
| Blockchain | Chain ID | Name |
| --------------- | -------- | ------------------------ |
| **Chain 138** | 138 | DeFi Oracle Meta Mainnet |
| **ALL Mainnet** | 651940 | ALL Mainnet |
For detailed development and testing, see [TESTING_INSTRUCTIONS.md](TESTING_INSTRUCTIONS.md). For implementation phases and backend APIs, see [docs/04-configuration/metamask/SNAP_IMPLEMENTATION_ROADMAP.md](../../docs/04-configuration/metamask/SNAP_IMPLEMENTATION_ROADMAP.md) in the repo root.
The Snap provides **network params**, **token list**, **market data** (USD prices), **swap quotes**, and **CCIP bridge routes** for these chains. It reads configuration from a **token-aggregation** (or compatible) API that you supply via `apiBaseUrl` or optional URL params.
**Integrators:** Production Snap ID: **`npm:chain138-snap`**. Market data, swap quote, and bridge route features require the dApp to pass `apiBaseUrl` (the token-aggregation service base URL) when invoking the Snap. You may also pass optional URLs: `networksUrl`, `tokenListUrl`, `bridgeListUrl`. Set `GATSBY_SNAP_API_BASE_URL` on the companion site so the demo page works. For production, set `SNAP_ORIGIN=npm:chain138-snap` in the site env so the companion uses the published Snap.
**Why this Snap:** MetaMask supports Chain 138 as a custom EVM network, but native **Swaps**, **Portfolio Bridge**, and **USD pricing** do not include Chain 138 or ALL Mainnet. This Snap gives in-wallet swap quotes, bridge routes, and market data by calling your token-aggregation API, so users get feature parity on both blockchains without waiting for upstream support.
This Snap targets the **latest stable MetaMask Snap SDK** (`@metamask/snaps-sdk`).
---
## Snaps is pre-release software
## Features (both blockchains)
To interact with (your) Snaps, you will need to install [MetaMask Flask](https://metamask.io/flask/),
a canary distribution for developers that provides access to upcoming features.
- **Networks & config** — EIP-3085 params for Chain 138 and ALL Mainnet (`get_networks`, `get_chain138_config`, `get_chain138_market_chains`).
- **Token list** — By chain; optional `chainId` (138 or 651940) for `get_token_list` / `get_token_list_url`.
- **Market data** — Tokens and USD prices; in-Snap dialog via `show_market_data`.
- **Bridge routes** — CCIP (WETH9/WETH10) and Trustless (Lockbox) routes to Ethereum Mainnet; dialog via `show_bridge_routes`.
- **Swap quote** — Quote for Chain 138; dialog via `show_swap_quote`.
- **Send (Chain 138)** — Companion site **Send** page (`/send`) so users can send ETH on Chain 138 without using MetaMasks in-wallet Send button (which can error with “No XChain Swaps native asset found” on custom chains).
- **Oracles & dynamic info** — API config and in-Snap dialog (`get_oracles`, `show_dynamic_info`).
## Getting Started
Every method and parameter is documented with **tables and diagrams** in **[docs/FEATURES.md](docs/FEATURES.md)** (method matrix, params, response shapes, request flow).
Clone the template-snap repository [using this template](https://github.com/MetaMask/template-snap-monorepo/generate)
and set up the development environment.
---
**Default (pnpm):**
## Snap ID
**Production:** `npm:chain138-snap`
Install from a dApp or the [companion site](https://github.com/bis-innovations/chain138-snap) by connecting with MetaMask and adding the Snap with this ID.
---
## Integrators
- **Snap ID:** `npm:chain138-snap`
- **Market data, swap quote, bridge routes** require the dApp to pass **`apiBaseUrl`** (your token-aggregation base URL) when invoking the Snap.
- Optional overrides: `networksUrl`, `tokenListUrl`, `bridgeListUrl` (see [INTEGRATORS.md](INTEGRATORS.md)).
- Companion site: set `GATSBY_SNAP_API_BASE_URL` for the demo; set `SNAP_ORIGIN=npm:chain138-snap` for production so the site uses the published Snap.
---
## Testing before publish
For **thorough pre-publish testing** (build, all RPC methods, **logos/images** for chain and tokens, companion site, Send page, production-like test, and recommendations), see **[docs/PRE_PUBLISH_TESTING.md](docs/PRE_PUBLISH_TESTING.md)**. Quick manual E2E: [MANUAL_E2E_CHECKLIST.md](MANUAL_E2E_CHECKLIST.md) and [TESTING_INSTRUCTIONS.md](TESTING_INSTRUCTIONS.md).
---
## Troubleshooting (balance, swap, data not showing)
If **main balance or USD is not showing**, **Swap is malfunctioning**, or **historical/market data** does not load, see **[docs/CHAIN138_SNAP_TROUBLESHOOTING.md](docs/CHAIN138_SNAP_TROUBLESHOOTING.md)**. Summary:
- **$0.00 / no conversion rate:** MetaMask has no price feed for Chain 138; use Snap “Show market data” on the companion site or accept quantity-only in the wallet.
- **In-wallet Swap fails:** MetaMask Swap does not support Chain 138; use [Send on Chain 138](https://explorer.d-bis.org/snap/send) and swap quotes from the Snap companion site.
- **Snap market/swap/bridge errors:** Ensure `GATSBY_SNAP_API_BASE_URL` points to a host that serves the token-aggregation API (`/api/v1/networks`, `/api/v1/tokens`, `/api/v1/quote`, etc.); see the troubleshooting doc and [FAQ](docs/FAQ.md).
---
## Getting started
**Clone this repo** and run:
```shell
pnpm install && pnpm start
```
**Alternative (yarn):**
Or with Yarn: `yarn install && yarn start`. See [PACKAGE_MANAGER.md](PACKAGE_MANAGER.md).
```shell
yarn install && yarn start
```
The companion site and Snap are served at **http://localhost:8000**. Use [MetaMask Flask](https://metamask.io/flask/) for development; once the Snap is allowlisted, standard MetaMask can install it.
See [PACKAGE_MANAGER.md](PACKAGE_MANAGER.md) for details.
---
## Cloning
## Documentation
This repository contains GitHub Actions that you may find useful, see
`.github/workflows` and [Releasing & Publishing](https://github.com/MetaMask/template-snap-monorepo/edit/main/README.md#releasing--publishing)
below for more information.
| Link | Description |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| [docs/README.md](docs/README.md) | Documentation index |
| [docs/FEATURES.md](docs/FEATURES.md) | **All functions and features** — RPC methods, params, both blockchains, tables, flow diagram |
| [INTEGRATORS.md](INTEGRATORS.md) | Integrator guide (Snap ID, apiBaseUrl, RPC list) |
| [TESTING_INSTRUCTIONS.md](TESTING_INSTRUCTIONS.md) | Development and E2E testing |
| [docs/FAQ.md](docs/FAQ.md) | FAQ |
If you clone or create this repository outside the MetaMask GitHub organization,
you probably want to run `./scripts/cleanup.sh` to remove some files that will
not work properly outside the MetaMask GitHub organization.
If you don't wish to use any of the existing GitHub actions in this repository,
simply delete the `.github/workflows` directory.
---
## Contributing
### Testing and Linting
- **Lint:** `pnpm run lint` / `pnpm run lint:fix`
- **Test:** `pnpm run test` (Snap unit tests), `pnpm run test:e2e` (Playwright; run `npx playwright install` once)
- See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) and [E2E_PREPARATION.md](E2E_PREPARATION.md) for full setup.
**pnpm (default):** `pnpm run test`, `pnpm run lint`, `pnpm run lint:fix`
**yarn:** `yarn test`, `yarn lint`, `yarn lint:fix`
Scripts are disabled by default (LavaMoat). If needed: `pnpm run allow-scripts` and enable the package in the `lavamoat.allowScripts` section of `package.json`. See [@lavamoat/allow-scripts](https://github.com/LavaMoat/LavaMoat/tree/main/packages/allow-scripts).
- **Unit tests:** `pnpm run test` (Snap Jest tests).
- **E2E (Playwright):** `pnpm run test:e2e` — starts the dev server if needed and runs companion-site E2E tests. First time run `npx playwright install`. See [TESTING_INSTRUCTIONS.md](TESTING_INSTRUCTIONS.md) and [E2E_PREPARATION.md](E2E_PREPARATION.md) for full manual E2E (MetaMask Flask) and token-aggregation setup.
---
### Using NPM packages with scripts
Scripts are disabled by default for security reasons. If you need to use NPM
packages with scripts, run **pnpm run allow-scripts** (or **yarn allow-scripts auto**) and enable the
script in the `lavamoat.allowScripts` section of `package.json`.
See the documentation for [@lavamoat/allow-scripts](https://github.com/LavaMoat/LavaMoat/tree/main/packages/allow-scripts)
for more information.
This Snap targets the **latest stable MetaMask Snap SDK** (`@metamask/snaps-sdk`).

View File

@@ -1,49 +0,0 @@
# Chain 138 Snap Runbook
Quick reference for building, deploying, and verifying the Snap companion site.
## Build
```bash
GATSBY_PATH_PREFIX=/snap GATSBY_BUILD_SHA=$(git rev-parse --short HEAD) pnpm --filter site run build
# Production API (market/bridge/swap):
# GATSBY_SNAP_API_BASE_URL=https://your-token-aggregation-api.com
```
## Deploy to VMID 5000
```bash
./scripts/deploy-snap-site-to-vmid5000.sh --build # build + deploy
GATSBY_SNAP_API_BASE_URL=https://your-api.com ./scripts/deploy-snap-site-to-vmid5000.sh --build # production API
./scripts/deploy-snap-site-to-vmid5000.sh # deploy existing build
```
## Verify
```bash
./scripts/verify-snap-site-vmid5000.sh [BASE_URL]
# Full explorer + API + Snap:
# cd ../explorer-monorepo && ./scripts/verify-vmid5000-all.sh [BASE_URL]
```
## Rollback
See `explorer-monorepo/RUNBOOK.md` (rollback from VM or from host using `/tmp/snap-site-last.tar`).
## Nginx
From Proxmox host: `cd explorer-monorepo && ./scripts/apply-nginx-snap-vmid5000.sh`. Or inside VMID 5000: `explorer-monorepo/scripts/fix-nginx-serve-custom-frontend.sh`. Ensures `/snap` and `/snap/` return 200 with content.
## Token / bridge list validation
```bash
./scripts/validate-token-lists.sh [URL1] [URL2] ...
# Or set TOKEN_LIST_JSON_URL, BRIDGE_LIST_JSON_URL, NETWORKS_JSON_URL
```
## URLs
- Production: https://explorer.d-bis.org/snap/
- Version/health: https://explorer.d-bis.org/snap/version.json
See also `DEPLOY_VMID5000.md` and `explorer-monorepo/RUNBOOK.md`.

View File

@@ -2,8 +2,8 @@
## HTTPS only
- The Snap companion site is intended to be served over **HTTPS** (e.g. https://explorer.d-bis.org/snap/). Avoid mixed content: ensure all API and asset URLs use HTTPS when the page is loaded over HTTPS.
- Explorer and token-aggregation API should be HTTPS in production.
- The Snap companion site is intended to be served over **HTTPS** in production. Avoid mixed content: ensure all API and asset URLs use HTTPS when the page is loaded over HTTPS.
- Your token-aggregation API should use HTTPS in production.
## Token-aggregation API (public)

View File

@@ -3,6 +3,8 @@
**Date:** 2026-01-30
**Status:** Built and ready for testing
**Thorough pre-publish testing:** For a complete pass before every npm publish (including **all logos/images**, every RPC method, companion site, Send page, production-like test, and recommendations), use **[docs/PRE_PUBLISH_TESTING.md](docs/PRE_PUBLISH_TESTING.md)**.
---
## Prerequisites
@@ -12,10 +14,12 @@
- Install as separate browser extension (won't conflict with regular MetaMask)
2. **Snap Development Server Running**
```bash
cd metamask-integration/chain138-snap
pnpm run start
```
(From the repo root.)
(Or use **yarn start** if you prefer Yarn; see [PACKAGE_MANAGER.md](PACKAGE_MANAGER.md).)
- Server will start on http://localhost:8000
- Keep this terminal open
@@ -81,7 +85,9 @@ await ethereum.request({
**Optional:** You can pass `networksUrl` instead of (or without) `apiBaseUrl` to fetch networks from a JSON URL (e.g. GitHub raw):
```javascript
params: { networksUrl: 'https://raw.githubusercontent.com/org/repo/main/networks.json' }
params: {
networksUrl: 'https://raw.githubusercontent.com/org/repo/main/networks.json';
}
```
#### Test `get_chain138_config`
@@ -191,7 +197,7 @@ params: { tokenListUrl: 'https://raw.githubusercontent.com/org/repo/main/token-l
#### Test `get_bridge_routes`
Requires `apiBaseUrl` or `bridgeListUrl`. Returns CCIP bridge routes (WETH9 / WETH10) and Chain 138 bridge addresses.
Requires `apiBaseUrl` or `bridgeListUrl`. Returns bridge routes: CCIP (WETH9/WETH10) and, when configured, Trustless (Lockbox on 138) and Chain 138 bridge addresses.
```javascript
await ethereum.request({
@@ -212,7 +218,7 @@ await ethereum.request({
#### Test `show_bridge_routes`
Requires `apiBaseUrl` or `bridgeListUrl`. Opens a Snap dialog with bridge route summary (WETH9/WETH10 → Ethereum Mainnet).
Requires `apiBaseUrl` or `bridgeListUrl`. Opens a Snap dialog with bridge route summary: CCIP (WETH9/WETH10) and Trustless (Lockbox) → Ethereum Mainnet.
```javascript
await ethereum.request({
@@ -284,7 +290,7 @@ Use this checklist for full manual E2E testing:
1. **Environment**
- [ ] MetaMask Flask installed
- [ ] Snap dev server running: `pnpm run start` (or `yarn start`) in `metamask-integration/chain138-snap`
- [ ] Snap dev server running: `pnpm run start` (or `yarn start`) in the repo root
- [ ] For API-dependent tests: token-aggregation service reachable. Set `apiBaseUrl` to your deployment (e.g. `https://your-token-aggregation-api.com`) or a local/staging URL (e.g. `http://localhost:3000` if running token-aggregation locally).
2. **Install Snap**
@@ -305,7 +311,7 @@ Use this checklist for full manual E2E testing:
4. **Companion site cards**
- [ ] Set `GATSBY_SNAP_API_BASE_URL` in `.env` (copy from `.env.production.dist` and fill) so the site passes apiBaseUrl to the Snap.
- [ ] **Market data:** "Show market data" opens Snap dialog; "Fetch market summary" displays tokens/prices below.
- [ ] **Bridge:** "Show bridge routes" opens Snap dialog with CCIP routes.
- [ ] **Bridge:** "Show bridge routes" opens Snap dialog with CCIP and Trustless routes.
- [ ] **Swap quote:** Enter token In/Out addresses and amount (raw), then "Get quote" shows amountOut; "Show quote in Snap" opens dialog.
---
@@ -343,6 +349,7 @@ Use this checklist for full manual E2E testing:
**Checklist before publishing:**
- [ ] **Thorough test:** Complete [docs/PRE_PUBLISH_TESTING.md](docs/PRE_PUBLISH_TESTING.md) (build, logos/images, all RPC methods, companion site, Send page, production-like, final sign-off).
- [ ] All manual E2E checklist items above completed and passing.
- [ ] Token-aggregation (or your API) deployed and stable; production `apiBaseUrl` known.
- [ ] Snap built with no errors; `prepublishOnly` has run (updates manifest shasum).
@@ -377,7 +384,7 @@ Use this checklist for full manual E2E testing:
- ✅ Token list and token list URL (`get_token_list`, `get_token_list_url`)
- ✅ Market data: `get_market_summary` (tokens with prices), `show_market_data` (dialog)
- ✅ Oracles config (`get_oracles`), dynamic info dialog (`show_dynamic_info`)
- ✅ Bridge routes (`get_bridge_routes`, `show_bridge_routes`) when bridge API is available
- ✅ Bridge routes (`get_bridge_routes`, `show_bridge_routes`) — CCIP and Trustless — when bridge API is available
- ✅ Swap quote (`get_swap_quote`, `show_swap_quote`) when quote API is available
---

View File

@@ -0,0 +1,167 @@
# Chain 138 Snap — Troubleshooting (balance, swap, data)
If **installation fails** (“snap is not on the allowlist”), **main balance or USD value is not showing**, **swap is malfunctioning**, or **historical/data not loading** when using the Chain 138 Snap with MetaMask (including Flask), use this guide.
---
## 1. Installation fails: "The snap is not on the allowlist"
**What you see:** When adding the Snap in MetaMask you get:
- **"Connection failed — Fetching of chain138-snap failed, check your network and try again."**
- **"Cannot install version '0.1.2' of snap 'npm:chain138-snap': The snap is not on the allowlist."**
**Cause:** MetaMask only allows installing Snaps that use **protected permissions** (such as `endowment:rpc` and `endowment:network-access`) if the Snap is on their **allowlist**. The Chain 138 Snap uses those permissions, so it must be allowlisted. The Snap is published on npm and has been **submitted** for allowlisting; until MetaMask/Consensys approves it, standard MetaMask will block installation.
**What you can do:**
1. **Use MetaMask Flask (workaround now)**
[MetaMask Flask](https://metamask.io/flask/) is the development build and **does not enforce the allowlist**. Install Flask, then add the Snap using the same ID: `npm:chain138-snap`. The Snap works the same once installed.
2. **Wait for allowlist approval**
Once the Snap is approved, it will be installable in standard MetaMask. No code changes are required on your side.
3. **Operators:** If you submitted the allowlist form and it has been pending for a long time, you can follow up via the [MetaMask Snaps allowlist process](https://docs.metamask.io/snaps/how-to/get-allowlisted/#1-submit-your-snap). For new versions (e.g. 0.1.3), use the [Snaps Directory Information Update form](https://docs.metamask.io/snaps/how-to/get-allowlisted/#5-update-your-snap) to add the new version. Pre-filled fields for the submission are in [ALLOWLIST_FORM_FIELDS.md](../ALLOWLIST_FORM_FIELDS.md).
---
## 2. Main balance / USD not showing ($0.00, "No conversion rate available")
**What you see:** MetaMask shows **$0.00** or **"No conversion rate available"** for ETH (or other tokens) even when you hold a nonzero balance (e.g. 1,000 ETH).
**Cause:** The **main wallet balance and USD value** are provided by **MetaMask (Consensys)**, not by the Snap. MetaMask gets conversion rates from its own providers (e.g. LavaPack). **Chain 138 (DeFi Oracle Meta Mainnet) is not in that list**, so MetaMask has no price feed for it and shows no USD value.
**This is expected** for custom chains until Consensys or price providers add support.
**What you can do:**
- **Trust the token quantity:** Your onchain balance (e.g. "1,000 ETH") is correct; only the USD conversion is missing in the wallet UI.
- **Use the Snap for prices:** Open the **Snap companion site** (e.g. https://explorer.d-bis.org/snap/) and use **"Show market data"** or **"Fetch market summary"**. Those use the tokenaggregation API (see §5) and can show USD prices for Chain 138 tokens.
- **Longterm:** Submit Chain 138 and tokens to CoinGecko/CMC and/or Consensys so native MetaMask USD may be supported later (see project docs, e.g. `COINGECKO_SUBMISSION_GUIDE`, Consensys outreach).
---
## 3. "We could not fetch any historical data"
**What you see:** In the token/asset detail view, MetaMask shows a raccoon mascot and **"We could not fetch any historical data"** with time range buttons (1D, 1W, 1M, etc.).
**Cause:** Historical price/chart data is also supplied by **MetaMasks portfolio/price providers**, which do not support Chain 138.
**What you can do:** This is a MetaMask UI limitation. Use the Snaps market data (companion site) or external explorers/APIs for historical data on Chain 138.
---
## 4. Swap malfunctioning (inwallet Swap button)
**What you see:** Clicking **Swap** in MetaMask on Chain 138 fails or shows errors (e.g. "No XChain Swaps native asset found for chainId: eip155:138").
**Cause:** MetaMasks **inwallet Swap** feature does **not** support Chain 138. It only supports chains in their native Swaps list.
**What you can do:**
- **Send ETH on Chain 138:** Use the dedicated **Send on Chain 138** page:
https://explorer.d-bis.org/snap/send
It uses `eth_sendTransaction` from the dApp context and works on Chain 138 (see `packages/site/src/pages/send.tsx`).
- **Swap quotes via Snap:** On the Snap companion site, use the **Swap quote** card: enter token In/Out and amount, then **"Get quote"** or **"Show quote in Snap"**. This uses the tokenaggregation API. Executing the actual swap must be done in your dApp or a DEX that supports Chain 138, not via MetaMasks Swap button.
- **WETH display quirk:** If you see wrong WETH balance formatting (e.g. "6,000,000,000.0T WETH"), see [METAMASK_WETH9_DISPLAY_BUG.md](../../docs/METAMASK_WETH9_DISPLAY_BUG.md) (token list / decimals).
---
## 5. Snap market data / swap quote / bridge not loading
**What you see:** On the companion site (e.g. https://explorer.d-bis.org/snap/), **"Show market data"**, **"Fetch market summary"**, **"Get quote"** / **"Show quote in Snap"**, or **bridge routes** show an error or "Set GATSBY_SNAP_API_BASE_URL".
**Cause:** Those features need a **tokenaggregationcompatible API**. The Snap calls:
- `GET {apiBaseUrl}/api/v1/networks`
- `GET {apiBaseUrl}/api/v1/tokens?chainId=138&limit=...`
- `GET {apiBaseUrl}/api/v1/quote?chainId=138&tokenIn=...&tokenOut=...&amountIn=...`
- `GET {apiBaseUrl}/api/v1/bridge/routes`
- etc.
The companion site passes `apiBaseUrl` from **GATSBY_SNAP_API_BASE_URL** at **build time**. If that URL does not serve the tokenaggregation API (see `smom-dbis-138/services/token-aggregation` and [REST_API_REFERENCE.md](../../../smom-dbis-138/services/token-aggregation/docs/REST_API_REFERENCE.md)), those calls fail.
**What you can do:**
1. **If you build with `GATSBY_SNAP_API_BASE_URL=https://explorer.d-bis.org`**
Then **explorer.d-bis.org** must expose the tokenaggregation API. The explorers normal Blockscout/Go APIs do **not** implement `/api/v1/networks`, `/api/v1/tokens`, `/api/v1/quote`, etc. You must either:
- **Deploy the tokenaggregation service** (from `smom-dbis-138/services/token-aggregation`) and **proxy** its routes under `https://explorer.d-bis.org/api/v1/...`. On the explorer VM (VMID 5000), run **`explorer-monorepo/scripts/apply-nginx-token-aggregation-proxy.sh`** (or ensure the nginx config includes a `location /api/v1/` proxy to the tokenaggregation port, e.g. 3000). The script **`fix-nginx-conflicts-vmid5000.sh`** in the same repo already adds this proxy when applied.
- Or use a **separate API host** (see below).
2. **If the tokenaggregation service runs elsewhere** (e.g. `https://api.d-bis.org` or an internal URL):
Build the Snap site with that base URL:
```bash
export GATSBY_SNAP_API_BASE_URL=https://your-token-aggregation-host
bash scripts/build-snap-site-for-explorer.sh
```
Deploy the built `packages/site/public/` to `/var/www/html/snap/` (or your Snap host). Then the companion site will pass the correct `apiBaseUrl` to the Snap and market/swap/bridge can work.
3. **Local development:** Set `GATSBY_SNAP_API_BASE_URL` in `packages/site/.env` to your tokenaggregation base URL (e.g. `http://localhost:3001` if the service runs there). See [FAQ.md](FAQ.md) (“The companion site shows Set GATSBY_SNAP_API_BASE_URL”).
---
## 6. No icons or tokens showing in MetaMask Flask
**What you see:** MetaMask Flask shows no token icons, no chain icons, or no tokens from the token list when viewing Chain 138 or ALL Mainnet.
**Cause:** MetaMask does not auto-load tokens from the Snap. Chain 138 is not in MetaMasks built-in token detection (which only covers Ethereum, Polygon, Arbitrum, etc.). You must add the token list URL manually. Icons come from `logoURI` (per token) and `iconUrls` (per network) in the API responses; if those URLs are missing or unreachable, MetaMask shows no icons.
**What you can do:**
1. **Get and add the token list URL**
- Open the Snap companion site (e.g. https://explorer.d-bis.org/snap/).
- Connect MetaMask Flask and install the Snap.
- Click **"Show dynamic info"** (requires `GATSBY_SNAP_API_BASE_URL` to be set at build time).
- The Snap dialog shows the **Token list URL** (e.g. `https://explorer.d-bis.org/api/v1/report/token-list`).
- Add that URL in MetaMask: **Settings → Security & Privacy → Token list** (or equivalent), if your MetaMask version supports custom token list URLs.
2. **If MetaMask does not support custom token list URLs**
- Add tokens manually: **Tokens** tab → **Import tokens** → **Custom token** → enter the token contract address for Chain 138 (e.g. cUSDC, cUSDT from the explorer or [CHAIN138_TOKEN_ADDRESSES](../../../docs/11-references/CHAIN138_TOKEN_ADDRESSES.md)).
3. **Ensure `apiBaseUrl` is set and reachable**
- The companion site must be built with `GATSBY_SNAP_API_BASE_URL` pointing at the token-aggregation API.
- Verify: `curl "https://explorer.d-bis.org/api/v1/report/token-list?chainId=138"` returns valid JSON with `tokens` and each token has `logoURI`.
4. **Icons not showing**
- Token icons: Each token in the token list must have a valid `logoURI`. The token-aggregation API provides these from `canonical-tokens.ts` (Trust Wallet assets).
- Network icons: `GET /api/v1/networks` must return `iconUrls` for each chain. Each network has a primary URL (e.g. `explorer.d-bis.org/favicon.ico`) and a fallback (Trust Wallet ETH logo). If the primary 404s, MetaMask uses the fallback. Operators: add `favicon.ico` to explorer and alltra sites if missing.
- If logo URLs (e.g. raw.githubusercontent.com) are blocked or return 404, operators can host logos locally and update `canonical-tokens.ts` or `networks.ts`.
**Operators:** See [PRE_PUBLISH_TESTING.md](../PRE_PUBLISH_TESTING.md) §4.3 (token list logoURI) and §4.4 (network iconUrls) for verification steps.
---
## 7. Permissions and connected site
The Snap needs:
- **Access the internet** — to call the tokenaggregation API.
- **Display dialog windows in MetaMask** — for market data, swap quote, bridge dialogs.
- **Allow websites to communicate directly with Chain 138** — so the companion site (e.g. explorer.d-bis.org) can invoke the Snap.
If market/swap work on the companion site but not from another origin, ensure that site is **connected** in MetaMask (Snap settings → Connected sites) and that youre using the same Snap origin (e.g. `npm:chain138-snap`) and correct `apiBaseUrl`.
---
## 8. Quick reference
| Issue | Cause | Action |
| --------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Installation: "snap is not on the allowlist" | Snap not yet approved on MetaMask allowlist | Use [MetaMask Flask](https://metamask.io/flask/) to install now; or wait for allowlist approval |
| No icons or tokens in MetaMask Flask | Token list not added; Chain 138 not in built-in detection | Use "Show dynamic info" to get token list URL; add in MetaMask Settings → Token list; or add tokens manually |
| Main balance USD = $0.00 / no conversion rate | MetaMask has no price feed for Chain 138 | Use Snap “Show market data” or accept quantity-only in wallet |
| Historical data not loading | MetaMask portfolio doesnt support Chain 138 | Use Snap/explorer or external APIs |
| Inwallet Swap fails | MetaMask Swap doesnt support Chain 138 | Use [Send on Chain 138](https://explorer.d-bis.org/snap/send); get swap quotes from Snap companion site and execute swap in dApp/DEX |
| Snap market/swap/bridge errors | `apiBaseUrl` not set or not serving tokenaggregation API | Set GATSBY_SNAP_API_BASE_URL to tokenaggregation host; ensure `/api/v1/...` is available there (or proxy it under explorer.d-bis.org) |
---
**Related docs**
- [TOKEN_LIST_AND_ICONS_GUIDE.md](TOKEN_LIST_AND_ICONS_GUIDE.md) — how to add token list URL and fix icons
- [FAQ.md](FAQ.md) — apiBaseUrl, permissions, “Set GATSBY_SNAP_API_BASE_URL”
- [TESTING_INSTRUCTIONS.md](../TESTING_INSTRUCTIONS.md) — testing market summary, swap quote, bridge
- [METAMASK_WETH9_DISPLAY_BUG.md](../../docs/METAMASK_WETH9_DISPLAY_BUG.md) — WETH balance display
- Tokenaggregation API: `smom-dbis-138/services/token-aggregation/docs/REST_API_REFERENCE.md`

View File

@@ -0,0 +1,62 @@
# Contributing to Chain 138 Snap
Thank you for your interest in contributing. This guide covers setup, testing, and publishing.
## Prerequisites
- **Node.js** 18.6.0 or later (LTS recommended)
- **pnpm** (default) or **Yarn** — see [PACKAGE_MANAGER.md](../PACKAGE_MANAGER.md)
## Setup
```bash
# Clone the repo (or your fork)
git clone https://github.com/bis-innovations/chain138-snap.git
cd chain138-snap
# Install dependencies (pnpm default)
pnpm install
# Optional: enable scripts for packages that need postinstall (e.g. sharp)
pnpm run allow-scripts
```
## Development
- **Start dev server** (Snap + companion site): `pnpm run start` — serves at http://localhost:8000
- **Build:** `pnpm run build` — builds Snap and site
- **Lint:** `pnpm run lint` — ESLint + Prettier (JSON/MD/YAML)
- **Lint fix:** `pnpm run lint:fix`
## Testing
- **Unit tests (Snap):** `pnpm run test` — builds the Snap and runs Jest in packages/snap
- **E2E (Playwright):** `pnpm run test:e2e` — runs Playwright against the companion site (first time: `npx playwright install`)
For full manual E2E (MetaMask Flask, all RPC methods), see [TESTING_INSTRUCTIONS.md](../TESTING_INSTRUCTIONS.md) and [E2E_PREPARATION.md](../E2E_PREPARATION.md).
## Packages with scripts
Scripts are disabled by default (LavaMoat). If you need to run install scripts (e.g. for sharp or gatsby):
```bash
pnpm run allow-scripts
```
Then enable the package in the lavamoat.allowScripts section of the root package.json if required.
## Submitting changes
1. Create a branch, make your changes, and run `pnpm run lint` and `pnpm run test`.
2. Open a pull request against main.
3. Ensure CI passes (build, lint, tests, and any security workflows).
## Publishing (maintainers)
- **Push to GitHub:** See [PUSH_AND_PUBLISH.md](../PUSH_AND_PUBLISH.md) (subtree push from parent repo if applicable).
- **Publish to npm:** From this repo root, `pnpm run publish:snap` (requires NPM_ACCESS_TOKEN in .env for 2FA bypass; see .env.example).
- **Allowlist:** After publishing a new version, submit an update via the MetaMask Snaps Directory Information Update form if the Snap is already allowlisted.
## Code of conduct
Be respectful and constructive. Report security issues privately (see [SECURITY.md](../SECURITY.md)).

View File

@@ -0,0 +1,65 @@
# Deploy the companion site
The companion site (`packages/site`) is a Gatsby app that lets users connect to and test the Snap. You can build it and deploy it to any static host (e.g. GitHub Pages, Netlify, or your own server).
## Build
From the repo root:
```bash
# Build with path prefix (e.g. /snap if served at https://yoursite.com/snap/)
GATSBY_PATH_PREFIX=/snap pnpm --filter site run build
# With production API URL (so Market data, Bridge, Swap cards work)
GATSBY_SNAP_API_BASE_URL=https://your-token-aggregation-api.com GATSBY_PATH_PREFIX=/snap pnpm --filter site run build
```
- Output is in `packages/site/public/`. Asset paths are prefixed by `GATSBY_PATH_PREFIX` (e.g. `/snap/`).
- Use `GATSBY_PATH_PREFIX=/` if the site is served at the root of your domain.
## Deploy to your host
1. **Upload** the contents of `packages/site/public/` to your web server (e.g. `/var/www/html/snap/` or your CDN).
2. **Web server:** Ensure the server serves the SPA correctly:
- For a path like `/snap/`, map `/snap` and `/snap/*` to the built files and use a fallback to `/snap/index.html` for client-side routing.
- Example (nginx):
```nginx
location /snap/ {
alias /var/www/html/snap/;
try_files $uri $uri/ /snap/index.html;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
```
3. **HTTPS:** Serve the site over HTTPS in production (see [SECURITY.md](../SECURITY.md)).
## Environment variables (build time)
| Variable | Description |
| -------------------------- | --------------------------------------------------------------------------------------------------- |
| `GATSBY_PATH_PREFIX` | Path prefix (e.g. `/snap`). Default: none (root). |
| `GATSBY_SNAP_API_BASE_URL` | Token-aggregation API base URL passed to the Snap for market data, bridge, swap. No trailing slash. |
| `GATSBY_SNAP_SITE_URL` | Public origin of the companion site (e.g. `https://explorer.d-bis.org`). When set, the "Send on Chain 138" link is absolute HTTPS so it never redirects to HTTP. |
| `GATSBY_BUILD_SHA` | Optional; written to `version.json` for display. |
Set these when running the build; they are baked into the static output.
## Verification
After deploy, open your site URL and confirm:
- The Snap Connect UI loads.
- Connecting with MetaMask (or MetaMask Flask) installs the Snap.
- If you set `GATSBY_SNAP_API_BASE_URL`, the Market data, Bridge, and Swap quote cards work when the API is reachable.
## Optional: validate token/list URLs
If you use external JSON URLs for networks, token list, or bridge list, you can validate them before deploy:
```bash
./scripts/validate-token-lists.sh [URL1] [URL2] ...
# Or set TOKEN_LIST_JSON_URL, BRIDGE_LIST_JSON_URL, NETWORKS_JSON_URL
```
See script help for details.

123
chain138-snap/docs/FAQ.md Normal file
View File

@@ -0,0 +1,123 @@
# Chain 138 Snap — FAQ
Frequently asked questions about the Chain 138 Snap (MetaMask).
---
## General
### What is the Chain 138 Snap?
A MetaMask Snap that adds **Chain 138** (DeFi Oracle Meta Mainnet) and **ALL Mainnet** (651940) support inside MetaMask: network parameters, token list, market data (USD prices), swap quotes, and bridge routes (CCIP and Trustless). It uses a token-aggregation (or compatible) API that you configure.
### Why use this Snap?
MetaMask supports Chain 138 as a custom EVM network, but native **Swaps**, **Portfolio Bridge**, and **USD pricing** do not include Chain 138. This Snap provides in-wallet swap quotes, bridge routes, and market data by calling your API, so users get feature parity without waiting for upstream support.
### What is the Snap ID?
**Production:** `npm:chain138-snap` (after installing from npm or the MetaMask Snaps Directory).
**Development:** `local:http://localhost:8000` when running the dev server.
---
## Installation and usage
### How do I install the Snap?
1. Install [MetaMask](https://metamask.io/) (or [MetaMask Flask](https://metamask.io/flask/) for development).
2. From a dApp or the companion site, connect and add the Snap using the ID `npm:chain138-snap`.
### Do I need MetaMask Flask?
- **Production:** No. Once the Snap is allowlisted, it installs in standard MetaMask.
- **Development:** Yes, for testing before allowlisting. Use [MetaMask Flask](https://metamask.io/flask/).
### Why do market data, swap quote, and bridge routes not work?
Those features require the dApp to pass **`apiBaseUrl`** (your token-aggregation service base URL) when invoking the Snap. Without it, the Snap cannot fetch data. See [INTEGRATORS.md](../INTEGRATORS.md).
### Can I use my own API or JSON URLs?
Yes. You can pass:
- **`apiBaseUrl`** — base URL of a token-aggregationcompatible API (networks, token list, bridge, quote endpoints).
- **`networksUrl`** — direct URL to a networks JSON (overrides API for networks).
- **`tokenListUrl`** — direct URL to a token list JSON.
- **`bridgeListUrl`** — direct URL to a bridge routes JSON.
See [INTEGRATORS.md](../INTEGRATORS.md) and [TESTING_INSTRUCTIONS.md](../TESTING_INSTRUCTIONS.md).
---
## Development
### How do I run the Snap locally?
From the repo root: `pnpm run start`. The companion site and Snap are served at http://localhost:8000. Use MetaMask Flask and connect to that URL.
### How do I run tests?
- **Unit (Jest):** `pnpm run test`
- **E2E (Playwright):** `pnpm run test:e2e` (run `npx playwright install` once)
See [TESTING_INSTRUCTIONS.md](../TESTING_INSTRUCTIONS.md) for full manual E2E.
### The companion site shows "Set GATSBY_SNAP_API_BASE_URL"
Set `GATSBY_SNAP_API_BASE_URL` in `packages/site/.env` (copy from `.env.production.dist`) to your token-aggregation API base URL so the site can pass `apiBaseUrl` to the Snap. Restart the dev server after changing env.
### How do I publish a new version?
1. Bump version in `packages/snap/package.json`.
2. From repo root: `pnpm run build` then `pnpm run publish:snap` (see [PUSH_AND_PUBLISH.md](../PUSH_AND_PUBLISH.md)).
3. Push to GitHub. If the Snap is allowlisted, submit a version update via the [MetaMask update form](https://docs.metamask.io/snaps/how-to/get-allowlisted/#5-update-your-snap).
---
## Permissions and security
### What permissions does the Snap use?
- **snap_dialog** — show dialogs (e.g. bridge routes, market data).
- **endowment:rpc** (dapps: true) — handle RPC from dApps.
- **endowment:network-access** — fetch data from the configured API/URLs.
No key-management or account APIs are used. See [SECURITY.md](../SECURITY.md) and `packages/snap/snap.manifest.json`.
### Is an audit required for allowlisting?
No. The Snap does not use key-management APIs, so a third-party audit is not required for the MetaMask Snaps Directory. See [ALLOWLIST_SOURCE_AND_COMPLIANCE_CHECKLIST.md](../ALLOWLIST_SOURCE_AND_COMPLIANCE_CHECKLIST.md).
---
## RPC methods
### What RPC methods are available?
See the table in [packages/snap/README.md](../packages/snap/README.md) or [TESTING_INSTRUCTIONS.md](../TESTING_INSTRUCTIONS.md). Summary: `hello`, `get_networks`, `get_chain138_config`, `get_chain138_market_chains`, `get_token_list`, `get_token_list_url`, `get_oracles`, `show_dynamic_info`, `get_market_summary`, `show_market_data`, `get_bridge_routes`, `show_bridge_routes`, `get_swap_quote`, `show_swap_quote`.
### How do I call the Snap from my dApp?
Use `wallet_requestSnaps` to install and `wallet_invokeSnap` to call methods. Example in [INTEGRATORS.md](../INTEGRATORS.md) and [packages/snap/README.md](../packages/snap/README.md).
---
## Troubleshooting
### Snap not appearing in MetaMask Flask
- Ensure the dev server is running on port 8000 and you opened http://localhost:8000.
- Check the browser console for errors and refresh the page.
### API calls failing (CORS, 404)
- Ensure your token-aggregation API allows the Snap/site origin in CORS.
- Verify `apiBaseUrl` is correct (no trailing slash) and the endpoints (e.g. `/api/v1/networks`, `/api/v1/report/token-list`) exist and return valid JSON.
### Permission errors
- Confirm `snap.manifest.json` includes `endowment:network-access` if you call APIs. Reinstall the Snap after changing the manifest.
For more, see the **Troubleshooting** section in [TESTING_INSTRUCTIONS.md](../TESTING_INSTRUCTIONS.md).

View File

@@ -0,0 +1,186 @@
# Chain 138 Snap — Features and RPC methods
This document lists every function and feature of the Snap, with parameters and response shapes. The Snap supports **both** blockchains: **Chain 138** (DeFi Oracle Meta Mainnet) and **ALL Mainnet** (651940).
---
## Blockchains supported
| Chain | Chain ID | Name | Use in Snap |
| --------------- | -------- | ------------------------ | ----------------------------------------------------- |
| **Chain 138** | 138 | DeFi Oracle Meta Mainnet | Primary; networks, config, market, swap, bridge |
| **ALL Mainnet** | 651940 | ALL Mainnet | Supported in networks, token list, optional `chainId` |
All methods that accept an optional `chainId` default to **138** when omitted.
---
## Feature overview (visual)
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Chain 138 Snap (npm:chain138-snap) │
├─────────────────────────────────────────────────────────────────────────┤
│ Networks & config │ get_networks, get_chain138_config, │
│ (Chain 138 + ALL) │ get_chain138_market_chains │
├─────────────────────────────────────────────────────────────────────────┤
│ Token list │ get_token_list, get_token_list_url │
│ (optional chainId) │ (chainId: 138 or 651940) │
├─────────────────────────────────────────────────────────────────────────┤
│ Market data │ get_market_summary, show_market_data │
│ (USD prices) │ (dialog: token symbols + prices) │
├─────────────────────────────────────────────────────────────────────────┤
│ Bridge routes │ get_bridge_routes, show_bridge_routes │
│ (CCIP + Trustless) │ (dialog: CCIP WETH9/WETH10 + Trustless Lockbox)│
├─────────────────────────────────────────────────────────────────────────┤
│ Swap quote │ get_swap_quote, show_swap_quote │
│ (Chain 138) │ (tokenIn, tokenOut, amountIn → amountOut) │
├─────────────────────────────────────────────────────────────────────────┤
│ Oracles & dynamic │ get_oracles, show_dynamic_info │
│ (API config) │ (dialog: networks + token list URL) │
├─────────────────────────────────────────────────────────────────────────┤
│ Test │ hello (returns greeting) │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## RPC method matrix
| Method | Chain 138 | ALL (651940) | Requires apiBaseUrl / URL param | Shows dialog (UI) |
| ---------------------------- | --------- | ------------ | ---------------------------------------- | ------------------ |
| `hello` | — | — | No | Yes (confirmation) |
| `get_networks` | ✅ | ✅ | apiBaseUrl or networksUrl | No |
| `get_chain138_config` | ✅ | — | apiBaseUrl or networksUrl | No |
| `get_chain138_market_chains` | ✅ | — | apiBaseUrl | No |
| `get_token_list` | ✅ | ✅ | apiBaseUrl or tokenListUrl | No |
| `get_token_list_url` | ✅ | ✅ | apiBaseUrl or tokenListUrl | No |
| `get_oracles` | ✅ | — | apiBaseUrl | No |
| `show_dynamic_info` | ✅ | ✅ | apiBaseUrl or networksUrl/tokenListUrl | **Yes** |
| `get_market_summary` | ✅ | ✅ | apiBaseUrl | No |
| `show_market_data` | ✅ | ✅ | apiBaseUrl | **Yes** |
| `get_bridge_routes` | ✅ | — | apiBaseUrl or bridgeListUrl | No |
| `show_bridge_routes` | ✅ | — | apiBaseUrl or bridgeListUrl | **Yes** |
| `get_swap_quote` | ✅ | — | apiBaseUrl + tokenIn, tokenOut, amountIn | No |
| `show_swap_quote` | ✅ | — | apiBaseUrl + tokenIn, tokenOut, amountIn | **Yes** |
---
## Method reference by category
### Test
| Method | Params | Response | UI |
| ------- | ------ | ------------------------------ | ------------------- |
| `hello` | — | `"Hello from Chain 138 Snap!"` | Confirmation dialog |
---
### Networks and chain config
| Method | Params | Response shape |
| ---------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------ |
| `get_networks` | `apiBaseUrl` or `networksUrl` | `{ version?, networks: EIP-3085[] }` |
| `get_chain138_config` | `apiBaseUrl` or `networksUrl` | Chain 138 params (chainId, chainName, rpcUrls, nativeCurrency, blockExplorerUrls, oracles) |
| `get_chain138_market_chains` | `apiBaseUrl` | `[{ chainId, name, nativeToken, rpcUrl, explorerUrl }]` |
**Visual (dialog):** None for get\_\*; use `show_dynamic_info` for an in-Snap dialog with networks and token list URL.
---
### Token list
| Method | Params | Response shape |
| -------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------- |
| `get_token_list` | `apiBaseUrl` or `tokenListUrl`, optional `chainId` (138 \| 651940) | `{ tokens: [{ symbol, name, address, ... }] }` or API shape |
| `get_token_list_url` | Same | URL string or object with token list URL |
---
### Oracles and dynamic info
| Method | Params | Response / UI |
| ------------------- | ---------------------------------------------- | ----------------------------------------------- |
| `get_oracles` | `apiBaseUrl` | Oracles config from API |
| `show_dynamic_info` | `apiBaseUrl` or `networksUrl` / `tokenListUrl` | **Dialog:** networks summary and token list URL |
---
### Market data (USD prices)
| Method | Params | Response shape | UI |
| -------------------- | ---------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `get_market_summary` | `apiBaseUrl`, optional `chainId` (default 138) | `{ tokens: [{ symbol, name, address, market?: { priceUsd, volume24h } }] }` | No |
| `show_market_data` | Same | — | **Dialog:** "Market data (Chain 138)" with token symbols and USD prices |
---
### Bridge routes (CCIP + Trustless)
| Method | Params | Response shape | UI |
| -------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `get_bridge_routes` | `apiBaseUrl` or `bridgeListUrl` | `{ routes: { weth9?, weth10?, trustless? }, chain138Bridges: { weth9?, weth10?, trustless? } }` | No |
| `show_bridge_routes` | Same | — | **Dialog:** CCIP (WETH9/WETH10) and Trustless (Lockbox on 138) routes to Ethereum Mainnet |
The API or `bridgeListUrl` JSON may include:
- **CCIP:** `routes.weth9`, `routes.weth10`, `chain138Bridges.weth9`, `chain138Bridges.weth10`.
- **Trustless:** `chain138Bridges.trustless` (Lockbox on Chain 138), optional `routes.trustless['Ethereum Mainnet (1)']` (Ethereum-side contract).
---
### Swap quote
| Method | Params | Response shape | UI |
| ----------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------- | -------------------------------- |
| `get_swap_quote` | `apiBaseUrl`, `tokenIn`, `tokenOut`, `amountIn` (raw string), optional `chainId` (default 138) | `{ amountOut?, error?, poolAddress? }` | No |
| `show_swap_quote` | Same | — | **Dialog:** In/Out amounts (raw) |
---
## Request flow (high level)
```mermaid
flowchart LR
subgraph dApp
A[dApp / Companion site]
end
subgraph Snap
B[Chain 138 Snap]
end
subgraph API
C[Token-aggregation API]
end
A -->|wallet_invokeSnap + apiBaseUrl / URL params| B
B -->|GET /api/v1/networks etc.| C
C -->|JSON| B
B -->|result or snap_dialog| A
```
---
## Optional URL overrides
Instead of (or in addition to) `apiBaseUrl`, you can pass:
| Param | Used by methods |
| --------------- | ----------------------------------------- |
| `networksUrl` | `get_networks`, `get_chain138_config` |
| `tokenListUrl` | `get_token_list`, `get_token_list_url` |
| `bridgeListUrl` | `get_bridge_routes`, `show_bridge_routes` |
---
## Screenshots and visuals (for maintainers)
To make the docs more visual, add screenshots under **`docs/images/`** and link them here and in the README. Suggested captures:
| Screenshot | Description |
| -------------------------- | ---------------------------------------------------------------- |
| `connect.png` | Companion site Connect button and MetaMask install prompt |
| `market-data-dialog.png` | Snap dialog from `show_market_data` (Chain 138 tokens + prices) |
| `bridge-routes-dialog.png` | Snap dialog from `show_bridge_routes` (CCIP + Trustless routes) |
| `swap-quote-dialog.png` | Snap dialog from `show_swap_quote` |
| `dynamic-info-dialog.png` | Snap dialog from `show_dynamic_info` (networks + token list URL) |
After adding images, link them in this section and in [README.md](../README.md).

View File

@@ -0,0 +1,253 @@
# Thorough Pre-Publish Testing Guide — Chain 138 Snap
Use this guide to **thoroughly test the Snap before every npm publish** and before submitting or updating the Snap in the MetaMask directory. It covers build, assets (logos/images), every RPC method, companion site, Send page, production-like flows, and recommendations. No details (including chain/token logos) are left out.
---
## 1. Overview and when to use
- **Purpose:** Ensure the Snap and companion site work end-to-end, all assets (icons, logos) are present and reachable, and the package is ready for npm and (optionally) the MetaMask directory.
- **When:** Before each `pnpm run publish:snap` and before submitting or updating the Snap via the [MetaMask Snaps Directory Information form](https://docs.metamask.io/snaps/how-to/get-allowlisted/#5-update-your-snap).
- **Scope:** Build, unit tests, Snap package contents, **all logos and images**, every RPC method, companion site cards, Send page, production-like test, allowlist checklist, and recommendations.
---
## 2. Prerequisites
### 2.1 MetaMask Flask (for full manual E2E)
- Install: https://metamask.io/flask/
- Use a **separate** browser profile or the Flask build as a separate extension so it does not conflict with regular MetaMask.
- Ensure Flask is unlocked and (optionally) create or import a test wallet you can use on Chain 138.
### 2.2 Token-aggregation API (for market data, swap quote, bridge, token list)
- **Local:** See [E2E_PREPARATION.md](../E2E_PREPARATION.md). Run from `smom-dbis-138/services/token-aggregation` (Node 20+, PostgreSQL if required). Default port **3000**. Verify: `curl http://localhost:3000/api/v1/networks` and `curl http://localhost:3000/api/v1/report/token-list?chainId=138`.
- **Staging/production:** Use a deployed base URL. Ensure CORS allows the Snap/site origin and all required endpoints respond (see E2E_PREPARATION.md §1). Run **`scripts/verify-snap-api-and-icons.sh [API_BASE_URL]`** to verify token list, networks, logoURIs, and iconUrls.
### 2.3 Companion site environment
- In `packages/site`: copy `.env.production.dist` to `.env` (dev) or `.env.production` (production build).
- Set **`GATSBY_SNAP_API_BASE_URL`** to the token-aggregation base URL (e.g. `http://localhost:3000` or `https://explorer.d-bis.org`). No trailing slash.
- For **production-like** test: set **`GATSBY_SNAP_ORIGIN=npm:chain138-snap`** so the site uses the published Snap (after it is on npm).
- Restart the dev server after changing env so Gatsby picks up the variables.
### 2.4 Optional but recommended
- A **real wallet** with a small balance on **Chain 138** (e.g. testnet ETH) to verify Send page and in-wallet display.
- **Deployed companion site** (e.g. https://explorer.d-bis.org/snap/) so you can test from a production URL and from a different origin (MetaMask “Connected sites”).
---
## 3. Build and unit tests
| Step | Command | What to verify |
| ------------------------ | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Install deps | `pnpm install` | No peer/version errors. |
| Build | `pnpm run build` | Both `packages/snap` and `packages/site` build successfully. Snap manifest shasum is updated (mm-snap may report “fixed”). |
| Unit tests | `pnpm run test` | All Jest tests pass (Snap package). |
| Automated E2E (optional) | `npx playwright install` once, then `pnpm run test:e2e` | Playwright tests pass (site loads, Connect/Reconnect/Install visible). Does not drive MetaMask. |
| Lint | `pnpm run lint` | No ESLint/Prettier errors. |
**Snap package contents (must be present for publish):**
- [ ] `packages/snap/dist/bundle.js` — exists and non-empty.
- [ ] `packages/snap/images/icon.svg` — exists (referenced in `snap.manifest.json` as `iconPath`).
- [ ] `packages/snap/snap.manifest.json``version` matches `packages/snap/package.json`, `source.location.npm.packageName` is `chain138-snap`, `iconPath` is `images/icon.svg`.
- [ ] `packages/snap/package.json``files` includes `dist/`, `images/`, `snap.manifest.json`; `publishConfig.access` is `public` if publishing as public.
---
## 4. Logos and images — complete checklist
Nothing should be missing or broken. Verify each of the following.
### 4.1 Snap icon (MetaMask Snap list and detail)
| Item | Location | Check |
| ----------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Snap icon | `packages/snap/images/icon.svg` | File exists; referenced in `snap.manifest.json``source.location.npm.iconPath`. |
| In MetaMask | After installing the Snap | In Settings → Snaps, “Chain 138” shows the purple “138” icon (or your icon). Icon is clear and not pixelated. |
If you use a different format (e.g. PNG), ensure `iconPath` and `files` in package.json include it and the manifest points to the correct path.
### 4.2 Companion site assets
| Item | Location | Check |
| ------------------- | ---------------------------------------------------------------- | -------------------------------------------------------- |
| Site favicon / icon | `packages/site/gatsby-config.ts``icon: 'src/assets/logo.svg'` | File exists; build succeeds; browser tab shows the icon. |
| Header logo | `packages/site/src/components/Header.tsx``<SnapLogo />` | Logo renders in the header; no broken image. |
Ensure `src/assets/logo.svg` (or the path you use) exists. Add a favicon or apple-touch icon in Gatsby config if required for directory/submission.
### 4.3 Token list — logoURI (token-aggregation API)
The Snap and MetaMask token list depend on **`GET /api/v1/report/token-list`** returning Uniswap-style tokens with **`logoURI`** per token and a **list-level `logoURI`**.
| Check | How to verify |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| List-level logoURI | `curl -s "${API_BASE}/api/v1/report/token-list?chainId=138" \| jq '.logoURI'` — present and URL is reachable (e.g. ETH or list logo). |
| Every token has logoURI | `curl -s "${API_BASE}/api/v1/report/token-list?chainId=138" \| jq '.tokens[] \| {symbol, logoURI}'` — no token has null or missing `logoURI`. |
| URLs resolve | For each distinct `logoURI` in the response, open in browser or `curl -sI <url>` — expect 200 (or 301/302 to a valid asset). |
**Token-aggregation source:** In `smom-dbis-138/services/token-aggregation`, `src/config/canonical-tokens.ts` defines `LOGO_BY_SYMBOL` and `getLogoUriForSpec()`. Defaults use Trust Wallet assets and ethereum.org ETH diamond. For Chain 138specific tokens, set `logoUrl` on the spec or ensure they fall back to a sensible default (e.g. ETH_LOGO). Add any new token symbols to `LOGO_BY_SYMBOL` or give them a `logoUrl` so no token is missing a logo.
### 4.4 Network icons (iconUrls) — token-aggregation API
**`GET /api/v1/networks`** must return each network with **`iconUrls`** (array of URLs) so wallets can show chain icons.
| Chain | Expected iconUrls (examples) | Verify |
| ---------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------- |
| Chain 138 (0x8a) | `https://explorer.d-bis.org/favicon.ico`, ETH diamond or similar | Each URL returns 200 (or redirect to valid image). |
| Ethereum Mainnet (0x1) | `https://raw.githubusercontent.com/ethereum/ethereum.org/.../eth-diamond-black.png` | URL reachable. |
| ALL Mainnet (651940) | `https://alltra.global/favicon.ico`, ETH diamond or similar | URL reachable. |
**Token-aggregation source:** `smom-dbis-138/services/token-aggregation/src/config/networks.ts` — each entry in `NETWORKS` has `iconUrls`. Ensure:
- [ ] `explorer.d-bis.org/favicon.ico` exists (explorer site serves a favicon).
- [ ] `alltra.global/favicon.ico` exists (or update to a valid URL).
- [ ] Any raw GitHub or CDN URLs (ethereum.org, Trust Wallet assets) are still valid and not 404.
### 4.5 Suggested screenshot list (for allowlist and docs)
Capture and keep for submission and `docs/images/`:
| File | Content |
| -------------------------- | ----------------------------------------------------------------- |
| `connect.png` | Companion site with Connect / Install Flask / Reconnect visible. |
| `market-data-dialog.png` | Snap dialog from “Show market data” (tokens + prices). |
| `bridge-routes-dialog.png` | Snap dialog from “Show bridge routes” (CCIP + Trustless). |
| `swap-quote-dialog.png` | Snap dialog from “Show quote in Snap”. |
| `dynamic-info-dialog.png` | Snap dialog from `show_dynamic_info` (networks + token list URL). |
| `send-page.png` | Send on Chain 138 page (network switch + send form). |
See [FEATURES.md](FEATURES.md) “Screenshots and visuals” and [ALLOWLIST_FORM_FIELDS.md](../ALLOWLIST_FORM_FIELDS.md) “Images”.
---
## 5. RPC methods — full verification table
Test each method (e.g. via browser console `wallet_invokeSnap` from the companion site origin). Use **local** Snap ID for dev (`local:http://localhost:8000` or the URL your dev server uses) or **npm** for production-like (`npm:chain138-snap`).
| Method | Params (required / optional) | Expected | Verify |
| ---------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `hello` | — | `{ message: "Hello, …" }` | No error; message contains “Chain 138” or similar. |
| `get_networks` | `apiBaseUrl` or `networksUrl` | `{ version, networks: [ … ] }` with EIP-3085 entries for 138, 1, 651940 | Each network has `chainId`, `rpcUrls`, `nativeCurrency`, `blockExplorerUrls`; **iconUrls** present and URLs reachable. |
| `get_chain138_config` | `apiBaseUrl` or `networksUrl` | Single Chain 138 config object | Matches your chain (e.g. DeFi Oracle Meta Mainnet, RPCs, explorer). |
| `get_chain138_market_chains` | `apiBaseUrl` | `{ chains: [ … ] }` | At least Chain 138; names and explorer URLs correct. |
| `get_token_list_url` | `apiBaseUrl` or `tokenListUrl` | `{ tokenListUrl, description }` | URL is your APIs token-list endpoint (e.g. `…/api/v1/report/token-list`). |
| `get_token_list` | `apiBaseUrl` or `tokenListUrl`; optional `chainId` | `{ tokens: [ … ] }` (Uniswap-style) | Each token has **logoURI**; list usable as MetaMask token list. |
| `get_oracles` | `apiBaseUrl`; optional `chainId` | `{ version, chains: [ … oracles ] }` | Oracles for 138 (and others) if configured. |
| `show_dynamic_info` | `apiBaseUrl` or `networksUrl` / `tokenListUrl` | MetaMask dialog | Dialog shows networks and token list URL; no “pass apiBaseUrl” error. |
| `get_market_summary` | `apiBaseUrl`; optional `chainId` | `{ tokens: [ { symbol, name, address, market?: { priceUsd, volume24h } } ] }` | Tokens and optional prices; no error or empty when API is up. |
| `show_market_data` | `apiBaseUrl`; optional `chainId` | MetaMask dialog | “Market data (Chain 138)” (or chosen chain) with token symbols and prices. |
| `get_bridge_routes` | `apiBaseUrl` or `bridgeListUrl` | `{ routes, chain138Bridges?, … }` | CCIP (WETH9/WETH10) and, if configured, Trustless routes. |
| `show_bridge_routes` | `apiBaseUrl` or `bridgeListUrl` | MetaMask dialog | Dialog lists bridge routes. |
| `get_swap_quote` | `apiBaseUrl`, `tokenIn`, `tokenOut`, `amountIn`; optional `chainId` | `{ amountOut?, error?, poolAddress? }` | When pool exists: `amountOut` present; when not: `error` or null amountOut. |
| `show_swap_quote` | Same as `get_swap_quote` | MetaMask dialog | Dialog shows quote or “no quote” message. |
**Error cases to test:**
- [ ] `get_networks` without `apiBaseUrl` and without `networksUrl`: returns error message asking for params.
- [ ] `get_market_summary` with invalid or down API: returns `{ error, tokens: [] }` or similar; no uncaught exception.
- [ ] `show_*` methods without `apiBaseUrl` (when required): dialog or alert asks user to pass `apiBaseUrl`.
---
## 6. Companion site — every card and page
### 6.1 Home page (index)
| Element | Check |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Connect / Install Flask / Reconnect | Correct button or link visible; clicking triggers MetaMask. |
| “Add Chain 138” / network card | If present, invoking Snap to add network works. |
| **Market data** card | “Show market data” opens Snap dialog; “Fetch market summary” shows tokens/prices below (when `GATSBY_SNAP_API_BASE_URL` is set). |
| **Bridge** card | “Show bridge routes” opens Snap dialog with routes. |
| **Swap quote** card | Enter token In/Out (addresses) and amount; “Get quote” shows amountOut (or error); “Show quote in Snap” opens dialog. |
| Notice / Send link | Link to “Send on Chain 138” is present and correct (e.g. `./send` or `/send`). |
| Footer | Version or build info if configured; no broken links. |
If `GATSBY_SNAP_API_BASE_URL` is not set, Market/Bridge/Swap cards should show a clear message (e.g. “Set GATSBY_SNAP_API_BASE_URL”) instead of failing silently.
### 6.2 Send page (`/send`)
| Step | Check |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Navigate | Open `/send` (or path under your pathPrefix, e.g. `/snap/send`). Page loads. |
| “Switch to Chain 138” | Click; MetaMask prompts to add/switch network; after approval, message confirms “Switched to Chain 138.” |
| Send form | Enter recipient address and amount. |
| Send transaction | Submit; MetaMask shows tx confirmation; after approval, tx is sent on Chain 138 and message or link to explorer is shown. |
| Back link | “Back to Chain 138 Snap” (or similar) returns to home. |
Use a test wallet with a small balance on Chain 138 to avoid real funds at risk.
---
## 7. Production-like test
Before publishing (or after publishing a version you intend to allowlist):
1. **Publish to npm** (or use the latest published version): `pnpm run publish:snap` (see [PUSH_AND_PUBLISH.md](../PUSH_AND_PUBLISH.md)).
2. **Build companion site with npm Snap:** Set `GATSBY_SNAP_ORIGIN=npm:chain138-snap` and `GATSBY_SNAP_API_BASE_URL` to your **production** token-aggregation URL (e.g. `https://explorer.d-bis.org` if API is proxied there). Build and serve (or deploy).
3. **Clean MetaMask Flask:** Remove the Snap if it was installed from local; optionally use a fresh profile.
4. **Install from production site:** Open the deployed companion site (e.g. https://explorer.d-bis.org/snap/). Connect and install the Snap — it should install from **npm** (no localhost). Verify Snap appears in Settings → Snaps as “Chain 138” with correct icon.
5. **Run through:** Repeat the RPC checks and companion site cards using the **production** API. Confirm market data, bridge, and swap quote work when the production API is up.
6. **Connected site:** In Snap settings, confirm the companion site origin is listed under “Connected sites” and can invoke the Snap.
---
## 8. Allowlist and directory submission
If you are submitting or updating the Snap in the MetaMask directory:
- [ ] Use [ALLOWLIST_FORM_FIELDS.md](../ALLOWLIST_FORM_FIELDS.md) for exact field values (Snap name, repo, npm, **version**).
- [ ] **Version:** Must match `packages/snap/package.json` and `snap.manifest.json` (e.g. 0.1.2).
- [ ] **Images:** Upload screenshots as required (companion site, Snap dialogs — see §4.5).
- [ ] **Demo video (optional):** Short walkthrough of install and use (Connect, add network, market data, bridge, swap quote, Send page).
- [ ] **Compliance:** [ALLOWLIST_SOURCE_AND_COMPLIANCE_CHECKLIST.md](../ALLOWLIST_SOURCE_AND_COMPLIANCE_CHECKLIST.md) — repo public, no key-management APIs, etc.
---
## 9. Recommendations and suggestions
- **Snapper / security:** If available, run [Snapper](https://docs.metamask.io/snaps/how-to/get-allowlisted/) (or the MetaMask security scanner) locally before publish.
- **Real wallet on Chain 138:** Test with a wallet that has a small balance on Chain 138 so in-wallet balance and the Send page reflect real behavior (and so you can verify token list and network icons in MetaMask).
- **Deployed companion site:** Test the full flow from the **deployed** companion site (e.g. https://explorer.d-bis.org/snap/) and from a different origin to confirm CORS and “Connected sites” behavior.
- **API health:** Before a release, confirm token-aggregation (or your API) is up and that `/api/v1/networks`, `/api/v1/report/token-list`, `/api/v1/bridge/routes`, `/api/v1/quote`, and `/api/v1/tokens` respond as expected. Document any env (e.g. `NETWORKS_JSON_URL`, `TOKEN_LIST_JSON_URL`) so operators can run the same checks.
- **Changelog / version:** Keep a short changelog (e.g. in README or CHANGELOG.md) and bump version deliberately; note any breaking changes for integrators (e.g. `apiBaseUrl` requirement).
- **Token list and logos:** When adding new tokens to the token-aggregation canonical list, always set **logoURI** (or `logoUrl` in spec) so MetaMask and the Snap never show a token without a logo.
- **Network icons:** When adding or changing chains in token-aggregation `networks.ts`, always set **iconUrls** and ensure URLs are stable and reachable (favicon or official chain logo).
---
## 10. Final sign-off checklist (before publish)
Use this as a single pass/fail before `pnpm run publish:snap`.
**Automatable (run `bash scripts/verify-pre-publish.sh`):** Build, unit tests, package contents, manifest/package version, Prettier check. Optional: `pnpm run test:e2e` (set `SKIP_E2E=1` to skip).
- [ ] **Build:** `pnpm run build` succeeds; manifest shasum is correct.
- [ ] **Tests:** `pnpm run test` passes; `pnpm run lint:misc --check` (Prettier) passes. *(Full `pnpm run lint` includes ESLint; repo may have existing ESLint rule warnings.)*
- [ ] **Snap package:** `dist/bundle.js`, `images/icon.svg`, `snap.manifest.json` present; `package.json` version and `files` correct.
- [ ] **Snap icon:** Icon displays correctly in MetaMask Snap list/detail.
- [ ] **Token list:** Every token from `/api/v1/report/token-list` has a valid **logoURI**; list-level logoURI set.
- [ ] **Networks:** Each network from `/api/v1/networks` has **iconUrls**; all URLs reachable.
- [ ] **RPC methods:** All methods in §5 tested; success and error paths as expected.
- [ ] **Companion site:** All cards (Market, Bridge, Swap) and Send page tested; links and messages correct.
- [ ] **Production-like:** Installed from npm and tested with production API (if applicable).
- [ ] **Allowlist:** If submitting/updating directory, form fields and screenshots ready (§8).
When all items are checked, the Snap is ready for publish and (optionally) directory submission.
---
**Related docs**
- **Script:** `scripts/verify-pre-publish.sh` — runs build, test, package contents, version check, Prettier. Use before publish.
- [TESTING_INSTRUCTIONS.md](../TESTING_INSTRUCTIONS.md) — RPC examples and E2E checklist.
- [E2E_PREPARATION.md](../E2E_PREPARATION.md) — Token-aggregation and env setup.
- [MANUAL_E2E_CHECKLIST.md](../MANUAL_E2E_CHECKLIST.md) — Short manual checklist.
- [CHAIN138_SNAP_TROUBLESHOOTING.md](CHAIN138_SNAP_TROUBLESHOOTING.md) — Balance, swap, and API issues.
- [PUSH_AND_PUBLISH.md](../PUSH_AND_PUBLISH.md) — Version bump and npm publish.
- [ALLOWLIST_FORM_FIELDS.md](../ALLOWLIST_FORM_FIELDS.md) — Directory form and images.

View File

@@ -0,0 +1,38 @@
# Chain 138 Snap — Documentation index
Documentation for the **Chain 138 Snap** (MetaMask): network params, token list, market data, swap quotes, and CCIP and Trustless bridge routes for Chain 138 and ALL Mainnet.
## Quick links
| Doc | Description |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| [FEATURES.md](FEATURES.md) | **All RPC methods and features** — method matrix, params, response shapes, blockchains (138 + 651940), flow diagram |
| [CONTRIBUTING.md](CONTRIBUTING.md) | How to contribute: setup, testing, linting, publishing |
| [FAQ.md](FAQ.md) | Frequently asked questions about the Snap |
| [DEPLOY_COMPANION_SITE.md](DEPLOY_COMPANION_SITE.md) | Build and deploy the companion site to your own host |
| [RUNBOOK.md](RUNBOOK.md) | Build, test, publish quick reference |
## Root-level docs (repo root)
| Doc | Description |
| -------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| [README](../README.md) | Project overview, getting started |
| [INTEGRATORS](../INTEGRATORS.md) | Integrator guide: Snap ID, apiBaseUrl, RPC methods |
| [TESTING_INSTRUCTIONS](../TESTING_INSTRUCTIONS.md) | Full testing guide: dev server, RPC examples, E2E checklist |
| [E2E_PREPARATION](../E2E_PREPARATION.md) | Token-aggregation and companion site setup for E2E |
| [MANUAL_E2E_CHECKLIST](../MANUAL_E2E_CHECKLIST.md) | Short manual E2E checklist |
| [PUSH_AND_PUBLISH](../PUSH_AND_PUBLISH.md) | Push to GitHub, publish to npm, allowlist |
| [PACKAGE_MANAGER](../PACKAGE_MANAGER.md) | pnpm vs Yarn |
| [SECURITY](../SECURITY.md) | Security notes: HTTPS, API, permissions |
| [ALLOWLIST_FORM_FIELDS](../ALLOWLIST_FORM_FIELDS.md) | MetaMask allowlist form fields |
| [ALLOWLIST_SOURCE_AND_COMPLIANCE_CHECKLIST](../ALLOWLIST_SOURCE_AND_COMPLIANCE_CHECKLIST.md) | Allowlist compliance checklist |
| [NEXT_STEPS](../NEXT_STEPS.md) | Completed items and future releases |
## Visual elements
- **Feature overview and method matrix:** [FEATURES.md](FEATURES.md) includes ASCII feature overview, RPC method matrix (all methods × Chain 138 / ALL Mainnet × params × UI), and a Mermaid request-flow diagram.
- **Screenshots:** Optional screenshots (Connect UI, market data dialog, bridge dialog, swap quote dialog) can be added under [docs/images/](images/). See the "Screenshots and visuals" section in [FEATURES.md](FEATURES.md) for suggested filenames.
## Snap package (npm)
The published Snap package has its own README on npm; a copy lives in [packages/snap/README.md](../packages/snap/README.md).

View File

@@ -0,0 +1,45 @@
# Chain 138 Snap — Runbook
Quick reference for building, testing, and publishing the Snap and companion site.
## Build
```bash
# Snap + site
pnpm run build
# Companion site only (e.g. for deploy)
GATSBY_PATH_PREFIX=/snap pnpm --filter site run build
# With production API:
# GATSBY_SNAP_API_BASE_URL=https://your-token-aggregation-api.com
```
## Test
```bash
pnpm run test # Snap unit tests (Jest)
pnpm run test:e2e # Playwright E2E (companion site)
pnpm run lint # ESLint + Prettier
```
## Publish Snap to npm
From repo root:
```bash
pnpm run build
pnpm run publish:snap # uses NPM_ACCESS_TOKEN from .env if set
```
See [PUSH_AND_PUBLISH.md](../PUSH_AND_PUBLISH.md).
## Deploy companion site
Build with path prefix and API URL, then upload `packages/site/public/` to your static host. See [DEPLOY_COMPANION_SITE.md](DEPLOY_COMPANION_SITE.md).
## Validate token/list URLs
```bash
./scripts/validate-token-lists.sh [URL1] [URL2] ...
# Or set TOKEN_LIST_JSON_URL, BRIDGE_LIST_JSON_URL, NETWORKS_JSON_URL
```

View File

@@ -0,0 +1,67 @@
# Chain 138 Snap — Token List and Icons Guide
How to display tokens and icons in MetaMask when using the Chain 138 Snap.
---
## Why tokens and icons may not show
- **Chain 138 is not in MetaMasks built-in token detection.** MetaMask only auto-detects tokens on Ethereum, Polygon, Arbitrum, Optimism, Base, zkSync, etc. Chain 138 and ALL Mainnet (651940) are custom chains, so you must add the token list or import tokens manually.
- **Icons come from the token list.** Each token needs a `logoURI` and each network needs `iconUrls`. If those URLs are missing or unreachable, MetaMask shows no icons.
---
## Step 1: Get the token list URL
1. Open the Snap companion site (e.g. https://explorer.d-bis.org/snap/).
2. Connect MetaMask (or MetaMask Flask) and install the Chain 138 Snap.
3. Click **"Show dynamic info"**.
4. The Snap dialog shows the **Token list URL** (e.g. `https://explorer.d-bis.org/api/v1/report/token-list`).
---
## Step 2: Add the token list in MetaMask
### If MetaMask supports custom token list URLs
- Go to **Settings → Security & Privacy** (or equivalent).
- Find **Token list** and add the URL from Step 1.
- MetaMask will fetch the list and display tokens with icons when you switch to Chain 138 or ALL Mainnet.
### If MetaMask does not support custom token list URLs
Add tokens manually:
1. Switch to Chain 138 in MetaMask.
2. Go to **Tokens** tab → **Import tokens** (or the plus button).
3. Select **Custom token**.
4. Enter the token contract address (see [CHAIN138_TOKEN_ADDRESSES](../../../docs/11-references/CHAIN138_TOKEN_ADDRESSES.md) for cUSDC, cUSDT, etc.).
5. MetaMask will fill symbol and decimals. Click **Import**.
---
## Step 3: Verify icons
If tokens show but icons are missing:
- **Token icons:** The token-aggregation API (`GET /api/v1/report/token-list`) must return a `logoURI` for each token. Operators: see [PRE_PUBLISH_TESTING.md](PRE_PUBLISH_TESTING.md) §4.3.
- **Network icons:** The networks API (`GET /api/v1/networks`) must return `iconUrls` for each chain. Operators: see [PRE_PUBLISH_TESTING.md](PRE_PUBLISH_TESTING.md) §4.4.
- **Logo URLs:** If logo URLs (e.g. raw.githubusercontent.com) are blocked or return 404, operators can host logos locally and update the token-aggregation config.
---
## Operators: verify API and icons
Run the verification script to check token list, networks, logoURIs, and iconUrls:
```bash
./scripts/verify-snap-api-and-icons.sh [API_BASE_URL]
# Example: ./scripts/verify-snap-api-and-icons.sh https://explorer.d-bis.org
```
---
## Related docs
- [CHAIN138_SNAP_TROUBLESHOOTING.md](CHAIN138_SNAP_TROUBLESHOOTING.md) — §6 No icons or tokens showing
- [PRE_PUBLISH_TESTING.md](PRE_PUBLISH_TESTING.md) — §4.3 Token list logoURI, §4.4 Network iconUrls

View File

@@ -0,0 +1 @@
# Add screenshots here: connect.png, market-data-dialog.png, bridge-routes-dialog.png, swap-quote-dialog.png, dynamic-info-dialog.png (see docs/FEATURES.md)

View File

@@ -4,14 +4,23 @@ test.describe('Chain 138 Snap companion site', () => {
test('loads and shows Connect, Install, or Reconnect', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
// Connect and Reconnect are buttons; Install MetaMask Flask is a link
const connectOrReconnect = page.getByRole('button', { name: /Connect|Reconnect/i });
const installLink = page.getByRole('link', { name: /Install MetaMask Flask/i });
await expect(connectOrReconnect.or(installLink).first()).toBeVisible({ timeout: 30_000 });
const connectOrReconnect = page.getByRole('button', {
name: /Connect|Reconnect/i,
});
const installLink = page.getByRole('link', {
name: /Install MetaMask Flask/i,
});
await expect(connectOrReconnect.or(installLink).first()).toBeVisible({
timeout: 30_000,
});
});
test('page has Snap-related content', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
const body = page.locator('body');
await expect(body).toContainText(/Connect|Chain 138|Get started|Snap|Install|Reconnect/i, { timeout: 30_000 });
await expect(body).toContainText(
/Connect|Chain 138|Get started|Snap|Install|Reconnect/i,
{ timeout: 30_000 },
);
});
});

View File

@@ -23,11 +23,21 @@
"lint:eslint": "eslint . --cache",
"lint:fix": "pnpm run lint:eslint --fix && pnpm run lint:misc --write",
"lint:misc": "prettier '**/*.json' '**/*.md' '**/*.yml' '!.yarnrc.yml' --ignore-path .gitignore --no-error-on-unmatched-pattern",
"publish:snap": "bash scripts/publish-snap-to-npm.sh",
"start": "pnpm -r --parallel run start",
"test": "pnpm --filter chain138-snap run build && pnpm --filter chain138-snap run test",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"publish:snap": "bash scripts/publish-snap-to-npm.sh"
"test:e2e:ui": "playwright test --ui"
},
"resolutions": {
"@sigmacomputing/babel-plugin-lodash>glob": "^7.2.0",
"cookie": "^0.7.1",
"gatsby>glob": "^7.2.0",
"glob": "^10.5.0",
"path-to-regexp@0.1.10": "^0.1.12",
"sharp": "^0.34.5",
"socket.io": "^4.8.1",
"ws": "^8.17.1"
},
"devDependencies": {
"@lavamoat/allow-scripts": "^3.4.2",
@@ -57,31 +67,34 @@
"node": ">=18.6.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"gatsby",
"sharp"
],
"peerDependencyRules": {
"allowedVersions": {
"eslint": "9",
"react": "18",
"@metamask/providers": "22",
"@metamask/snaps-controllers": "17",
"@typescript-eslint/eslint-plugin": "5",
"@typescript-eslint/parser": "5",
"eslint-plugin-jest": "28"
"eslint": "9",
"eslint-plugin-jest": "28",
"react": "18"
}
},
"onlyBuiltDependencies": [
"gatsby",
"sharp"
],
"overrides": {
"@sigmacomputing/babel-plugin-lodash>glob": "^7.2.0",
"cookie": "^0.7.1",
"eslint-import-resolver-typescript": "^3.6.3",
"path-to-regexp@0.1.10": "^0.1.12",
"sharp": "^0.33.5",
"socket.io": "^4.8.1",
"ws": "^8.17.1",
"gatsby>eslint": "^7.32.0",
"eslint-config-react-app>eslint": "^7.32.0",
"eslint-plugin-flowtype>eslint": "^7.32.0"
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-flowtype>eslint": "^7.32.0",
"gatsby>eslint": "^7.32.0",
"gatsby>glob": "^7.2.0",
"glob": "^10.5.0",
"path-to-regexp@0.1.10": "^0.1.12",
"sharp": "^0.34.5",
"socket.io": "^4.8.1",
"ws": "^8.17.1"
}
},
"lavamoat": {

View File

@@ -4,6 +4,10 @@
*/
SNAP_ORIGIN=
# Snap origin for production (must be GATSBY_* so it is inlined into the client bundle).
# E.g. npm:chain138-snap. Leave empty for local (local:http://localhost:8080).
GATSBY_SNAP_ORIGIN=
# Token-aggregation API base URL for Snap (market data, token list, bridge routes, quotes).
# E2E (local): http://localhost:3000
# Production: set to your live token-aggregation API (e.g. https://api.example.com). No trailing slash.

View File

@@ -4,6 +4,11 @@ import { StrictMode } from 'react';
import { App } from './src/App';
import { Root } from './src/Root';
/**
*
* @param options0
* @param options0.element
*/
export const wrapRootElement: GatsbyBrowser['wrapRootElement'] = ({
element,
}) => (
@@ -12,6 +17,11 @@ export const wrapRootElement: GatsbyBrowser['wrapRootElement'] = ({
</StrictMode>
);
/**
*
* @param options0
* @param options0.element
*/
export const wrapPageElement: GatsbyBrowser['wrapPageElement'] = ({
element,
}) => <App>{element}</App>;

View File

@@ -12,12 +12,12 @@ const config: GatsbyConfig = {
{
resolve: 'gatsby-plugin-manifest',
options: {
name: 'Template Snap',
name: 'Chain 138 Snap',
icon: 'src/assets/logo.svg',
/* eslint-disable @typescript-eslint/naming-convention */
theme_color: '#6F4CFF',
background_color: '#FFFFFF',
/* eslint-enable @typescript-eslint/naming-convention */
display: 'standalone',
},
},

View File

@@ -4,12 +4,22 @@ import { StrictMode } from 'react';
import { App } from './src/App';
import { Root } from './src/Root';
/**
*
* @param options0
* @param options0.element
*/
export const wrapRootElement: GatsbySSR['wrapRootElement'] = ({ element }) => (
<StrictMode>
<Root>{element}</Root>
</StrictMode>
);
/**
*
* @param options0
* @param options0.element
*/
export const wrapPageElement: GatsbySSR['wrapPageElement'] = ({ element }) => (
<App>{element}</App>
);

View File

@@ -14,10 +14,21 @@ const Wrapper = styled.div`
max-width: 100vw;
`;
/**
*
*/
export type AppProps = {
/**
*
*/
children: ReactNode;
};
/**
*
* @param options0
* @param options0.children
*/
export const App: FunctionComponent<AppProps> = ({ children }) => {
const toggleTheme = useContext(ToggleThemeContext);

View File

@@ -6,19 +6,36 @@ import { dark, light } from './config/theme';
import { MetaMaskProvider } from './hooks';
import { getThemePreference, setLocalStorage } from './utils';
/**
*
*/
export type RootProps = {
/**
*
*/
children: ReactNode;
};
/**
*
*/
type ToggleTheme = () => void;
export const ToggleThemeContext = createContext<ToggleTheme>(
(): void => undefined,
);
/**
*
* @param options0
* @param options0.children
*/
export const Root: FunctionComponent<RootProps> = ({ children }) => {
const [darkTheme, setDarkTheme] = useState(getThemePreference());
/**
*
*/
const toggleTheme: ToggleTheme = () => {
setLocalStorage('theme', darkTheme ? 'light' : 'dark');
setDarkTheme(!darkTheme);

View File

@@ -70,6 +70,9 @@ const ConnectedIndicator = styled.div`
background-color: green;
`;
/**
*
*/
export const InstallFlaskButton = () => (
<Link href="https://metamask.io/flask/" target="_blank">
<FlaskFox />
@@ -77,15 +80,31 @@ export const InstallFlaskButton = () => (
</Link>
);
export const ConnectButton = (props: ComponentProps<typeof Button>) => {
/**
*
* @param props
*/
export const ConnectButton = (
props: ComponentProps<typeof Button> & {
/**
*
*/
label?: string;
},
) => {
const { label = 'Connect', ...rest } = props;
return (
<Button {...props}>
<Button {...rest}>
<FlaskFox />
<ButtonText>Connect</ButtonText>
<ButtonText>{label}</ButtonText>
</Button>
);
};
/**
*
* @param props
*/
export const ReconnectButton = (props: ComponentProps<typeof Button>) => {
return (
<Button {...props}>
@@ -95,10 +114,17 @@ export const ReconnectButton = (props: ComponentProps<typeof Button>) => {
);
};
/**
*
* @param props
*/
export const SendHelloButton = (props: ComponentProps<typeof Button>) => {
return <Button {...props}>Send message</Button>;
};
/**
*
*/
export const HeaderButtons = () => {
const requestSnap = useRequestSnap();
const { isFlask, installedSnap } = useMetaMask();
@@ -108,7 +134,12 @@ export const HeaderButtons = () => {
}
if (!installedSnap) {
return <ConnectButton onClick={requestSnap} />;
return (
<ConnectButton
onClick={requestSnap}
label={isFlask ? 'Connect MetaMask Flask' : 'Connect'}
/>
);
}
if (shouldDisplayReconnectButton(installedSnap)) {

View File

@@ -1,18 +1,45 @@
import type { ReactNode } from 'react';
import styled from 'styled-components';
/**
*
*/
type CardProps = {
/**
*
*/
content: {
/**
*
*/
title?: string;
/**
*
*/
description: ReactNode;
/**
*
*/
button?: ReactNode;
};
/**
*
*/
disabled?: boolean;
/**
*
*/
fullWidth?: boolean;
};
const CardWrapper = styled.div<{
/**
*
*/
fullWidth?: boolean | undefined;
/**
*
*/
disabled?: boolean | undefined;
}>`
display: flex;
@@ -48,6 +75,13 @@ const Description = styled.div`
margin-bottom: 2.4rem;
`;
/**
*
* @param options0
* @param options0.content
* @param options0.disabled
* @param options0.fullWidth
*/
export const Card = ({ content, disabled = false, fullWidth }: CardProps) => {
const { title, description, button } = content;
return (

View File

@@ -39,6 +39,9 @@ const PoweredByContainer = styled.div`
margin-left: 1rem;
`;
/**
*
*/
export const Footer = () => {
const theme = useTheme();
const buildVersion = getBuildVersion();

View File

@@ -36,9 +36,17 @@ const RightContainer = styled.div`
align-items: center;
`;
/**
*
* @param options0
* @param options0.handleToggleClick
*/
export const Header = ({
handleToggleClick,
}: {
/**
*
*/
handleToggleClick: () => void;
}) => {
const theme = useTheme();
@@ -47,7 +55,7 @@ export const Header = ({
<HeaderWrapper>
<LogoWrapper>
<SnapLogo color={theme.colors.icon?.default} size={36} />
<Title>template-snap</Title>
<Title>Chain 138 Snap</Title>
</LogoWrapper>
<RightContainer>
<Toggle

View File

@@ -1,4 +1,16 @@
export const MetaMask = ({ color }: { color?: string | undefined }) => (
/**
*
* @param options0
* @param options0.color
*/
export const MetaMask = ({
color,
}: {
/**
*
*/
color?: string | undefined;
}) => (
<svg
width="98"
height="12"

View File

@@ -1,4 +1,16 @@
export const PoweredBy = ({ color }: { color?: string | undefined }) => (
/**
*
* @param options0
* @param options0.color
*/
export const PoweredBy = ({
color,
}: {
/**
*
*/
color?: string | undefined;
}) => (
<svg
width="60"
height="12"

View File

@@ -1,8 +1,20 @@
/**
*
* @param options0
* @param options0.color
* @param options0.size
*/
export const SnapLogo = ({
color,
size,
}: {
/**
*
*/
color?: string | undefined;
/**
*
*/
size: number;
}) => (
<svg

View File

@@ -1,7 +1,13 @@
import { useState } from 'react';
import styled from 'styled-components';
/**
*
*/
type CheckedProps = {
/**
*
*/
readonly checked: boolean;
};
@@ -90,15 +96,30 @@ const ToggleCircle = styled.div<CheckedProps>`
transition: all 0.25s ease;
`;
/**
*
* @param options0
* @param options0.onToggle
* @param options0.defaultChecked
*/
export const Toggle = ({
onToggle,
defaultChecked = false,
}: {
/**
*
*/
onToggle: () => void;
/**
*
*/
defaultChecked?: boolean;
}) => {
const [checked, setChecked] = useState(defaultChecked);
/**
*
*/
const handleChange = () => {
onToggle();
setChecked(!checked);

View File

@@ -5,10 +5,16 @@ export { defaultSnapOrigin } from './snap';
* Set GATSBY_SNAP_API_BASE_URL in .env or .env.production for production.
*/
export const getSnapApiBaseUrl = (): string =>
(typeof process !== 'undefined' &&
process.env?.GATSBY_SNAP_API_BASE_URL) ||
(typeof process !== 'undefined' && process.env?.GATSBY_SNAP_API_BASE_URL) ||
'';
/**
* Public origin of the Snap companion site (e.g. https://explorer.d-bis.org).
* Set GATSBY_SNAP_SITE_URL so "Send on Chain 138" link is absolute HTTPS and never redirects to HTTP.
*/
export const getSnapSiteUrl = (): string =>
(typeof process !== 'undefined' && process.env?.GATSBY_SNAP_SITE_URL) || '';
/** Build ID or git SHA for support/debug (set at build: GATSBY_BUILD_SHA, GATSBY_APP_VERSION). */
export const getBuildVersion = (): string =>
(typeof process !== 'undefined' &&

View File

@@ -2,10 +2,11 @@
* The snap origin to use.
* Will default to the local hosted snap if no value is provided in environment.
*
* You may be tempted to change this to the URL where your production snap is hosted, but please
* don't. Instead, rename `.env.production.dist` to `.env.production` and set the production URL
* there. Running `yarn build` will automatically use the production environment variables.
* Use GATSBY_SNAP_ORIGIN so Gatsby inlines it into the client bundle (only GATSBY_*
* vars are exposed to the browser). E.g. GATSBY_SNAP_ORIGIN=npm:chain138-snap for production.
*/
export const defaultSnapOrigin =
// eslint-disable-next-line no-restricted-globals
process.env.SNAP_ORIGIN ?? `local:http://localhost:8080`;
process.env.GATSBY_SNAP_ORIGIN ??
process.env.SNAP_ORIGIN ??
`local:http://localhost:8080`;

View File

@@ -5,11 +5,29 @@ import { createContext, useContext, useEffect, useState } from 'react';
import type { Snap } from '../types';
import { getSnapsProvider } from '../utils';
/**
*
*/
type MetaMaskContextType = {
/**
*
*/
provider: MetaMaskInpageProvider | null;
/**
*
*/
installedSnap: Snap | null;
/**
*
*/
error: Error | null;
/**
*
*/
setInstalledSnap: (snap: Snap | null) => void;
/**
*
*/
setError: (error: Error) => void;
};
@@ -17,9 +35,15 @@ export const MetaMaskContext = createContext<MetaMaskContextType>({
provider: null,
installedSnap: null,
error: null,
/**
*
*/
setInstalledSnap: () => {
/* no-op */
},
/**
*
*/
setError: () => {
/* no-op */
},
@@ -32,7 +56,14 @@ export const MetaMaskContext = createContext<MetaMaskContextType>({
* @param props.children - React component to be wrapped by the Provider.
* @returns JSX.
*/
export const MetaMaskProvider = ({ children }: { children: ReactNode }) => {
export const MetaMaskProvider = ({
children,
}: {
/**
*
*/
children: ReactNode;
}) => {
const [provider, setProvider] = useState<MetaMaskInpageProvider | null>(null);
const [installedSnap, setInstalledSnap] = useState<Snap | null>(null);
const [error, setError] = useState<Error | null>(null);

View File

@@ -1,8 +1,17 @@
import { useRequest } from './useRequest';
import { defaultSnapOrigin } from '../config';
/**
*
*/
export type InvokeSnapParams = {
/**
*
*/
method: string;
/**
*
*/
params?: Record<string, unknown>;
};

View File

@@ -20,13 +20,17 @@ export const useMetaMask = () => {
/**
* Detect if the version of MetaMask is Flask.
* web3_clientVersion returns a string (e.g. "MetaMask/v11.0.0-flask").
*/
const detectFlask = async () => {
const clientVersion = await request({
method: 'web3_clientVersion',
});
const isFlaskDetected = (clientVersion as string[])?.includes('flask');
const versionStr = Array.isArray(clientVersion)
? (clientVersion as string[]).join(' ')
: String(clientVersion ?? '');
const isFlaskDetected = versionStr.toLowerCase().includes('flask');
setIsFlask(isFlaskDetected);
};
@@ -43,6 +47,9 @@ export const useMetaMask = () => {
};
useEffect(() => {
/**
*
*/
const detect = async () => {
if (provider) {
await detectFlask();

View File

@@ -2,6 +2,9 @@ import type { RequestArguments } from '@metamask/providers';
import { useMetaMaskContext } from './MetamaskContext';
/**
*
*/
export type Request = (params: RequestArguments) => Promise<unknown | null>;
/**

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import styled from 'styled-components';
import { useState } from 'react';
import {
Button,
ConnectButton,
@@ -9,7 +9,7 @@ import {
SendHelloButton,
Card,
} from '../components';
import { defaultSnapOrigin, getSnapApiBaseUrl } from '../config';
import { defaultSnapOrigin, getSnapApiBaseUrl, getSnapSiteUrl } from '../config';
import {
useMetaMask,
useInvokeSnap,
@@ -109,6 +109,9 @@ const MarketSummaryList = styled.div`
margin-top: 0.8rem;
`;
/**
*
*/
const Index = () => {
const { error } = useMetaMaskContext();
const { isFlask, snapsDetected, installedSnap } = useMetaMask();
@@ -116,17 +119,52 @@ const Index = () => {
const invokeSnap = useInvokeSnap();
const apiBaseUrl = getSnapApiBaseUrl();
const [marketSummary, setMarketSummary] = useState<{
tokens: Array<{
/**
*
*/
tokens: {
/**
*
*/
symbol?: string;
/**
*
*/
name?: string;
/**
*
*/
address?: string;
market?: { priceUsd?: number; volume24h?: number };
}>;
/**
*
*/
market?: {
/**
*
*/
priceUsd?: number; /**
*
*/
volume24h?: number;
};
}[];
/**
*
*/
error?: string;
} | null>(null);
const [swapQuote, setSwapQuote] = useState<{
/**
*
*/
amountOut?: string;
/**
*
*/
error?: string;
/**
*
*/
poolAddress?: string | null;
} | null>(null);
const [swapTokenIn, setSwapTokenIn] = useState('');
@@ -139,12 +177,20 @@ const Index = () => {
const snapParams = apiBaseUrl ? { apiBaseUrl } : undefined;
/**
*
*/
const handleSendHelloClick = async () => {
await invokeSnap(
snapParams ? { method: 'hello', params: snapParams } : { method: 'hello' },
snapParams
? { method: 'hello', params: snapParams }
: { method: 'hello' },
);
};
/**
*
*/
const handleShowMarketData = async () => {
if (!apiBaseUrl) {
return;
@@ -155,6 +201,9 @@ const Index = () => {
});
};
/**
*
*/
const handleGetMarketSummary = async () => {
if (!apiBaseUrl) {
setMarketSummary({ tokens: [], error: 'Set GATSBY_SNAP_API_BASE_URL' });
@@ -165,12 +214,38 @@ const Index = () => {
method: 'get_market_summary',
params: { apiBaseUrl },
})) as {
tokens?: Array<{
/**
*
*/
tokens?: {
/**
*
*/
symbol?: string;
/**
*
*/
name?: string;
/**
*
*/
address?: string;
market?: { priceUsd?: number; volume24h?: number };
}>;
/**
*
*/
market?: {
/**
*
*/
priceUsd?: number; /**
*
*/
volume24h?: number;
};
}[];
/**
*
*/
error?: string;
};
if (result?.error) {
@@ -186,6 +261,9 @@ const Index = () => {
}
};
/**
*
*/
const handleShowBridgeRoutes = async () => {
if (!apiBaseUrl) {
return;
@@ -196,6 +274,23 @@ const Index = () => {
});
};
/**
* Show dynamic info (networks + token list URL) in a Snap dialog.
* Use the token list URL in MetaMask Settings → Token list to get tokens and icons.
*/
const handleShowDynamicInfo = async () => {
if (!apiBaseUrl) {
return;
}
await invokeSnap({
method: 'show_dynamic_info',
params: { apiBaseUrl },
});
};
/**
*
*/
const handleGetSwapQuote = async () => {
if (!apiBaseUrl) {
setSwapQuote({ error: 'Set GATSBY_SNAP_API_BASE_URL' });
@@ -216,8 +311,17 @@ const Index = () => {
amountIn: swapAmountIn.trim(),
},
})) as {
/**
*
*/
amountOut?: string;
/**
*
*/
error?: string;
/**
*
*/
poolAddress?: string | null;
};
setSwapQuote({
@@ -232,6 +336,9 @@ const Index = () => {
}
};
/**
*
*/
const handleShowSwapQuote = async () => {
if (!apiBaseUrl || !swapTokenIn || !swapTokenOut || !swapAmountIn) {
return;
@@ -251,10 +358,11 @@ const Index = () => {
return (
<Container>
<Heading>
Welcome to <Span>template-snap</Span>
Welcome to <Span>Chain 138 Snap</Span>
</Heading>
<Subtitle>
Get started by editing <code>src/index.tsx</code>
Add Chain 138 (DeFi Oracle Meta) to MetaMask and use market data,
bridge, and swap.
</Subtitle>
<Notice>
<p>
@@ -263,6 +371,22 @@ const Index = () => {
Bridge, and Swap cards below (set GATSBY_SNAP_API_BASE_URL for
production API).
</p>
<p style={{ marginTop: '1rem', marginBottom: 0 }}>
<strong>Chain 138 Send:</strong> If MetaMasks in-wallet
&quot;Send&quot; errors with &quot;No XChain Swaps native asset
found&quot;, use{' '}
<a
href={
getSnapSiteUrl()
? `${getSnapSiteUrl()}${(typeof process !== 'undefined' && process.env?.GATSBY_PATH_PREFIX) || ''}/send`
: './send'
}
style={{ color: 'var(--color-primary-default, #6F4CFF)' }}
>
Send on Chain 138
</a>{' '}
instead.
</p>
</Notice>
<CardContainer>
{error && (
@@ -291,6 +415,7 @@ const Index = () => {
<ConnectButton
onClick={requestSnap}
disabled={!isMetaMaskReady}
label={isFlask ? 'Connect MetaMask Flask' : 'Connect'}
/>
),
}}
@@ -370,7 +495,7 @@ const Index = () => {
content={{
title: 'Bridge',
description: apiBaseUrl
? 'Show CCIP bridge routes (WETH9 / WETH10) in a Snap dialog. Use explorer for executing transfers.'
? 'Show CCIP and Trustless bridge routes in a Snap dialog. Use explorer for executing transfers.'
: 'Set GATSBY_SNAP_API_BASE_URL to show bridge routes.',
button: (
<Button
@@ -383,6 +508,23 @@ const Index = () => {
}}
disabled={!installedSnap}
/>
<Card
content={{
title: 'Token list URL',
description: apiBaseUrl
? 'Show networks and token list URL. Add the URL in MetaMask Settings → Token list to display tokens and icons.'
: 'Set GATSBY_SNAP_API_BASE_URL to get the token list URL.',
button: (
<Button
onClick={handleShowDynamicInfo}
disabled={!installedSnap || !apiBaseUrl}
>
Show dynamic info
</Button>
),
}}
disabled={!installedSnap}
/>
<Card
fullWidth
content={{
@@ -412,7 +554,9 @@ const Index = () => {
style={{ width: '100%' }}
/>
</div>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<div
style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}
>
<Button
onClick={handleGetSwapQuote}
disabled={!installedSnap}
@@ -482,10 +626,11 @@ const Index = () => {
)}
<Notice>
<p>
Please note that the <b>snap.manifest.json</b> and{' '}
<b>package.json</b> must be located in the server root directory and
the bundle must be hosted at the location specified by the location
field.
When hosting the Snap yourself (e.g. for development),{' '}
<b>snap.manifest.json</b> and <b>package.json</b> must be at the
hosts root path, and the bundle must be served at the path given in{' '}
<b>source.location</b> in the manifest. When installing from npm
(production), the published package already satisfies this layout.
</p>
</Notice>
</CardContainer>

View File

@@ -0,0 +1,329 @@
/**
* Send page — Chain 138 native send (bypasses MetaMask's broken Send UI).
*
* MetaMask's in-wallet "Send" can throw "No XChain Swaps native asset found for chainId: eip155:138"
* because Chain 138 is not in their LavaPack list. This page sends via eth_sendTransaction from the
* dApp context, which uses a different code path and works on Chain 138.
*/
import { useState } from 'react';
import styled from 'styled-components';
import { Button } from '../components';
import { useMetaMaskContext } from '../hooks/MetamaskContext';
const CHAIN_ID_138_HEX = '0x8a';
const CHAIN_138_PARAMS = {
chainId: CHAIN_ID_138_HEX,
chainName: 'DeFi Oracle Meta Mainnet',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: ['https://rpc-http-pub.d-bis.org'],
blockExplorerUrls: ['https://explorer.d-bis.org'],
};
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
max-width: 32rem;
margin: 4rem auto;
padding: 0 2.4rem;
${({ theme }) => theme.mediaQueries?.small} {
margin: 2rem auto;
padding: 0 1.2rem;
}
`;
const Title = styled.h1`
margin: 0 0 0.5rem;
font-size: ${({ theme }) => theme.fontSizes?.title ?? '1.5rem'};
`;
const Subtitle = styled.p`
margin: 0 0 2rem;
color: ${({ theme }) => theme.colors?.text?.alternative};
font-size: ${({ theme }) => theme.fontSizes?.small};
`;
const Form = styled.form`
width: 100%;
display: flex;
flex-direction: column;
gap: 1.2rem;
`;
const Label = styled.label`
display: flex;
flex-direction: column;
gap: 0.4rem;
font-size: ${({ theme }) => theme.fontSizes?.small};
font-weight: 500;
`;
const Input = styled.input`
padding: 0.8rem 1rem;
border: 1px solid ${({ theme }) => theme.colors?.border?.default};
border-radius: ${({ theme }) => theme.radii?.default ?? '6px'};
font-size: ${({ theme }) => theme.fontSizes?.text};
background: ${({ theme }) => theme.colors?.background?.default};
color: ${({ theme }) => theme.colors?.text?.default};
font-family: inherit;
&::placeholder {
color: ${({ theme }) => theme.colors?.text?.muted};
}
`;
const Message = styled.div<{
/**
*
*/
$error?: boolean;
}>`
padding: 1rem;
border-radius: ${({ theme }) => theme.radii?.default ?? '6px'};
font-size: ${({ theme }) => theme.fontSizes?.small};
background: ${({ theme, $error }) =>
$error
? theme.colors?.error?.muted
: theme.colors?.background?.alternative};
color: ${({ theme, $error }) =>
$error ? theme.colors?.error?.default : theme.colors?.text?.default};
word-break: break-all;
`;
const Row = styled.div`
display: flex;
gap: 0.8rem;
flex-wrap: wrap;
`;
const StyledButton = styled(Button)`
flex: 1;
min-width: 8rem;
`;
const BackLink = styled.a`
margin-top: 2rem;
font-size: ${({ theme }) => theme.fontSizes?.small};
color: ${({ theme }) => theme.colors?.primary?.default};
text-decoration: none;
&:hover {
text-decoration: underline;
}
`;
/**
*
* @param eth
*/
function ethToWeiHex(eth: string): string {
const parsed = parseFloat(eth);
if (Number.isNaN(parsed) || parsed < 0) {
return '0x0';
}
const wei = BigInt(Math.floor(parsed * 1e18));
return `0x${wei.toString(16)}`;
}
/**
*
* @param addr
*/
function isValidAddress(addr: string): boolean {
return /^0x[a-fA-F0-9]{40}$/.test(addr.trim());
}
/**
*
*/
export default function SendPage() {
const { provider } = useMetaMaskContext();
const [to, setTo] = useState('');
const [amount, setAmount] = useState('');
const [message, setMessage] = useState<{
/**
*
*/
text: string;
/**
*
*/
error?: boolean;
} | null>(null);
const [loading, setLoading] = useState(false);
/**
*
*/
const ensureChain138 = async (): Promise<boolean> => {
if (!provider) {
setMessage({
text: 'MetaMask not detected. Install and connect MetaMask.',
error: true,
});
return false;
}
try {
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: CHAIN_ID_138_HEX }],
});
return true;
} catch (err: unknown) {
const code = (
err as {
/**
*
*/
code?: number;
}
)?.code;
if (code === 4902) {
try {
await provider.request({
method: 'wallet_addEthereumChain',
params: [CHAIN_138_PARAMS],
});
return true;
} catch (addErr) {
setMessage({
text: `Failed to add Chain 138: ${addErr instanceof Error ? addErr.message : String(addErr)}`,
error: true,
});
return false;
}
}
setMessage({
text: `Failed to switch to Chain 138: ${err instanceof Error ? err.message : String(err)}`,
error: true,
});
return false;
}
};
/**
*
*/
const handleSwitchChain = async () => {
setMessage(null);
setLoading(true);
const ok = await ensureChain138();
setLoading(false);
if (ok) {
setMessage({ text: 'Switched to Chain 138.' });
}
};
/**
*
* @param e
*/
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setMessage(null);
if (!provider) {
setMessage({ text: 'MetaMask not detected.', error: true });
return;
}
const toAddress = to.trim();
if (!isValidAddress(toAddress)) {
setMessage({ text: 'Enter a valid 0x address.', error: true });
return;
}
const amountNum = parseFloat(amount);
if (Number.isNaN(amountNum) || amountNum <= 0) {
setMessage({ text: 'Enter a valid amount (ETH).', error: true });
return;
}
const ok = await ensureChain138();
if (!ok) {
return;
}
setLoading(true);
try {
const accounts = (await provider.request({
method: 'eth_requestAccounts',
params: [],
})) as string[];
const from = accounts?.[0];
if (!from) {
setMessage({ text: 'No account selected in MetaMask.', error: true });
setLoading(false);
return;
}
const txHash = (await provider.request({
method: 'eth_sendTransaction',
params: [
{
from,
to: toAddress,
value: ethToWeiHex(amount),
gasLimit: '0x5208', // 21000 for simple transfer
},
],
})) as string;
setMessage({
text: `Sent. Tx: ${txHash}. Confirm in MetaMask and check the block explorer.`,
});
setTo('');
setAmount('');
} catch (err) {
setMessage({
text: err instanceof Error ? err.message : String(err),
error: true,
});
} finally {
setLoading(false);
}
};
return (
<Container>
<Title>Send on Chain 138</Title>
<Subtitle>
Use this page to send ETH on Chain 138. It bypasses MetaMasks in-wallet
Send button, which errors on custom chains.
</Subtitle>
<Form onSubmit={handleSubmit}>
<Label>
Recipient address
<Input
type="text"
placeholder="0x..."
value={to}
onChange={(e) => setTo(e.target.value)}
disabled={loading}
/>
</Label>
<Label>
Amount (ETH)
<Input
type="text"
inputMode="decimal"
placeholder="0.1"
value={amount}
onChange={(e) => setAmount(e.target.value)}
disabled={loading}
/>
</Label>
{message && <Message $error={message.error}>{message.text}</Message>}
<Row>
<StyledButton
type="button"
onClick={handleSwitchChain}
disabled={loading || !provider}
>
{loading ? '…' : 'Switch to Chain 138'}
</StyledButton>
<StyledButton type="submit" disabled={loading || !provider}>
{loading ? '…' : 'Send'}
</StyledButton>
</Row>
</Form>
<BackLink href="./"> Back to Chain 138 Snap</BackLink>
</Container>
);
}

View File

@@ -8,18 +8,40 @@ import type {
* Window type extension to support ethereum
*/
declare global {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
/**
*
*/
interface Window {
/**
*
*/
ethereum: MetaMaskInpageProvider & {
/**
*
*/
setProvider?: (provider: MetaMaskInpageProvider) => void;
/**
*
*/
detected?: MetaMaskInpageProvider[];
/**
*
*/
providers?: MetaMaskInpageProvider[];
};
}
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
/**
*
*/
interface WindowEventMap {
/**
*
*/
'eip6963:requestProvider': EIP6963RequestProviderEvent;
/**
*
*/
'eip6963:announceProvider': EIP6963AnnounceProviderEvent;
}
}

View File

@@ -1,8 +1,26 @@
/**
*
*/
export type GetSnapsResponse = Record<string, Snap>;
/**
*
*/
export type Snap = {
/**
*
*/
permissionName: string;
/**
*
*/
id: string;
/**
*
*/
version: string;
/**
*
*/
initialPermissions: Record<string, unknown>;
};

View File

@@ -7,13 +7,37 @@ import 'styled-components';
*/
declare module 'styled-components' {
/* eslint-disable @typescript-eslint/consistent-type-definitions */
/**
*
*/
export interface DefaultTheme {
/**
*
*/
fonts: Record<string, string>;
/**
*
*/
fontSizes: Record<string, string>;
/**
*
*/
breakpoints: string[];
/**
*
*/
mediaQueries: Record<string, string>;
/**
*
*/
radii: Record<string, string>;
/**
*
*/
shadows: Record<string, string>;
/**
*
*/
colors: Record<string, Record<string, string>>;
}
}

View File

@@ -1,5 +1,9 @@
import { isLocalSnap } from './snap';
import type { Snap } from '../types';
/**
*
* @param installedSnap
*/
export const shouldDisplayReconnectButton = (installedSnap: Snap | null) =>
installedSnap && isLocalSnap(installedSnap?.id);

View File

@@ -79,9 +79,31 @@ export async function getMetaMaskEIP6963Provider() {
});
}
/**
* Check if a provider reports as MetaMask Flask via web3_clientVersion.
*
* @param provider
*/
async function isFlaskProvider(
provider: MetaMaskInpageProvider,
): Promise<boolean> {
try {
const clientVersion = (await provider.request({
method: 'web3_clientVersion',
})) as string | string[] | undefined;
const versionStr = Array.isArray(clientVersion)
? clientVersion.join(' ')
: String(clientVersion ?? '');
return versionStr.toLowerCase().includes('flask');
} catch {
return false;
}
}
/**
* Get a provider that supports snaps. This will loop through all the detected
* providers and return the first one that supports snaps.
* providers and return the first one that supports snaps. Prefers Flask when
* multiple snaps-capable providers exist so the UI can show "Connect MetaMask Flask".
*
* @returns The provider, or `null` if no provider supports snaps.
*/
@@ -103,11 +125,20 @@ export async function getSnapsProvider() {
}
if (window.ethereum?.providers) {
const snapsProviders: MetaMaskInpageProvider[] = [];
for (const provider of window.ethereum.providers) {
if (await hasSnapsSupport(provider)) {
return provider;
snapsProviders.push(provider);
}
}
if (snapsProviders.length > 0) {
for (const provider of snapsProviders) {
if (await isFlaskProvider(provider)) {
return provider;
}
}
return snapsProviders[0];
}
}
const eip6963Provider = await getMetaMaskEIP6963Provider();
@@ -116,5 +147,23 @@ export async function getSnapsProvider() {
return eip6963Provider;
}
// Fallback: if window.ethereum reports Flask, use it (Flask supports snaps;
// hasSnapsSupport may have failed e.g. if wallet was locked).
if (window.ethereum) {
try {
const clientVersion = (await window.ethereum.request({
method: 'web3_clientVersion',
})) as string | string[] | undefined;
const versionStr = Array.isArray(clientVersion)
? clientVersion.join(' ')
: String(clientVersion ?? '');
if (versionStr.toLowerCase().includes('flask')) {
return window.ethereum as MetaMaskInpageProvider;
}
} catch {
// ignore
}
}
return null;
}

View File

@@ -1 +1 @@
{"version":"dev","buildTime":"2026-02-16T06:07:18.288Z"}
{"version":"dev","buildTime":"2026-02-22T19:21:30.615Z"}

View File

@@ -1,6 +1,6 @@
# chain138-snap
**Chain 138 Snap** adds [DeFi Oracle Meta Mainnet](https://chainlist.org/chain/138) (ChainID 138) and **ALL Mainnet** (651940) support inside MetaMask: network params, token list, market data, swap quotes, and CCIP bridge routes.
**Chain 138 Snap** adds [DeFi Oracle Meta Mainnet](https://chainlist.org/chain/138) (ChainID 138) and **ALL Mainnet** (651940) support inside MetaMask: network params, token list, market data, swap quotes, and bridge routes (CCIP and Trustless).
MetaMask already supports Chain 138 as a custom EVM network, but native **Swaps**, **Portfolio Bridge**, and **USD pricing** do not include Chain 138. This Snap provides in-wallet swap quotes, bridge routes, and market data by calling your token-aggregation (or compatible) API.
@@ -41,18 +41,18 @@ For **market data**, **swap quotes**, and **bridge routes**, the dApp must pass
### RPC methods
| Method | Description |
|--------|-------------|
| `hello` | Basic test; returns a greeting. |
| `get_networks` | Full EIP-3085 chain params (Chain 138, Ethereum, ALL Mainnet). |
| `get_chain138_config` | Chain 138 config from API. |
| `get_chain138_market_chains` | Market chains list. |
| `get_token_list` / `get_token_list_url` | Token list (optional `chainId`). |
| `get_oracles` | Oracles config. |
| `show_dynamic_info` | In-Snap dialog with networks and token list URL. |
| `get_market_summary` / `show_market_data` | Tokens and USD prices. |
| `get_bridge_routes` / `show_bridge_routes` | CCIP bridge routes. |
| `get_swap_quote` / `show_swap_quote` | Swap quote (requires `tokenIn`, `tokenOut`, `amountIn`). |
| Method | Description |
| ------------------------------------------ | -------------------------------------------------------------- |
| `hello` | Basic test; returns a greeting. |
| `get_networks` | Full EIP-3085 chain params (Chain 138, Ethereum, ALL Mainnet). |
| `get_chain138_config` | Chain 138 config from API. |
| `get_chain138_market_chains` | Market chains list. |
| `get_token_list` / `get_token_list_url` | Token list (optional `chainId`). |
| `get_oracles` | Oracles config. |
| `show_dynamic_info` | In-Snap dialog with networks and token list URL. |
| `get_market_summary` / `show_market_data` | Tokens and USD prices. |
| `get_bridge_routes` / `show_bridge_routes` | CCIP and Trustless bridge routes. |
| `get_swap_quote` / `show_swap_quote` | Swap quote (requires `tokenIn`, `tokenOut`, `amountIn`). |
## Repository and docs

View File

@@ -1,15 +1,15 @@
{
"name": "chain138-snap",
"version": "0.1.1",
"description": "Chain 138 (DeFi Oracle Meta Mainnet) and ALL Mainnet Snap: networks, token list, market data, swap quotes, CCIP bridge routes for MetaMask.",
"repository": {
"type": "git",
"url": "https://github.com/bis-innovations/chain138-snap.git"
},
"version": "0.1.2",
"description": "Chain 138 (DeFi Oracle Meta Mainnet) and ALL Mainnet Snap: networks, token list, market data, swap quotes, CCIP and Trustless bridge routes for MetaMask.",
"homepage": "https://github.com/bis-innovations/chain138-snap#readme",
"bugs": {
"url": "https://github.com/bis-innovations/chain138-snap/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/bis-innovations/chain138-snap.git"
},
"license": "(MIT-0 OR Apache-2.0)",
"main": "./dist/bundle.js",
"files": [
@@ -39,6 +39,7 @@
"@metamask/providers": "^22.1.0",
"@metamask/snaps-cli": "^8.3.0",
"@metamask/snaps-jest": "^9.8.0",
"@types/jest": "^29.5.0",
"@types/react": "18.2.4",
"@types/react-dom": "18.2.4",
"eslint": "^9.39.2",

View File

@@ -1,13 +1,13 @@
{
"version": "0.1.1",
"description": "Chain 138 (DeFi Oracle Meta Mainnet) and ALL Mainnet: networks, token list, market data, swap quotes, and CCIP bridge routes for MetaMask.",
"version": "0.1.2",
"description": "Chain 138 (DeFi Oracle Meta Mainnet) and ALL Mainnet: networks, token list, market data, swap quotes, and CCIP and Trustless bridge routes for MetaMask.",
"proposedName": "Chain 138",
"repository": {
"type": "git",
"url": "https://github.com/bis-innovations/chain138-snap.git"
},
"source": {
"shasum": "6CuMlWe0q/GCAHp8l6U+niT/Um5DHEYex4GPhbs5bkg=",
"shasum": "8GYAFlgbiR/jXAwnprqqE4jTIvQv/Uhkn3MiH23g/tQ=",
"location": {
"npm": {
"filePath": "dist/bundle.js",

View File

@@ -3,9 +3,21 @@ import type { SnapConfirmationInterface } from '@metamask/snaps-jest';
import { installSnap } from '@metamask/snaps-jest';
import { Box, Text, Bold } from '@metamask/snaps-sdk/jsx';
/**
*
*/
type SnapsJestExpect = {
/**
*
*/
toRender: (expected: unknown) => void;
/**
*
*/
toRespondWith: (expected: unknown) => void;
/**
*
*/
toRespondWithError: (expected: unknown) => void;
};
@@ -36,7 +48,9 @@ describe('onRpcRequest', () => {
await ui.ok();
(expect(await response) as unknown as SnapsJestExpect).toRespondWith(true);
(expect(await response) as unknown as SnapsJestExpect).toRespondWith(
true,
);
});
});

View File

@@ -6,6 +6,9 @@ const DEFAULT_MARKET_API_BASE = '';
/** RPC params: apiBaseUrl and optional override URLs for networks, token list, bridge list */
export type SnapRpcParams = {
/**
*
*/
apiBaseUrl?: string;
/** When set, get_networks / get_chain138_config fetch this URL instead of apiBaseUrl */
networksUrl?: string;
@@ -13,6 +16,15 @@ export type SnapRpcParams = {
tokenListUrl?: string;
/** When set, get_bridge_routes / show_bridge_routes fetch this URL instead of apiBaseUrl */
bridgeListUrl?: string;
/** For get_token_mapping: source chain ID (e.g. 138) */
fromChain?: number;
/** For get_token_mapping: destination chain ID (e.g. 651940) */
toChain?: number;
/** For get_token_mapping (resolve): token address on source chain to resolve to destination */
address?: string;
/**
*
*/
chainId?: number;
[key: string]: unknown;
};
@@ -23,7 +35,16 @@ export type SnapRpcParams = {
* @param params - Request params with optional apiBaseUrl.
* @returns API base URL string.
*/
function getApiBase(params: { apiBaseUrl?: string } | undefined): string {
function getApiBase(
params:
| {
/**
*
*/
apiBaseUrl?: string;
}
| undefined,
): string {
return (params?.apiBaseUrl ?? DEFAULT_MARKET_API_BASE).replace(/\/$/u, '');
}
@@ -39,16 +60,65 @@ async function fetchNetworks(apiBase: string) {
throw new Error(`HTTP ${res.status}`);
}
return res.json() as Promise<{
/**
*
*/
version?: string;
/**
*
*/
networks?: {
/**
*
*/
chainId: string;
/**
*
*/
chainIdDecimal: number;
/**
*
*/
chainName: string;
/**
*
*/
rpcUrls: string[];
nativeCurrency: { name: string; symbol: string; decimals: number };
/**
*
*/
nativeCurrency: {
/**
*
*/
name: string; /**
*
*/
symbol: string; /**
*
*/
decimals: number;
};
/**
*
*/
blockExplorerUrls: string[];
/**
*
*/
iconUrls?: string[];
oracles?: { name: string; address: string }[];
/**
*
*/
oracles?: {
/**
*
*/
name: string; /**
*
*/
address: string;
}[];
}[];
}>;
}
@@ -76,12 +146,26 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
if (networksUrl) {
try {
const res = await fetch(networksUrl);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = (await res.json()) as { version?: string; networks?: unknown[] };
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = (await res.json()) as {
/**
*
*/
version?: string;
/**
*
*/
networks?: unknown[];
};
return { version: data.version, networks: data.networks ?? [] };
} catch (error) {
return {
error: error instanceof Error ? error.message : 'Failed to fetch networks URL',
error:
error instanceof Error
? error.message
: 'Failed to fetch networks URL',
version: undefined,
networks: [],
};
@@ -89,8 +173,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
}
if (!base) {
return {
error:
'Pass apiBaseUrl or networksUrl to fetch networks',
error: 'Pass apiBaseUrl or networksUrl to fetch networks',
version: undefined,
networks: [],
};
@@ -109,11 +192,26 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
}
case 'get_chain138_config': {
/**
*
*/
const loadNetworks = async () => {
if (networksUrl) {
const res = await fetch(networksUrl);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = (await res.json()) as { networks?: { chainIdDecimal?: number }[] };
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = (await res.json()) as {
/**
*
*/
networks?: {
/**
*
*/
chainIdDecimal?: number;
}[];
};
return data.networks ?? [];
}
if (base) {
@@ -125,20 +223,71 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
try {
const networks = await loadNetworks();
if (networks === null) {
return { error: 'Pass apiBaseUrl or networksUrl to fetch chain config' };
return {
error: 'Pass apiBaseUrl or networksUrl to fetch chain config',
};
}
const chain138 = networks.find((net: { chainIdDecimal?: number }) => net.chainIdDecimal === 138);
const chain138 = networks.find(
(net: {
/**
*
*/
chainIdDecimal?: number;
}) => net.chainIdDecimal === 138,
);
if (!chain138) {
return { error: 'Chain 138 not found in networks response' };
}
return {
chainId: (chain138 as { chainId?: string }).chainId,
chainId: (
chain138 as {
/**
*
*/
chainId?: string;
}
).chainId,
chainIdDecimal: chain138.chainIdDecimal,
chainName: (chain138 as { chainName?: string }).chainName,
rpcUrls: (chain138 as { rpcUrls?: string[] }).rpcUrls,
nativeCurrency: (chain138 as { nativeCurrency?: unknown }).nativeCurrency,
blockExplorerUrls: (chain138 as { blockExplorerUrls?: string[] }).blockExplorerUrls,
oracles: (chain138 as { oracles?: unknown }).oracles,
chainName: (
chain138 as {
/**
*
*/
chainName?: string;
}
).chainName,
rpcUrls: (
chain138 as {
/**
*
*/
rpcUrls?: string[];
}
).rpcUrls,
nativeCurrency: (
chain138 as {
/**
*
*/
nativeCurrency?: unknown;
}
).nativeCurrency,
blockExplorerUrls: (
chain138 as {
/**
*
*/
blockExplorerUrls?: string[];
}
).blockExplorerUrls,
oracles: (
chain138 as {
/**
*
*/
oracles?: unknown;
}
).oracles,
};
} catch (error) {
return {
@@ -197,18 +346,23 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
case 'get_token_list': {
if (tokenListUrl) {
const chainIdParam = params?.chainId as number | undefined;
const chainIdParam = params?.chainId;
const url = chainIdParam
? `${tokenListUrl}${tokenListUrl.includes('?') ? '&' : '?'}chainId=${chainIdParam}`
: tokenListUrl;
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
return data;
} catch (error) {
return {
error: error instanceof Error ? error.message : 'Failed to fetch token list URL',
error:
error instanceof Error
? error.message
: 'Failed to fetch token list URL',
tokens: [],
};
}
@@ -219,7 +373,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
tokens: [],
};
}
const chainIdParam = params?.chainId as number | undefined;
const chainIdParam = params?.chainId;
const url = chainIdParam
? `${base}/api/v1/report/token-list?chainId=${chainIdParam}`
: `${base}/api/v1/report/token-list`;
@@ -245,7 +399,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
if (!base) {
return { error: 'Pass apiBaseUrl to fetch oracles config', chains: [] };
}
const chainIdParam = params?.chainId as number | undefined;
const chainIdParam = params?.chainId;
const configUrl = chainIdParam
? `${base}/api/v1/config?chainId=${chainIdParam}`
: `${base}/api/v1/config`;
@@ -274,7 +428,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
content: (
<Box>
<Text>
Pass apiBaseUrl, networksUrl, or tokenListUrl to see dynamic networks and token list URL.
Pass apiBaseUrl, networksUrl, or tokenListUrl to see dynamic
networks and token list URL.
</Text>
</Box>
),
@@ -286,16 +441,36 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
if (networksUrl) {
const res = await fetch(networksUrl);
if (res.ok) {
const data = (await res.json()) as { networks?: { chainName?: string; chainIdDecimal?: number }[] };
const data = (await res.json()) as {
/**
*
*/
networks?: {
/**
*
*/
chainName?: string; /**
*
*/
chainIdDecimal?: number;
}[];
};
const nets = data.networks ?? [];
chainNames = nets.map((n) => `${n.chainName ?? ''} (${n.chainIdDecimal ?? ''})`).join(', ') || 'None';
chainNames =
nets
.map((n) => `${n.chainName ?? ''} (${n.chainIdDecimal ?? ''})`)
.join(', ') || 'None';
}
} else if (base) {
const data = await fetchNetworks(base);
const networks = data.networks ?? [];
chainNames = networks.map((net) => `${net.chainName} (${net.chainIdDecimal})`).join(', ') || 'None';
chainNames =
networks
.map((net) => `${net.chainName} (${net.chainIdDecimal})`)
.join(', ') || 'None';
}
const displayTokenListUrl = tokenListUrl || (base ? `${base}/api/v1/report/token-list` : '');
const displayTokenListUrl =
tokenListUrl || (base ? `${base}/api/v1/report/token-list` : '');
return snap.request({
method: 'snap_dialog',
params: {
@@ -344,7 +519,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
tokens: [],
};
}
const chainIdParam = (params?.chainId as number | undefined) ?? 138;
const chainIdParam = params?.chainId ?? 138;
try {
const res = await fetch(
`${base}/api/v1/tokens?chainId=${chainIdParam}&limit=50`,
@@ -353,12 +528,35 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
throw new Error(`HTTP ${res.status}`);
}
const data = (await res.json()) as {
tokens?: Array<{
/**
*
*/
tokens?: {
/**
*
*/
symbol?: string;
/**
*
*/
name?: string;
/**
*
*/
address?: string;
market?: { priceUsd?: number; volume24h?: number };
}>;
/**
*
*/
market?: {
/**
*
*/
priceUsd?: number; /**
*
*/
volume24h?: number;
};
}[];
};
const tokens = (data.tokens ?? []).map((t) => ({
symbol: t.symbol,
@@ -388,7 +586,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
amountOut: undefined,
};
}
const chainIdParam = (params?.chainId as number | undefined) ?? 138;
const chainIdParam = params?.chainId ?? 138;
const tokenIn = params?.tokenIn as string | undefined;
const tokenOut = params?.tokenOut as string | undefined;
const amountIn = params?.amountIn as string | undefined;
@@ -406,8 +604,17 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
url.searchParams.set('amountIn', String(amountIn));
const res = await fetch(url.toString());
const data = (await res.json()) as {
/**
*
*/
amountOut?: string | null;
/**
*
*/
error?: string;
/**
*
*/
poolAddress?: string | null;
};
if (!res.ok) {
@@ -449,7 +656,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
},
});
}
const chainIdParam = (params?.chainId as number | undefined) ?? 138;
const chainIdParam = params?.chainId ?? 138;
const tokenIn = params?.tokenIn as string | undefined;
const tokenOut = params?.tokenOut as string | undefined;
const amountIn = params?.amountIn as string | undefined;
@@ -477,7 +684,13 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
url.searchParams.set('amountIn', String(amountIn));
const res = await fetch(url.toString());
const data = (await res.json()) as {
/**
*
*/
amountOut?: string | null;
/**
*
*/
error?: string;
};
if (!res.ok || data.error) {
@@ -533,7 +746,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
}
case 'get_bridge_routes': {
const bridgeUrl = bridgeListUrl || (base ? `${base}/api/v1/bridge/routes` : '');
const bridgeUrl =
bridgeListUrl || (base ? `${base}/api/v1/bridge/routes` : '');
if (!bridgeUrl) {
return {
error: 'Pass apiBaseUrl or bridgeListUrl to fetch bridge routes',
@@ -543,9 +757,17 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
}
try {
const res = await fetch(bridgeUrl);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = (await res.json()) as {
/**
*
*/
routes?: Record<string, Record<string, string>>;
/**
*
*/
chain138Bridges?: Record<string, string>;
};
return {
@@ -565,7 +787,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
}
case 'show_bridge_routes': {
const showBridgeUrl = bridgeListUrl || (base ? `${base}/api/v1/bridge/routes` : '');
const showBridgeUrl =
bridgeListUrl || (base ? `${base}/api/v1/bridge/routes` : '');
if (!showBridgeUrl) {
return snap.request({
method: 'snap_dialog',
@@ -574,7 +797,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
content: (
<Box>
<Text>
Pass apiBaseUrl or bridgeListUrl to see bridge routes (CCIP WETH9 / WETH10).
Pass apiBaseUrl or bridgeListUrl to see bridge routes (CCIP
and Trustless).
</Text>
</Box>
),
@@ -587,25 +811,50 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
throw new Error(`HTTP ${res.status}`);
}
const data = (await res.json()) as {
/**
*
*/
routes?: Record<string, Record<string, string>>;
/**
*
*/
chain138Bridges?: Record<string, string>;
};
const lines: string[] = [];
if (data.chain138Bridges) {
// CCIP (WETH9 / WETH10)
if (data.chain138Bridges?.weth9 || data.chain138Bridges?.weth10) {
lines.push('CCIP');
if (data.chain138Bridges.weth9) {
lines.push(
` WETH9 (138): ${String(data.chain138Bridges.weth9).slice(0, 10)}...`,
);
}
if (data.chain138Bridges.weth10) {
lines.push(
` WETH10 (138): ${String(data.chain138Bridges.weth10).slice(0, 10)}...`,
);
}
if (data.routes?.weth9?.['Ethereum Mainnet (1)']) {
lines.push(' WETH9 → Ethereum Mainnet');
}
if (data.routes?.weth10?.['Ethereum Mainnet (1)']) {
lines.push(' WETH10 → Ethereum Mainnet');
}
lines.push('');
}
// Trustless bridge (Lockbox on 138)
if (data.chain138Bridges?.trustless) {
lines.push('Trustless');
lines.push(
`WETH9 Bridge (138): ${String(data.chain138Bridges.weth9 ?? '').slice(0, 10)}...`,
);
lines.push(
`WETH10 Bridge (138): ${String(data.chain138Bridges.weth10 ?? '').slice(0, 10)}...`,
` Lockbox (138): ${String(data.chain138Bridges.trustless).slice(0, 10)}...`,
);
if (data.routes?.trustless?.['Ethereum Mainnet (1)']) {
lines.push(' Trustless → Ethereum Mainnet');
} else {
lines.push(' → Ethereum Mainnet (use explorer to transfer)');
}
lines.push('');
}
if (data.routes?.weth9?.['Ethereum Mainnet (1)']) {
lines.push('WETH9 → Ethereum Mainnet');
}
if (data.routes?.weth10?.['Ethereum Mainnet (1)']) {
lines.push('WETH10 → Ethereum Mainnet');
}
lines.push('');
lines.push('Use the explorer to execute bridge transfers.');
return snap.request({
method: 'snap_dialog',
@@ -614,7 +863,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
content: (
<Box>
<Text>
<Bold>CCIP Bridge Routes</Bold>
<Bold>Bridge Routes (CCIP + Trustless)</Bold>
</Text>
<Text>{lines.join('\n')}</Text>
</Box>
@@ -639,6 +888,92 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
}
}
case 'get_token_mapping': {
const fromChain = params?.fromChain ?? 138;
const toChain = params?.toChain ?? 651940;
const address = params?.address?.trim();
if (!base) {
return {
error:
'Pass apiBaseUrl to use token-mapping API (cross-chain address resolution).',
mapping: undefined,
resolvedAddress: undefined,
};
}
try {
if (address) {
const res = await fetch(
`${base}/api/v1/token-mapping/resolve?fromChain=${fromChain}&toChain=${toChain}&address=${encodeURIComponent(address)}`,
);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = (await res.json()) as {
/**
*
*/
addressOnTarget?: string | null;
/**
*
*/
addressOnSource?: string;
/**
*
*/
error?: string;
};
return {
resolvedAddress: data.addressOnTarget ?? undefined,
error: data.error,
};
}
const res = await fetch(
`${base}/api/v1/token-mapping?fromChain=${fromChain}&toChain=${toChain}`,
);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = (await res.json()) as {
/**
*
*/
tokens?: unknown[];
/**
*
*/
addressMapFromTo?: Record<string, string>;
/**
*
*/
addressMapToFrom?: Record<string, string>;
/**
*
*/
error?: string;
};
if (data.error) {
return { error: data.error, mapping: undefined };
}
return {
mapping: {
tokens: data.tokens,
addressMapFromTo: data.addressMapFromTo,
addressMapToFrom: data.addressMapToFrom,
},
error: undefined,
};
} catch (error) {
return {
error:
error instanceof Error
? error.message
: 'Failed to fetch token mapping',
mapping: undefined,
resolvedAddress: undefined,
};
}
}
case 'show_market_data': {
if (!base) {
return snap.request({
@@ -655,7 +990,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
},
});
}
const chainIdParam = (params?.chainId as number | undefined) ?? 138;
const chainIdParam = params?.chainId ?? 138;
try {
const res = await fetch(
`${base}/api/v1/tokens?chainId=${chainIdParam}&limit=20`,
@@ -664,11 +999,28 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
throw new Error(`HTTP ${res.status}`);
}
const data = (await res.json()) as {
tokens?: Array<{
/**
*
*/
tokens?: {
/**
*
*/
symbol?: string;
/**
*
*/
name?: string;
market?: { priceUsd?: number };
}>;
/**
*
*/
market?: {
/**
*
*/
priceUsd?: number;
};
}[];
};
const tokens = data.tokens ?? [];
const lines =

View File

@@ -8,7 +8,7 @@ import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: 'html',

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Build the Snap companion site for https://explorer.d-bis.org/snap/
# Uses GATSBY_SNAP_API_BASE_URL=https://explorer.d-bis.org so Market data, Bridge, Swap cards work.
# For that to work, explorer.d-bis.org must serve the token-aggregation API at /api/v1/... (deploy
# smom-dbis-138/services/token-aggregation and proxy it, or set GATSBY_SNAP_API_BASE_URL to the API host).
# Output: packages/site/public/ (upload to /var/www/html/snap/ on VMID 5000).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT"
export GATSBY_PATH_PREFIX=/snap
export GATSBY_SNAP_API_BASE_URL="${GATSBY_SNAP_API_BASE_URL:-https://explorer.d-bis.org}"
# So "Send on Chain 138" link is absolute HTTPS (avoids redirect to http and mixed-content).
export GATSBY_SNAP_SITE_URL="${GATSBY_SNAP_SITE_URL:-https://explorer.d-bis.org}"
# Production: use npm snap so MetaMask does not try to fetch localhost:8080 (GATSBY_* is inlined into client bundle).
export GATSBY_SNAP_ORIGIN="${GATSBY_SNAP_ORIGIN:-npm:chain138-snap}"
# Required for Gatsby to apply pathPrefix to script/asset URLs (see path prefix docs).
export PREFIX_PATHS=1
echo "Building Snap site: GATSBY_PATH_PREFIX=$GATSBY_PATH_PREFIX GATSBY_SNAP_SITE_URL=$GATSBY_SNAP_SITE_URL PREFIX_PATHS=$PREFIX_PATHS GATSBY_SNAP_ORIGIN=$GATSBY_SNAP_ORIGIN GATSBY_SNAP_API_BASE_URL=$GATSBY_SNAP_API_BASE_URL"
pnpm --filter site run build
echo "Done. Output in packages/site/public/ — deploy to /var/www/html/snap/ on explorer VM (VMID 5000)."

View File

@@ -1,144 +0,0 @@
#!/bin/bash
# Deploy Chain 138 Snap companion site to VMID 5000 (explorer host).
# Serves the site at https://explorer.d-bis.org/snap/
# Requires: built site (run with --build to build first), Proxmox host with pct or SSH.
set -euo pipefail
VMID=5000
VM_IP="${EXPLORER_VM_IP:-192.168.11.140}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SNAP_REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SITE_PUBLIC="${SNAP_REPO_ROOT}/packages/site/public"
PROXMOX_HOST="${PROXMOX_HOST_R630_02:-192.168.11.12}"
BUILD_FIRST=false
for arg in "$@"; do
[ "$arg" = "--build" ] && BUILD_FIRST=true
done
echo "=========================================="
echo "Deploy Chain 138 Snap site to VMID $VMID"
echo "=========================================="
echo ""
if [ "$BUILD_FIRST" = true ]; then
echo "=== Building site (pathPrefix=/snap) ==="
BUILD_ENV="GATSBY_PATH_PREFIX=/snap GATSBY_BUILD_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
[ -n "${GATSBY_SNAP_API_BASE_URL:-}" ] && BUILD_ENV="$BUILD_ENV GATSBY_SNAP_API_BASE_URL=$GATSBY_SNAP_API_BASE_URL"
(cd "$SNAP_REPO_ROOT" && eval "$BUILD_ENV" pnpm --filter site run build)
echo ""
fi
if [ ! -f "${SITE_PUBLIC}/index.html" ]; then
echo "❌ Site not built. Run from repo root: GATSBY_PATH_PREFIX=/snap pnpm --filter site run build"
echo " Or run this script with: $0 --build"
echo " For production API (market/bridge/swap): GATSBY_SNAP_API_BASE_URL=https://your-api.com $0 --build"
exit 1
fi
# Detect run context
if [ -f "/proc/1/cgroup" ] && grep -q "lxc" /proc/1/cgroup 2>/dev/null; then
echo "Running inside VMID $VMID"
DEPLOY_METHOD="direct"
run_in_vm() { "$@"; }
elif command -v pct &>/dev/null; then
echo "Running from Proxmox host (pct exec $VMID)"
DEPLOY_METHOD="pct"
run_in_vm() { pct exec $VMID -- "$@"; }
else
echo "Running from remote (SSH to $PROXMOX_HOST, then pct to $VMID)"
DEPLOY_METHOD="remote"
run_in_vm() { ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@"${PROXMOX_HOST}" "pct exec $VMID -- $*"; }
fi
echo "=== Creating tarball of site ==="
TARBALL="/tmp/snap-site-deploy-$$.tar"
(cd "$SITE_PUBLIC" && tar -cf "$TARBALL" .)
cleanup_tarball() { rm -f "$TARBALL"; }
trap cleanup_tarball EXIT
echo "✅ Tarball: $TARBALL"
# Keep last tarball for rollback (on host: /tmp/snap-site-last.tar; in VM: previous files overwritten)
LAST_TARBALL="/tmp/snap-site-last.tar"
cp "$TARBALL" "$LAST_TARBALL" 2>/dev/null || true
echo "✅ Rollback tarball saved: $LAST_TARBALL"
echo ""
echo "=== Deploying to /var/www/html/snap/ ==="
# Optional: backup current deploy for rollback (inside VM)
run_in_vm "mkdir -p /var/www/html/snap"
run_in_vm "tar -cf /var/www/html/snap-rollback.tar -C /var/www/html/snap . 2>/dev/null || true"
if [ "$DEPLOY_METHOD" = "direct" ]; then
tar -xf "$TARBALL" -C /var/www/html/snap
chown -R www-data:www-data /var/www/html/snap
elif [ "$DEPLOY_METHOD" = "remote" ]; then
TARNAME="$(basename "$TARBALL")"
scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$TARBALL" root@"${PROXMOX_HOST}":/tmp/
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@"${PROXMOX_HOST}" "pct push $VMID /tmp/$TARNAME /tmp/snap-deploy.tar"
run_in_vm "tar -xf /tmp/snap-deploy.tar -C /var/www/html/snap"
run_in_vm "rm -f /tmp/snap-deploy.tar"
run_in_vm "chown -R www-data:www-data /var/www/html/snap"
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@"${PROXMOX_HOST}" "rm -f /tmp/$TARNAME"
else
pct push $VMID "$TARBALL" /tmp/snap-deploy.tar
run_in_vm "tar -xf /tmp/snap-deploy.tar -C /var/www/html/snap"
run_in_vm "rm -f /tmp/snap-deploy.tar"
run_in_vm "chown -R www-data:www-data /var/www/html/snap"
fi
echo "✅ Files deployed"
echo ""
echo "=== Nginx: ensure /snap/ is served ==="
if run_in_vm "grep -q 'location /snap/' /etc/nginx/sites-available/blockscout 2>/dev/null"; then
echo "✅ Nginx already has location /snap/"
run_in_vm "nginx -t && systemctl reload nginx" 2>/dev/null || true
else
echo "⚠️ Add location /snap/ to nginx on VMID $VMID (e.g. run explorer-monorepo scripts/fix-nginx-serve-custom-frontend.sh inside the VM)"
fi
echo ""
echo "=== Verification checks ==="
VERIFY_FAIL=0
if run_in_vm "test -f /var/www/html/snap/index.html"; then
echo "✅ /var/www/html/snap/index.html exists"
else
echo "❌ /var/www/html/snap/index.html missing"
VERIFY_FAIL=1
fi
if run_in_vm "grep -q 'location /snap/' /etc/nginx/sites-available/blockscout 2>/dev/null"; then
echo "✅ Nginx config has location /snap/"
else
echo "❌ Nginx config missing location /snap/"
VERIFY_FAIL=1
fi
SNAP_CODE="$(run_in_vm "curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 5 http://127.0.0.1/snap/ 2>/dev/null" 2>/dev/null || echo "000")"
if [ "$SNAP_CODE" = "200" ]; then
echo "✅ http://localhost/snap/ returns 200"
else
echo "❌ http://localhost/snap/ returned $SNAP_CODE (expected 200)"
VERIFY_FAIL=1
fi
SNAP_BODY="$(run_in_vm "curl -sS --connect-timeout 5 http://127.0.0.1/snap/ 2>/dev/null | head -c 4096" 2>/dev/null || true)"
if echo "$SNAP_BODY" | grep -qE 'Connect|template-snap|Snap|MetaMask'; then
echo "✅ /snap/ response contains Snap app content"
else
echo "⚠️ /snap/ response may not contain expected content (check in browser)"
fi
if [ "$VERIFY_FAIL" -eq 1 ]; then
echo ""
echo "⚠️ Some checks failed; see above. Snap may still work if nginx is updated."
fi
echo ""
echo "=========================================="
echo "Deployment complete"
echo "=========================================="
echo "Snap site should be available at:"
echo " - https://explorer.d-bis.org/snap/"
echo " - http://${VM_IP}/snap/"
echo ""
echo "Run full verification: metamask-integration/chain138-snap/scripts/verify-snap-site-vmid5000.sh"
echo "Or explorer + snap: explorer-monorepo/scripts/verify-vmid5000-all.sh"
echo ""
echo "Rollback: re-deploy previous build with: run_in_vm 'tar -xf /var/www/html/snap-rollback.tar -C /var/www/html/snap' (or use $LAST_TARBALL from host)."
echo ""

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Run automatable items from docs/PRE_PUBLISH_TESTING.md (build, test, package contents, optional lint).
# Usage: [SKIP_LINT=1] [SKIP_E2E=1] bash scripts/verify-pre-publish.sh
set -e
cd "$(dirname "$0")/.."
ROOT="$PWD"
echo "=== Verify pre-publish (automatable items) ==="
echo "[1/5] Build..."
pnpm run build
echo "[2/5] Unit tests..."
pnpm run test
echo "[3/5] Package contents..."
SNAP_DIR="$ROOT/packages/snap"
for f in dist/bundle.js images/icon.svg snap.manifest.json; do
if [ ! -f "$SNAP_DIR/$f" ]; then
echo "Missing: packages/snap/$f"
exit 1
fi
done
echo " dist/bundle.js, images/icon.svg, snap.manifest.json OK"
echo "[4/5] Manifest vs package.json version..."
MANIFEST_VER=$(jq -r .version "$SNAP_DIR/snap.manifest.json")
PKG_VER=$(jq -r .version "$SNAP_DIR/package.json")
if [ "$MANIFEST_VER" != "$PKG_VER" ]; then
echo "Version mismatch: snap.manifest.json=$MANIFEST_VER package.json=$PKG_VER"
exit 1
fi
echo " Version $MANIFEST_VER OK"
if [ "${SKIP_LINT:-0}" != "1" ]; then
echo "[5/5] Lint (Prettier only; ESLint may have existing rules)..."
pnpm run lint:misc --check
else
echo "[5/5] Lint skipped (SKIP_LINT=1)"
fi
if [ "${SKIP_E2E:-0}" != "1" ]; then
echo "[E2E] Playwright (optional)..."
if pnpm run test:e2e 2>/dev/null; then
echo " E2E passed"
else
echo " E2E failed or not run (run 'npx playwright install' once if needed)"
fi
else
echo "[E2E] Skipped (SKIP_E2E=1)"
fi
echo "=== Automatable checks done. Complete manual items in docs/PRE_PUBLISH_TESTING.md ==="

View File

@@ -0,0 +1,159 @@
#!/usr/bin/env bash
# Verify token-aggregation API reachability, CORS, token logoURIs, and network iconUrls.
# Usage: ./verify-snap-api-and-icons.sh [API_BASE_URL]
# API_BASE_URL defaults to https://explorer.d-bis.org or from GATSBY_SNAP_API_BASE_URL.
# Requires: curl, jq
set -euo pipefail
API_BASE="${1:-${GATSBY_SNAP_API_BASE_URL:-https://explorer.d-bis.org}}"
API_BASE="${API_BASE%/}"
TOKEN_LIST_URL="${API_BASE}/api/v1/report/token-list?chainId=138"
NETWORKS_URL="${API_BASE}/api/v1/networks"
PASS=0
FAIL=0
check() {
if "$@"; then
((PASS++)) || true
return 0
else
((FAIL++)) || true
return 1
fi
}
echo "=============================================="
echo "Chain 138 Snap — API and Icons Verification"
echo "API base: $API_BASE"
echo "=============================================="
echo ""
# 1. Token list reachable
echo "1. Token list API reachable"
if body=$(curl -sS -L --connect-timeout 15 --max-time 30 "$TOKEN_LIST_URL" 2>/dev/null) && [ -n "$body" ]; then
if echo "$body" | jq -e . &>/dev/null; then
if echo "$body" | jq -e '.tokens' &>/dev/null; then
echo "$TOKEN_LIST_URL returns valid token list JSON"
((PASS++)) || true
else
echo "$TOKEN_LIST_URL returns JSON but no .tokens (proxy may route to wrong backend)"
((FAIL++)) || true
fi
else
echo "$TOKEN_LIST_URL returns invalid JSON"
((FAIL++)) || true
fi
else
echo "$TOKEN_LIST_URL failed to fetch"
((FAIL++)) || true
body=""
fi
echo ""
# 2. Networks API reachable
echo "2. Networks API reachable"
if net_body=$(curl -sS -L --connect-timeout 15 --max-time 30 "$NETWORKS_URL" 2>/dev/null) && [ -n "$net_body" ]; then
if echo "$net_body" | jq -e . &>/dev/null; then
if echo "$net_body" | jq -e '.networks' &>/dev/null; then
echo "$NETWORKS_URL returns valid networks JSON"
((PASS++)) || true
else
echo "$NETWORKS_URL returns JSON but no .networks (proxy may route to wrong backend)"
((FAIL++)) || true
fi
else
echo "$NETWORKS_URL returns invalid JSON"
((FAIL++)) || true
fi
else
echo "$NETWORKS_URL failed to fetch"
((FAIL++)) || true
net_body=""
fi
echo ""
# 3. CORS headers (allow browser/MetaMask fetch)
echo "3. CORS headers"
cors_headers=$(curl -sS -I -X OPTIONS -H "Origin: https://explorer.d-bis.org" -H "Access-Control-Request-Method: GET" "$TOKEN_LIST_URL" 2>/dev/null || true)
if echo "$cors_headers" | grep -qi "access-control-allow-origin"; then
echo " ✅ CORS headers present (token-aggregation uses cors())"
((PASS++)) || true
else
echo " ⚠ CORS headers not detected (OPTIONS preflight). GET may still work if server allows *."
echo " Token-aggregation uses cors() by default; verify in browser if issues occur."
fi
echo ""
# 4. Every token has logoURI
echo "4. Token logoURI"
if [ -n "$body" ]; then
missing=$(echo "$body" | jq -r '.tokens[]? | select(.logoURI == null or .logoURI == "") | .symbol' 2>/dev/null || true)
if [ -z "$missing" ]; then
count=$(echo "$body" | jq '.tokens | length' 2>/dev/null || echo 0)
echo " ✅ All $count tokens have logoURI"
((PASS++)) || true
else
echo " ❌ Tokens missing logoURI: $missing"
((FAIL++)) || true
fi
else
echo " ⏭ Skipped (token list not fetched)"
fi
echo ""
# 5. List-level logoURI
echo "5. List-level logoURI"
if [ -n "$body" ]; then
list_logo=$(echo "$body" | jq -r '.logoURI // empty' 2>/dev/null)
if [ -n "$list_logo" ]; then
echo " ✅ List logoURI: $list_logo"
((PASS++)) || true
else
echo " ⚠ List-level logoURI missing (optional)"
fi
else
echo " ⏭ Skipped (token list not fetched)"
fi
echo ""
# 6. Network iconUrls
echo "6. Network iconUrls"
if [ -n "$net_body" ]; then
missing=$(echo "$net_body" | jq -r '.networks[]? | select(.iconUrls == null or (.iconUrls | length) == 0) | "\(.chainName) (\(.chainIdDecimal))"' 2>/dev/null || true)
if [ -z "$missing" ]; then
count=$(echo "$net_body" | jq '.networks | length' 2>/dev/null || echo 0)
echo " ✅ All $count networks have iconUrls"
((PASS++)) || true
else
echo " ❌ Networks missing iconUrls: $missing"
((FAIL++)) || true
fi
else
echo " ⏭ Skipped (networks not fetched)"
fi
echo ""
# 7. Sample logo URL reachable
echo "7. Sample logo URLs"
if [ -n "$body" ]; then
sample_logo=$(echo "$body" | jq -r '.tokens[0].logoURI // empty' 2>/dev/null)
if [ -n "$sample_logo" ]; then
if curl -sS -o /dev/null -w "%{http_code}" -L --connect-timeout 10 "$sample_logo" 2>/dev/null | grep -qE "^(200|301|302)$"; then
echo " ✅ Sample logo reachable: ${sample_logo:0:60}..."
((PASS++)) || true
else
echo " ⚠ Sample logo may be unreachable: $sample_logo"
fi
fi
fi
echo ""
echo "=============================================="
echo "Result: $PASS passed, $FAIL failed"
echo "=============================================="
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0

View File

@@ -1,82 +0,0 @@
#!/usr/bin/env bash
# Verify Chain 138 Snap site deployment on VMID 5000.
# Usage: ./verify-snap-site-vmid5000.sh [BASE_URL]
# BASE_URL defaults to https://explorer.d-bis.org (or use http://192.168.11.140 for LAN)
set -euo pipefail
BASE_URL="${1:-https://explorer.d-bis.org}"
BASE_URL="${BASE_URL%/}"
VMID=5000
VM_IP="${EXPLORER_VM_IP:-192.168.11.140}"
PROXMOX_HOST="${PROXMOX_HOST_R630_02:-192.168.11.12}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PASS=0
FAIL=0
check() {
local name="$1"
if eval "$2"; then
echo "$name"
((PASS++)) || true
return 0
else
echo "$name"
((FAIL++)) || true
return 1
fi
}
echo "=============================================="
echo "Snap site (VMID $VMID) verification"
echo "BASE_URL=$BASE_URL"
echo "=============================================="
echo ""
# 1) Public URL /snap/ returns 200 (follow redirects)
HTTP_CODE="$(curl -sS -L -o /dev/null -w "%{http_code}" --connect-timeout 10 "$BASE_URL/snap/" 2>/dev/null || echo 000)"
check "$BASE_URL/snap/ returns 200" "[ \"$HTTP_CODE\" = \"200\" ]"
# 2) /snap/ response contains Snap app content (follow redirects)
SNAP_BODY="$(curl -sS -L --connect-timeout 10 "$BASE_URL/snap/" 2>/dev/null | head -c 8192)" || true
check "/snap/ contains Snap app content (Connect|Snap|MetaMask)" "echo \"$SNAP_BODY\" | grep -qE 'Connect|template-snap|Snap|MetaMask'"
# 3) /snap/index.html returns 200 (follow redirects)
HTTP_CODE="$(curl -sS -L -o /dev/null -w "%{http_code}" --connect-timeout 10 "$BASE_URL/snap/index.html" 2>/dev/null || echo 000)"
check "$BASE_URL/snap/index.html returns 200" "[ \"$HTTP_CODE\" = \"200\" ]"
# 4) Optional: /snap/version.json returns 200 and valid JSON (build version/health)
VERSION_CODE="$(curl -sS -L -o /dev/null -w "%{http_code}" --connect-timeout 5 "$BASE_URL/snap/version.json" 2>/dev/null || echo 000)"
if [ "$VERSION_CODE" = "200" ]; then
echo "$BASE_URL/snap/version.json returns 200 (build version/health)"
((PASS++)) || true
else
echo "$BASE_URL/snap/version.json returned $VERSION_CODE (optional; set prebuild to generate)"
fi
# 6) Optional: when pct or SSH available, check inside VM
if command -v pct &>/dev/null; then
if pct exec $VMID -- test -f /var/www/html/snap/index.html 2>/dev/null; then
echo "✅ /var/www/html/snap/index.html exists in VM"
((PASS++)) || true
fi
if pct exec $VMID -- grep -q 'location /snap/' /etc/nginx/sites-available/blockscout 2>/dev/null; then
echo "✅ Nginx has location /snap/ in VM"
((PASS++)) || true
fi
elif ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no root@"${PROXMOX_HOST}" "pct exec $VMID -- test -f /var/www/html/snap/index.html" 2>/dev/null; then
echo "✅ /var/www/html/snap/index.html exists in VM (via SSH)"
((PASS++)) || true
fi
echo ""
echo "=============================================="
echo "Result: $PASS passed, $FAIL failed"
echo "=============================================="
if [ "$FAIL" -gt 0 ]; then
echo ""
echo "See: $SCRIPT_DIR/../DEPLOY_VMID5000.md"
exit 1
fi
exit 0